1 # -*- coding: utf-8 -*-
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 from uuid
import uuid4
18 from hashlib
import sha256
19 from http
import HTTPStatus
21 from validation
import user_new_schema
, user_edit_schema
, project_new_schema
, project_edit_schema
22 from validation
import vim_account_new_schema
, vim_account_edit_schema
, sdn_new_schema
, sdn_edit_schema
23 from validation
import wim_account_new_schema
, wim_account_edit_schema
, roles_new_schema
, roles_edit_schema
24 from validation
import validate_input
25 from validation
import ValidationError
26 from base_topic
import BaseTopic
, EngineException
28 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
31 class UserTopic(BaseTopic
):
34 schema_new
= user_new_schema
35 schema_edit
= user_edit_schema
37 def __init__(self
, db
, fs
, msg
):
38 BaseTopic
.__init
__(self
, db
, fs
, msg
)
41 def _get_project_filter(session
, write
=False, show_all
=True):
43 Generates a filter dictionary for querying database users.
44 Current policy is admin can show all, non admin, only its own user.
45 :param session: contains "username", if user is "admin" and the working "project_id"
46 :param write: if operation is for reading (False) or writing (True)
47 :param show_all: if True it will show public or
50 if session
["admin"]: # allows all
53 return {"username": session
["username"]}
55 def check_conflict_on_new(self
, session
, indata
, force
=False):
56 # check username not exists
57 if self
.db
.get_one(self
.topic
, {"username": indata
.get("username")}, fail_on_empty
=False, fail_on_more
=False):
58 raise EngineException("username '{}' exists".format(indata
["username"]), HTTPStatus
.CONFLICT
)
61 for p
in indata
["projects"]:
64 if not self
.db
.get_one("projects", {"_id": p
}, fail_on_empty
=False, fail_on_more
=False):
65 raise EngineException("project '{}' does not exists".format(p
), HTTPStatus
.CONFLICT
)
67 def check_conflict_on_del(self
, session
, _id
, force
=False):
68 if _id
== session
["username"]:
69 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
72 def format_on_new(content
, project_id
=None, make_public
=False):
73 BaseTopic
.format_on_new(content
, make_public
=False)
74 content
["_id"] = content
["username"]
76 content
["_admin"]["salt"] = salt
77 if content
.get("password"):
78 content
["password"] = sha256(content
["password"].encode('utf-8') + salt
.encode('utf-8')).hexdigest()
81 def format_on_edit(final_content
, edit_content
):
82 BaseTopic
.format_on_edit(final_content
, edit_content
)
83 if edit_content
.get("password"):
85 final_content
["_admin"]["salt"] = salt
86 final_content
["password"] = sha256(edit_content
["password"].encode('utf-8') +
87 salt
.encode('utf-8')).hexdigest()
89 def edit(self
, session
, _id
, indata
=None, kwargs
=None, force
=False, content
=None):
90 if not session
["admin"]:
91 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
92 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, force
=force
, content
=content
)
94 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
95 if not session
["admin"]:
96 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
97 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
, force
=force
,
98 make_public
=make_public
)
101 class ProjectTopic(BaseTopic
):
103 topic_msg
= "projects"
104 schema_new
= project_new_schema
105 schema_edit
= project_edit_schema
107 def __init__(self
, db
, fs
, msg
):
108 BaseTopic
.__init
__(self
, db
, fs
, msg
)
110 def check_conflict_on_new(self
, session
, indata
, force
=False):
111 if not indata
.get("name"):
112 raise EngineException("missing 'name'")
113 # check name not exists
114 if self
.db
.get_one(self
.topic
, {"name": indata
.get("name")}, fail_on_empty
=False, fail_on_more
=False):
115 raise EngineException("name '{}' exists".format(indata
["name"]), HTTPStatus
.CONFLICT
)
118 def format_on_new(content
, project_id
=None, make_public
=False):
119 BaseTopic
.format_on_new(content
, None)
120 content
["_id"] = content
["name"]
122 def check_conflict_on_del(self
, session
, _id
, force
=False):
123 if _id
== session
["project_id"]:
124 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
127 _filter
= {"projects": _id
}
128 if self
.db
.get_list("users", _filter
):
129 raise EngineException("There is some USER that contains this project", http_code
=HTTPStatus
.CONFLICT
)
131 def edit(self
, session
, _id
, indata
=None, kwargs
=None, force
=False, content
=None):
132 if not session
["admin"]:
133 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
134 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, force
=force
, content
=content
)
136 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
137 if not session
["admin"]:
138 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
139 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
, force
=force
,
140 make_public
=make_public
)
143 class VimAccountTopic(BaseTopic
):
144 topic
= "vim_accounts"
145 topic_msg
= "vim_account"
146 schema_new
= vim_account_new_schema
147 schema_edit
= vim_account_edit_schema
148 vim_config_encrypted
= ("admin_password", "nsx_password", "vcenter_password")
150 def __init__(self
, db
, fs
, msg
):
151 BaseTopic
.__init
__(self
, db
, fs
, msg
)
153 def check_conflict_on_new(self
, session
, indata
, force
=False):
154 self
.check_unique_name(session
, indata
["name"], _id
=None)
156 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
157 if not force
and edit_content
.get("name"):
158 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
161 schema_version
= final_content
.get("schema_version")
163 if edit_content
.get("vim_password"):
164 final_content
["vim_password"] = self
.db
.encrypt(edit_content
["vim_password"],
165 schema_version
=schema_version
, salt
=_id
)
166 if edit_content
.get("config"):
167 for p
in self
.vim_config_encrypted
:
168 if edit_content
["config"].get(p
):
169 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
170 schema_version
=schema_version
, salt
=_id
)
172 def format_on_new(self
, content
, project_id
=None, make_public
=False):
173 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
174 content
["schema_version"] = schema_version
= "1.1"
177 if content
.get("vim_password"):
178 content
["vim_password"] = self
.db
.encrypt(content
["vim_password"], schema_version
=schema_version
,
180 if content
.get("config"):
181 for p
in self
.vim_config_encrypted
:
182 if content
["config"].get(p
):
183 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
186 content
["_admin"]["operationalState"] = "PROCESSING"
188 def delete(self
, session
, _id
, force
=False, dry_run
=False):
190 Delete item by its internal _id
191 :param session: contains the used login username, working project, and admin rights
192 :param _id: server internal id
193 :param force: indicates if deletion must be forced in case of conflict
194 :param dry_run: make checking but do not delete
195 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
197 # TODO add admin to filter, validate rights
198 if dry_run
or force
: # delete completely
199 return BaseTopic
.delete(self
, session
, _id
, force
, dry_run
)
200 else: # if not, sent to kafka
201 v
= BaseTopic
.delete(self
, session
, _id
, force
, dry_run
=True)
202 self
.db
.set_one("vim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
203 self
._send
_msg
("delete", {"_id": _id
})
204 return v
# TODO indicate an offline operation to return 202 ACCEPTED
207 class WimAccountTopic(BaseTopic
):
208 topic
= "wim_accounts"
209 topic_msg
= "wim_account"
210 schema_new
= wim_account_new_schema
211 schema_edit
= wim_account_edit_schema
212 wim_config_encrypted
= ()
214 def __init__(self
, db
, fs
, msg
):
215 BaseTopic
.__init
__(self
, db
, fs
, msg
)
217 def check_conflict_on_new(self
, session
, indata
, force
=False):
218 self
.check_unique_name(session
, indata
["name"], _id
=None)
220 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
221 if not force
and edit_content
.get("name"):
222 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
225 schema_version
= final_content
.get("schema_version")
227 if edit_content
.get("wim_password"):
228 final_content
["wim_password"] = self
.db
.encrypt(edit_content
["wim_password"],
229 schema_version
=schema_version
, salt
=_id
)
230 if edit_content
.get("config"):
231 for p
in self
.wim_config_encrypted
:
232 if edit_content
["config"].get(p
):
233 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
234 schema_version
=schema_version
, salt
=_id
)
236 def format_on_new(self
, content
, project_id
=None, make_public
=False):
237 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
238 content
["schema_version"] = schema_version
= "1.1"
241 if content
.get("wim_password"):
242 content
["wim_password"] = self
.db
.encrypt(content
["wim_password"], schema_version
=schema_version
,
244 if content
.get("config"):
245 for p
in self
.wim_config_encrypted
:
246 if content
["config"].get(p
):
247 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
250 content
["_admin"]["operationalState"] = "PROCESSING"
252 def delete(self
, session
, _id
, force
=False, dry_run
=False):
254 Delete item by its internal _id
255 :param session: contains the used login username, working project, and admin rights
256 :param _id: server internal id
257 :param force: indicates if deletion must be forced in case of conflict
258 :param dry_run: make checking but do not delete
259 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
261 # TODO add admin to filter, validate rights
262 if dry_run
or force
: # delete completely
263 return BaseTopic
.delete(self
, session
, _id
, force
, dry_run
)
264 else: # if not, sent to kafka
265 v
= BaseTopic
.delete(self
, session
, _id
, force
, dry_run
=True)
266 self
.db
.set_one("wim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
267 self
._send
_msg
("delete", {"_id": _id
})
268 return v
# TODO indicate an offline operation to return 202 ACCEPTED
271 class SdnTopic(BaseTopic
):
274 schema_new
= sdn_new_schema
275 schema_edit
= sdn_edit_schema
277 def __init__(self
, db
, fs
, msg
):
278 BaseTopic
.__init
__(self
, db
, fs
, msg
)
280 def check_conflict_on_new(self
, session
, indata
, force
=False):
281 self
.check_unique_name(session
, indata
["name"], _id
=None)
283 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
284 if not force
and edit_content
.get("name"):
285 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
288 schema_version
= final_content
.get("schema_version")
289 if schema_version
and edit_content
.get("password"):
290 final_content
["password"] = self
.db
.encrypt(edit_content
["password"], schema_version
=schema_version
,
293 def format_on_new(self
, content
, project_id
=None, make_public
=False):
294 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
295 content
["schema_version"] = schema_version
= "1.1"
297 if content
.get("password"):
298 content
["password"] = self
.db
.encrypt(content
["password"], schema_version
=schema_version
,
301 content
["_admin"]["operationalState"] = "PROCESSING"
303 def delete(self
, session
, _id
, force
=False, dry_run
=False):
305 Delete item by its internal _id
306 :param session: contains the used login username, working project, and admin rights
307 :param _id: server internal id
308 :param force: indicates if deletion must be forced in case of conflict
309 :param dry_run: make checking but do not delete
310 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
312 if dry_run
or force
: # delete completely
313 return BaseTopic
.delete(self
, session
, _id
, force
, dry_run
)
314 else: # if not sent to kafka
315 v
= BaseTopic
.delete(self
, session
, _id
, force
, dry_run
=True)
316 self
.db
.set_one("sdns", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
317 self
._send
_msg
("delete", {"_id": _id
})
318 return v
# TODO indicate an offline operation to return 202 ACCEPTED
321 class UserTopicAuth(UserTopic
):
324 schema_new
= user_new_schema
325 schema_edit
= user_edit_schema
327 def __init__(self
, db
, fs
, msg
, auth
):
328 UserTopic
.__init
__(self
, db
, fs
, msg
)
331 def check_conflict_on_new(self
, session
, indata
, force
=False):
333 Check that the data to be inserted is valid
335 :param session: contains "username", if user is "admin" and the working "project_id"
336 :param indata: data to be inserted
337 :param force: boolean. With force it is more tolerant
338 :return: None or raises EngineException
340 username
= indata
.get("username")
341 user_list
= list(map(lambda x
: x
["username"], self
.auth
.get_user_list()))
343 if username
in user_list
:
344 raise EngineException("username '{}' exists".format(username
), HTTPStatus
.CONFLICT
)
346 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
348 Check that the data to be edited/uploaded is valid
350 :param session: contains "username", if user is "admin" and the working "project_id"
351 :param final_content: data once modified
352 :param edit_content: incremental data that contains the modifications to apply
353 :param _id: internal _id
354 :param force: boolean. With force it is more tolerant
355 :return: None or raises EngineException
357 users
= self
.auth
.get_user_list()
358 admin_user
= [user
for user
in users
if user
["name"] == "admin"][0]
360 if _id
== admin_user
["_id"] and edit_content
["project_role_mappings"]:
363 "role": "system_admin"
365 if elem
not in edit_content
:
366 raise EngineException("You cannot remove system_admin role from admin user",
367 http_code
=HTTPStatus
.FORBIDDEN
)
369 def check_conflict_on_del(self
, session
, _id
, force
=False):
371 Check if deletion can be done because of dependencies if it is not force. To override
373 :param session: contains "username", if user is "admin" and the working "project_id"
374 :param _id: internal _id
375 :param force: Avoid this checking
376 :return: None if ok or raises EngineException with the conflict
378 if _id
== session
["username"]:
379 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
382 def format_on_new(content
, project_id
=None, make_public
=False):
384 Modifies content descriptor to include _id.
386 NOTE: No password salt required because the authentication backend
387 should handle these security concerns.
389 :param content: descriptor to be modified
390 :param make_public: if included it is generated as public for reading.
391 :return: None, but content is modified
393 BaseTopic
.format_on_new(content
, make_public
=False)
394 content
["_id"] = content
["username"]
395 content
["password"] = content
["password"]
398 def format_on_edit(final_content
, edit_content
):
400 Modifies final_content descriptor to include the modified date.
402 NOTE: No password salt required because the authentication backend
403 should handle these security concerns.
405 :param final_content: final descriptor generated
406 :param edit_content: alterations to be include
407 :return: None, but final_content is modified
409 BaseTopic
.format_on_edit(final_content
, edit_content
)
410 if "password" in edit_content
:
411 final_content
["password"] = edit_content
["password"]
413 final_content
["project_role_mappings"] = edit_content
["project_role_mappings"]
415 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
417 Creates a new entry into the authentication backend.
419 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
421 :param rollback: list to append created items at database in case a rollback may to be done
422 :param session: contains the used login username and working project
423 :param indata: data to be inserted
424 :param kwargs: used to override the indata descriptor
425 :param headers: http request headers
426 :param force: If True avoid some dependence checks
427 :param make_public: Make the created item public to all projects
428 :return: _id: identity of the inserted data.
431 content
= BaseTopic
._remove
_envelop
(indata
)
433 # Override descriptor with query string kwargs
434 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
435 content
= self
._validate
_input
_new
(content
, force
)
436 self
.check_conflict_on_new(session
, content
, force
=force
)
437 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=make_public
)
438 _id
= self
.auth
.create_user(content
["username"], content
["password"])
439 rollback
.append({"topic": self
.topic
, "_id": _id
})
440 del content
["password"]
441 # self._send_msg("create", content)
443 except ValidationError
as e
:
444 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
446 def show(self
, session
, _id
):
448 Get complete information on an topic
450 :param session: contains the used login username and working project
451 :param _id: server internal id
452 :return: dictionary, raise exception if not found.
454 users
= [user
for user
in self
.auth
.get_user_list() if user
["_id"] == _id
]
459 raise EngineException("Too many users found", HTTPStatus
.CONFLICT
)
461 raise EngineException("User not found", HTTPStatus
.NOT_FOUND
)
463 def edit(self
, session
, _id
, indata
=None, kwargs
=None, force
=False, content
=None):
465 Updates an user entry.
467 :param session: contains the used login username and working project
469 :param indata: data to be inserted
470 :param kwargs: used to override the indata descriptor
471 :param force: If True avoid some dependence checks
473 :return: _id: identity of the inserted data.
475 indata
= self
._remove
_envelop
(indata
)
477 # Override descriptor with query string kwargs
479 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
481 indata
= self
._validate
_input
_edit
(indata
, force
=force
)
484 content
= self
.show(session
, _id
)
485 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
, force
=force
)
486 self
.format_on_edit(content
, indata
)
488 if "password" in content
:
489 self
.auth
.change_password(content
["name"], content
["password"])
491 users
= self
.auth
.get_user_list()
492 user
= [user
for user
in users
if user
["_id"] == content
["_id"]][0]
493 original_mapping
= []
494 edit_mapping
= content
["project_role_mappings"]
496 for project
in user
["projects"]:
497 for role
in project
["roles"]:
498 original_mapping
+= {
499 "project": project
["name"],
503 mappings_to_remove
= [mapping
for mapping
in original_mapping
504 if mapping
not in edit_mapping
]
506 mappings_to_add
= [mapping
for mapping
in edit_mapping
507 if mapping
not in original_mapping
]
509 for mapping
in mappings_to_remove
:
510 self
.auth
.remove_role_from_user(
516 for mapping
in mappings_to_add
:
517 self
.auth
.assign_role_to_user(
523 return content
["_id"]
524 except ValidationError
as e
:
525 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
527 def list(self
, session
, filter_q
=None):
529 Get a list of the topic that matches a filter
530 :param session: contains the used login username and working project
531 :param filter_q: filter of data to be applied
532 :return: The list, it can be empty if no one match the filter.
534 return self
.auth
.get_user_list()
536 def delete(self
, session
, _id
, force
=False, dry_run
=False):
538 Delete item by its internal _id
540 :param session: contains the used login username, working project, and admin rights
541 :param _id: server internal id
542 :param force: indicates if deletion must be forced in case of conflict
543 :param dry_run: make checking but do not delete
544 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
546 self
.check_conflict_on_del(session
, _id
, force
)
548 v
= self
.auth
.delete_user(_id
)
553 class ProjectTopicAuth(ProjectTopic
):
555 topic_msg
= "projects"
556 schema_new
= project_new_schema
557 schema_edit
= project_edit_schema
559 def __init__(self
, db
, fs
, msg
, auth
):
560 ProjectTopic
.__init
__(self
, db
, fs
, msg
)
563 def check_conflict_on_new(self
, session
, indata
, force
=False):
565 Check that the data to be inserted is valid
567 :param session: contains "username", if user is "admin" and the working "project_id"
568 :param indata: data to be inserted
569 :param force: boolean. With force it is more tolerant
570 :return: None or raises EngineException
572 project
= indata
.get("name")
573 project_list
= list(map(lambda x
: x
["name"], self
.auth
.get_project_list()))
575 if project
in project_list
:
576 raise EngineException("project '{}' exists".format(project
), HTTPStatus
.CONFLICT
)
578 def check_conflict_on_del(self
, session
, _id
, force
=False):
580 Check if deletion can be done because of dependencies if it is not force. To override
582 :param session: contains "username", if user is "admin" and the working "project_id"
583 :param _id: internal _id
584 :param force: Avoid this checking
585 :return: None if ok or raises EngineException with the conflict
587 projects
= self
.auth
.get_project_list()
588 current_project
= [project
for project
in projects
589 if project
["name"] == session
["project_id"]][0]
591 if _id
== current_project
["_id"]:
592 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
594 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
596 Creates a new entry into the authentication backend.
598 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
600 :param rollback: list to append created items at database in case a rollback may to be done
601 :param session: contains the used login username and working project
602 :param indata: data to be inserted
603 :param kwargs: used to override the indata descriptor
604 :param headers: http request headers
605 :param force: If True avoid some dependence checks
606 :param make_public: Make the created item public to all projects
607 :return: _id: identity of the inserted data.
610 content
= BaseTopic
._remove
_envelop
(indata
)
612 # Override descriptor with query string kwargs
613 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
614 content
= self
._validate
_input
_new
(content
, force
)
615 self
.check_conflict_on_new(session
, content
, force
=force
)
616 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=make_public
)
617 _id
= self
.auth
.create_project(content
["name"])
618 rollback
.append({"topic": self
.topic
, "_id": _id
})
619 # self._send_msg("create", content)
621 except ValidationError
as e
:
622 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
624 def show(self
, session
, _id
):
626 Get complete information on an topic
628 :param session: contains the used login username and working project
629 :param _id: server internal id
630 :return: dictionary, raise exception if not found.
632 projects
= [project
for project
in self
.auth
.get_project_list() if project
["_id"] == _id
]
634 if len(projects
) == 1:
636 elif len(projects
) > 1:
637 raise EngineException("Too many projects found", HTTPStatus
.CONFLICT
)
639 raise EngineException("Project not found", HTTPStatus
.NOT_FOUND
)
641 def list(self
, session
, filter_q
=None):
643 Get a list of the topic that matches a filter
645 :param session: contains the used login username and working project
646 :param filter_q: filter of data to be applied
647 :return: The list, it can be empty if no one match the filter.
649 return self
.auth
.get_project_list()
651 def delete(self
, session
, _id
, force
=False, dry_run
=False):
653 Delete item by its internal _id
655 :param session: contains the used login username, working project, and admin rights
656 :param _id: server internal id
657 :param force: indicates if deletion must be forced in case of conflict
658 :param dry_run: make checking but do not delete
659 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
661 self
.check_conflict_on_del(session
, _id
, force
)
663 v
= self
.auth
.delete_project(_id
)
668 class RoleTopicAuth(BaseTopic
):
669 topic
= "roles_operations"
671 schema_new
= roles_new_schema
672 schema_edit
= roles_edit_schema
674 def __init__(self
, db
, fs
, msg
, auth
, ops
):
675 BaseTopic
.__init
__(self
, db
, fs
, msg
)
677 self
.operations
= ops
680 def validate_role_definition(operations
, role_definitions
):
682 Validates the role definition against the operations defined in
683 the resources to operations files.
685 :param operations: operations list
686 :param role_definitions: role definition to test
687 :return: None if ok, raises ValidationError exception on error
689 for role_def
in role_definitions
.keys():
692 if role_def
[-1] == ".":
693 raise ValidationError("Operation cannot end with \".\"")
695 role_def_matches
= [op
for op
in operations
if op
.starswith(role_def
)]
697 if len(role_def_matches
) == 0:
698 raise ValidationError("No matching operation found.")
700 def _validate_input_new(self
, input, force
=False):
702 Validates input user content for a new entry.
704 :param input: user input content for the new topic
705 :param force: may be used for being more tolerant
706 :return: The same input content, or a changed version of it.
709 validate_input(input, self
.schema_new
)
710 if "definition" in input and input["definition"]:
711 self
.validate_role_definition(self
.operations
, input["definition"])
714 def _validate_input_edit(self
, input, force
=False):
716 Validates input user content for updating an entry.
718 :param input: user input content for the new topic
719 :param force: may be used for being more tolerant
720 :return: The same input content, or a changed version of it.
723 validate_input(input, self
.schema_edit
)
724 if "definition" in input and input["definition"]:
725 self
.validate_role_definition(self
.operations
, input["definition"])
728 def check_conflict_on_new(self
, session
, indata
, force
=False):
730 Check that the data to be inserted is valid
732 :param session: contains "username", if user is "admin" and the working "project_id"
733 :param indata: data to be inserted
734 :param force: boolean. With force it is more tolerant
735 :return: None or raises EngineException
737 role
= indata
.get("name")
738 role_list
= list(map(lambda x
: x
["name"], self
.auth
.get_role_list()))
740 if role
in role_list
:
741 raise EngineException("role '{}' exists".format(role
), HTTPStatus
.CONFLICT
)
743 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
745 Check that the data to be edited/uploaded is valid
747 :param session: contains "username", if user is "admin" and the working "project_id"
748 :param final_content: data once modified
749 :param edit_content: incremental data that contains the modifications to apply
750 :param _id: internal _id
751 :param force: boolean. With force it is more tolerant
752 :return: None or raises EngineException
754 roles
= self
.auth
.get_role_list()
755 system_admin_role
= [role
for role
in roles
756 if roles
["name"] == "system_admin"][0]
758 if _id
== system_admin_role
["_id"]:
759 raise EngineException("You cannot edit system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
761 def check_conflict_on_del(self
, session
, _id
, force
=False):
763 Check if deletion can be done because of dependencies if it is not force. To override
765 :param session: contains "username", if user is "admin" and the working "project_id"
766 :param _id: internal _id
767 :param force: Avoid this checking
768 :return: None if ok or raises EngineException with the conflict
770 roles
= self
.auth
.get_role_list()
771 system_admin_role
= [role
for role
in roles
772 if roles
["name"] == "system_admin"][0]
774 if _id
== system_admin_role
["_id"]:
775 raise EngineException("You cannot delete system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
778 def format_on_new(content
, project_id
=None, make_public
=False):
780 Modifies content descriptor to include _admin
782 :param content: descriptor to be modified
783 :param project_id: if included, it add project read/write permissions
784 :param make_public: if included it is generated as public for reading.
785 :return: None, but content is modified
788 if "_admin" not in content
:
789 content
["_admin"] = {}
790 if not content
["_admin"].get("created"):
791 content
["_admin"]["created"] = now
792 content
["_admin"]["modified"] = now
793 content
["root"] = False
795 # Saving the role definition
796 if "definition" in content
and content
["definition"]:
797 for role_def
, value
in content
["definition"].items():
799 content
["root"] = value
801 content
[role_def
.replace(".", ":")] = value
803 # Cleaning undesired values
804 if "definition" in content
:
805 del content
["definition"]
808 def format_on_edit(final_content
, edit_content
):
810 Modifies final_content descriptor to include the modified date.
812 :param final_content: final descriptor generated
813 :param edit_content: alterations to be include
814 :return: None, but final_content is modified
816 final_content
["_admin"]["modified"] = time()
818 ignore_fields
= ["_id", "name", "_admin"]
819 delete_keys
= [key
for key
in final_content
.keys() if key
not in ignore_fields
]
821 for key
in delete_keys
:
822 del final_content
[key
]
824 # Saving the role definition
825 if "definition" in edit_content
and edit_content
["definition"]:
826 for role_def
, value
in edit_content
["definition"].items():
828 final_content
["root"] = value
830 final_content
[role_def
.replace(".", ":")] = value
832 if "root" not in final_content
:
833 final_content
["root"] = False
836 def format_on_show(content
):
838 Modifies the content of the role information to separate the role
839 metadata from the role definition. Eases the reading process of the
842 :param definition: role definition to be processed
844 ignore_fields
= ["_admin", "_id", "name", "root"]
845 content_keys
= list(content
.keys())
846 definition
= dict(content
)
848 for key
in content_keys
:
849 if key
in ignore_fields
:
854 definition
[key
.replace(":", ".")] = definition
[key
]
858 content
["definition"] = definition
860 def show(self
, session
, _id
):
862 Get complete information on an topic
864 :param session: contains the used login username and working project
865 :param _id: server internal id
866 :return: dictionary, raise exception if not found.
868 filter_db
= self
._get
_project
_filter
(session
, write
=False, show_all
=True)
869 filter_db
["_id"] = _id
871 role
= self
.db
.get_one(self
.topic
, filter_db
)
872 new_role
= dict(role
)
873 self
.format_on_show(new_role
)
877 def list(self
, session
, filter_q
=None):
879 Get a list of the topic that matches a filter
881 :param session: contains the used login username and working project
882 :param filter_q: filter of data to be applied
883 :return: The list, it can be empty if no one match the filter.
888 roles
= self
.db
.get_list(self
.topic
, filter_q
)
892 new_role
= dict(role
)
893 self
.format_on_show(new_role
)
894 new_roles
.append(new_role
)
898 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
900 Creates a new entry into database.
902 :param rollback: list to append created items at database in case a rollback may to be done
903 :param session: contains the used login username and working project
904 :param indata: data to be inserted
905 :param kwargs: used to override the indata descriptor
906 :param headers: http request headers
907 :param force: If True avoid some dependence checks
908 :param make_public: Make the created item public to all projects
909 :return: _id: identity of the inserted data.
912 content
= BaseTopic
._remove
_envelop
(indata
)
914 # Override descriptor with query string kwargs
915 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
916 content
= self
._validate
_input
_new
(content
, force
)
917 self
.check_conflict_on_new(session
, content
, force
=force
)
918 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=make_public
)
919 role_name
= content
["name"]
920 role
= self
.auth
.create_role(role_name
)
921 content
["_id"] = role
["_id"]
922 _id
= self
.db
.create(self
.topic
, content
)
923 rollback
.append({"topic": self
.topic
, "_id": _id
})
924 # self._send_msg("create", content)
926 except ValidationError
as e
:
927 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
929 def delete(self
, session
, _id
, force
=False, dry_run
=False):
931 Delete item by its internal _id
933 :param session: contains the used login username, working project, and admin rights
934 :param _id: server internal id
935 :param force: indicates if deletion must be forced in case of conflict
936 :param dry_run: make checking but do not delete
937 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
939 self
.check_conflict_on_del(session
, _id
, force
)
940 filter_q
= self
._get
_project
_filter
(session
, write
=True, show_all
=True)
941 filter_q
["_id"] = _id
943 self
.auth
.delete_role(_id
)
944 v
= self
.db
.del_one(self
.topic
, filter_q
)
948 def edit(self
, session
, _id
, indata
=None, kwargs
=None, force
=False, content
=None):
950 Updates a role entry.
952 :param session: contains the used login username and working project
954 :param indata: data to be inserted
955 :param kwargs: used to override the indata descriptor
956 :param force: If True avoid some dependence checks
958 :return: _id: identity of the inserted data.
960 indata
= self
._remove
_envelop
(indata
)
962 # Override descriptor with query string kwargs
964 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
966 indata
= self
._validate
_input
_edit
(indata
, force
=force
)
969 content
= self
.show(session
, _id
)
970 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
, force
=force
)
971 self
.format_on_edit(content
, indata
)
972 self
.db
.replace(self
.topic
, _id
, content
)
974 except ValidationError
as e
:
975 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)