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 validation
import is_valid_uuid
# To check that User/Project Names don't look like UUIDs
27 from base_topic
import BaseTopic
, EngineException
29 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
32 class UserTopic(BaseTopic
):
35 schema_new
= user_new_schema
36 schema_edit
= user_edit_schema
39 def __init__(self
, db
, fs
, msg
):
40 BaseTopic
.__init
__(self
, db
, fs
, msg
)
43 def _get_project_filter(session
):
45 Generates a filter dictionary for querying database users.
46 Current policy is admin can show all, non admin, only its own user.
47 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
50 if session
["admin"]: # allows all
53 return {"username": session
["username"]}
55 def check_conflict_on_new(self
, session
, indata
):
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
)
60 if not session
["force"]:
61 for p
in indata
.get("projects"):
62 # To allow project addressing by Name as well as ID
63 if not self
.db
.get_one("projects", {BaseTopic
.id_field("projects", p
): p
}, fail_on_empty
=False,
65 raise EngineException("project '{}' does not exist".format(p
), HTTPStatus
.CONFLICT
)
67 def check_conflict_on_del(self
, session
, _id
):
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 # Removed so that the UUID is kept, to allow User Name modification
75 # content["_id"] = content["username"]
77 content
["_admin"]["salt"] = salt
78 if content
.get("password"):
79 content
["password"] = sha256(content
["password"].encode('utf-8') + salt
.encode('utf-8')).hexdigest()
82 def format_on_edit(final_content
, edit_content
):
83 BaseTopic
.format_on_edit(final_content
, edit_content
)
84 if edit_content
.get("password"):
86 final_content
["_admin"]["salt"] = salt
87 final_content
["password"] = sha256(edit_content
["password"].encode('utf-8') +
88 salt
.encode('utf-8')).hexdigest()
90 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
91 if not session
["admin"]:
92 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
93 # Names that look like UUIDs are not allowed
94 name
= (indata
if indata
else kwargs
).get("username")
95 if is_valid_uuid(name
):
96 raise EngineException("Usernames that look like UUIDs are not allowed",
97 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
98 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, content
=content
)
100 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
101 if not session
["admin"]:
102 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
103 # Names that look like UUIDs are not allowed
104 name
= indata
["username"] if indata
else kwargs
["username"]
105 if is_valid_uuid(name
):
106 raise EngineException("Usernames that look like UUIDs are not allowed",
107 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
108 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
)
111 class ProjectTopic(BaseTopic
):
113 topic_msg
= "projects"
114 schema_new
= project_new_schema
115 schema_edit
= project_edit_schema
118 def __init__(self
, db
, fs
, msg
):
119 BaseTopic
.__init
__(self
, db
, fs
, msg
)
122 def _get_project_filter(session
):
124 Generates a filter dictionary for querying database users.
125 Current policy is admin can show all, non admin, only its own user.
126 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
129 if session
["admin"]: # allows all
132 return {"_id.cont": session
["project_id"]}
134 def check_conflict_on_new(self
, session
, indata
):
135 if not indata
.get("name"):
136 raise EngineException("missing 'name'")
137 # check name not exists
138 if self
.db
.get_one(self
.topic
, {"name": indata
.get("name")}, fail_on_empty
=False, fail_on_more
=False):
139 raise EngineException("name '{}' exists".format(indata
["name"]), HTTPStatus
.CONFLICT
)
142 def format_on_new(content
, project_id
=None, make_public
=False):
143 BaseTopic
.format_on_new(content
, None)
144 # Removed so that the UUID is kept, to allow Project Name modification
145 # content["_id"] = content["name"]
147 def check_conflict_on_del(self
, session
, _id
):
148 if _id
in session
["project_id"]:
149 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
152 _filter
= {"projects": _id
}
153 if self
.db
.get_list("users", _filter
):
154 raise EngineException("There is some USER that contains this project", http_code
=HTTPStatus
.CONFLICT
)
156 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
157 if not session
["admin"]:
158 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
159 # Names that look like UUIDs are not allowed
160 name
= (indata
if indata
else kwargs
).get("name")
161 if is_valid_uuid(name
):
162 raise EngineException("Project names that look like UUIDs are not allowed",
163 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
164 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, content
=content
)
166 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
167 if not session
["admin"]:
168 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
169 # Names that look like UUIDs are not allowed
170 name
= indata
["name"] if indata
else kwargs
["name"]
171 if is_valid_uuid(name
):
172 raise EngineException("Project names that look like UUIDs are not allowed",
173 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
174 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
)
177 class VimAccountTopic(BaseTopic
):
178 topic
= "vim_accounts"
179 topic_msg
= "vim_account"
180 schema_new
= vim_account_new_schema
181 schema_edit
= vim_account_edit_schema
182 vim_config_encrypted
= ("admin_password", "nsx_password", "vcenter_password")
185 def __init__(self
, db
, fs
, msg
):
186 BaseTopic
.__init
__(self
, db
, fs
, msg
)
188 def check_conflict_on_new(self
, session
, indata
):
189 self
.check_unique_name(session
, indata
["name"], _id
=None)
191 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
192 if not session
["force"] and edit_content
.get("name"):
193 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
196 schema_version
= final_content
.get("schema_version")
198 if edit_content
.get("vim_password"):
199 final_content
["vim_password"] = self
.db
.encrypt(edit_content
["vim_password"],
200 schema_version
=schema_version
, salt
=_id
)
201 if edit_content
.get("config"):
202 for p
in self
.vim_config_encrypted
:
203 if edit_content
["config"].get(p
):
204 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
205 schema_version
=schema_version
, salt
=_id
)
207 def format_on_new(self
, content
, project_id
=None, make_public
=False):
208 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
209 content
["schema_version"] = schema_version
= "1.1"
212 if content
.get("vim_password"):
213 content
["vim_password"] = self
.db
.encrypt(content
["vim_password"], schema_version
=schema_version
,
215 if content
.get("config"):
216 for p
in self
.vim_config_encrypted
:
217 if content
["config"].get(p
):
218 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
221 content
["_admin"]["operationalState"] = "PROCESSING"
223 def delete(self
, session
, _id
, dry_run
=False):
225 Delete item by its internal _id
226 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
227 :param _id: server internal id
228 :param dry_run: make checking but do not delete
229 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
231 # TODO add admin to filter, validate rights
232 if dry_run
or session
["force"]: # delete completely
233 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
234 else: # if not, sent to kafka
235 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
236 self
.db
.set_one("vim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
237 self
._send
_msg
("delete", {"_id": _id
})
238 return v
# TODO indicate an offline operation to return 202 ACCEPTED
241 class WimAccountTopic(BaseTopic
):
242 topic
= "wim_accounts"
243 topic_msg
= "wim_account"
244 schema_new
= wim_account_new_schema
245 schema_edit
= wim_account_edit_schema
247 wim_config_encrypted
= ()
249 def __init__(self
, db
, fs
, msg
):
250 BaseTopic
.__init
__(self
, db
, fs
, msg
)
252 def check_conflict_on_new(self
, session
, indata
):
253 self
.check_unique_name(session
, indata
["name"], _id
=None)
255 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
256 if not session
["force"] and edit_content
.get("name"):
257 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
260 schema_version
= final_content
.get("schema_version")
262 if edit_content
.get("wim_password"):
263 final_content
["wim_password"] = self
.db
.encrypt(edit_content
["wim_password"],
264 schema_version
=schema_version
, salt
=_id
)
265 if edit_content
.get("config"):
266 for p
in self
.wim_config_encrypted
:
267 if edit_content
["config"].get(p
):
268 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
269 schema_version
=schema_version
, salt
=_id
)
271 def format_on_new(self
, content
, project_id
=None, make_public
=False):
272 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
273 content
["schema_version"] = schema_version
= "1.1"
276 if content
.get("wim_password"):
277 content
["wim_password"] = self
.db
.encrypt(content
["wim_password"], schema_version
=schema_version
,
279 if content
.get("config"):
280 for p
in self
.wim_config_encrypted
:
281 if content
["config"].get(p
):
282 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
285 content
["_admin"]["operationalState"] = "PROCESSING"
287 def delete(self
, session
, _id
, dry_run
=False):
289 Delete item by its internal _id
290 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
291 :param _id: server internal id
292 :param dry_run: make checking but do not delete
293 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
295 # TODO add admin to filter, validate rights
296 if dry_run
or session
["force"]: # delete completely
297 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
298 else: # if not, sent to kafka
299 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
300 self
.db
.set_one("wim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
301 self
._send
_msg
("delete", {"_id": _id
})
302 return v
# TODO indicate an offline operation to return 202 ACCEPTED
305 class SdnTopic(BaseTopic
):
308 schema_new
= sdn_new_schema
309 schema_edit
= sdn_edit_schema
312 def __init__(self
, db
, fs
, msg
):
313 BaseTopic
.__init
__(self
, db
, fs
, msg
)
315 def check_conflict_on_new(self
, session
, indata
):
316 self
.check_unique_name(session
, indata
["name"], _id
=None)
318 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
319 if not session
["force"] and edit_content
.get("name"):
320 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
323 schema_version
= final_content
.get("schema_version")
324 if schema_version
and edit_content
.get("password"):
325 final_content
["password"] = self
.db
.encrypt(edit_content
["password"], schema_version
=schema_version
,
328 def format_on_new(self
, content
, project_id
=None, make_public
=False):
329 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
330 content
["schema_version"] = schema_version
= "1.1"
332 if content
.get("password"):
333 content
["password"] = self
.db
.encrypt(content
["password"], schema_version
=schema_version
,
336 content
["_admin"]["operationalState"] = "PROCESSING"
338 def delete(self
, session
, _id
, dry_run
=False):
340 Delete item by its internal _id
341 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
342 :param _id: server internal id
343 :param dry_run: make checking but do not delete
344 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
346 if dry_run
or session
["force"]: # delete completely
347 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
348 else: # if not sent to kafka
349 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
350 self
.db
.set_one("sdns", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
351 self
._send
_msg
("delete", {"_id": _id
})
352 return v
# TODO indicate an offline operation to return 202 ACCEPTED
355 class UserTopicAuth(UserTopic
):
357 # topic_msg = "users"
358 # schema_new = user_new_schema
359 # schema_edit = user_edit_schema
361 def __init__(self
, db
, fs
, msg
, auth
):
362 UserTopic
.__init
__(self
, db
, fs
, msg
)
365 def check_conflict_on_new(self
, session
, indata
):
367 Check that the data to be inserted is valid
369 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
370 :param indata: data to be inserted
371 :return: None or raises EngineException
373 username
= indata
.get("username")
374 user_list
= list(map(lambda x
: x
["username"], self
.auth
.get_user_list()))
376 if username
in user_list
:
377 raise EngineException("username '{}' exists".format(username
), HTTPStatus
.CONFLICT
)
379 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
381 Check that the data to be edited/uploaded is valid
383 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
384 :param final_content: data once modified
385 :param edit_content: incremental data that contains the modifications to apply
386 :param _id: internal _id
387 :return: None or raises EngineException
389 users
= self
.auth
.get_user_list()
390 admin_user
= [user
for user
in users
if user
["name"] == "admin"][0]
392 if _id
== admin_user
["_id"] and edit_content
["project_role_mappings"]:
395 "role": "system_admin"
397 if elem
not in edit_content
:
398 raise EngineException("You cannot remove system_admin role from admin user",
399 http_code
=HTTPStatus
.FORBIDDEN
)
401 def check_conflict_on_del(self
, session
, _id
):
403 Check if deletion can be done because of dependencies if it is not force. To override
405 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
406 :param _id: internal _id
407 :return: None if ok or raises EngineException with the conflict
409 if _id
== session
["username"]:
410 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
413 def format_on_new(content
, project_id
=None, make_public
=False):
415 Modifies content descriptor to include _id.
417 NOTE: No password salt required because the authentication backend
418 should handle these security concerns.
420 :param content: descriptor to be modified
421 :param make_public: if included it is generated as public for reading.
422 :return: None, but content is modified
424 BaseTopic
.format_on_new(content
, make_public
=False)
425 content
["_id"] = content
["username"]
426 content
["password"] = content
["password"]
429 def format_on_edit(final_content
, edit_content
):
431 Modifies final_content descriptor to include the modified date.
433 NOTE: No password salt required because the authentication backend
434 should handle these security concerns.
436 :param final_content: final descriptor generated
437 :param edit_content: alterations to be include
438 :return: None, but final_content is modified
440 BaseTopic
.format_on_edit(final_content
, edit_content
)
441 if "password" in edit_content
:
442 final_content
["password"] = edit_content
["password"]
444 final_content
["project_role_mappings"] = edit_content
["project_role_mappings"]
446 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
448 Creates a new entry into the authentication backend.
450 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
452 :param rollback: list to append created items at database in case a rollback may to be done
453 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
454 :param indata: data to be inserted
455 :param kwargs: used to override the indata descriptor
456 :param headers: http request headers
457 :return: _id: identity of the inserted data.
460 content
= BaseTopic
._remove
_envelop
(indata
)
462 # Override descriptor with query string kwargs
463 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
464 content
= self
._validate
_input
_new
(content
, session
["force"])
465 self
.check_conflict_on_new(session
, content
)
466 self
.format_on_new(content
, session
["project_id"], make_public
=session
["public"])
467 _id
= self
.auth
.create_user(content
["username"], content
["password"])
468 rollback
.append({"topic": self
.topic
, "_id": _id
})
469 del content
["password"]
470 # self._send_msg("create", content)
472 except ValidationError
as e
:
473 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
475 def show(self
, session
, _id
):
477 Get complete information on an topic
479 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
480 :param _id: server internal id
481 :return: dictionary, raise exception if not found.
483 users
= [user
for user
in self
.auth
.get_user_list() if user
["_id"] == _id
]
488 raise EngineException("Too many users found", HTTPStatus
.CONFLICT
)
490 raise EngineException("User not found", HTTPStatus
.NOT_FOUND
)
492 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
494 Updates an user entry.
496 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
498 :param indata: data to be inserted
499 :param kwargs: used to override the indata descriptor
501 :return: _id: identity of the inserted data.
503 indata
= self
._remove
_envelop
(indata
)
505 # Override descriptor with query string kwargs
507 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
509 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
512 content
= self
.show(session
, _id
)
513 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
514 self
.format_on_edit(content
, indata
)
516 if "password" in content
:
517 self
.auth
.change_password(content
["name"], content
["password"])
519 users
= self
.auth
.get_user_list()
520 user
= [user
for user
in users
if user
["_id"] == content
["_id"]][0]
521 original_mapping
= []
522 edit_mapping
= content
["project_role_mappings"]
524 for project
in user
["projects"]:
525 for role
in project
["roles"]:
526 original_mapping
+= {
527 "project": project
["name"],
531 mappings_to_remove
= [mapping
for mapping
in original_mapping
532 if mapping
not in edit_mapping
]
534 mappings_to_add
= [mapping
for mapping
in edit_mapping
535 if mapping
not in original_mapping
]
537 for mapping
in mappings_to_remove
:
538 self
.auth
.remove_role_from_user(
544 for mapping
in mappings_to_add
:
545 self
.auth
.assign_role_to_user(
551 return content
["_id"]
552 except ValidationError
as e
:
553 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
555 def list(self
, session
, filter_q
=None):
557 Get a list of the topic that matches a filter
558 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
559 :param filter_q: filter of data to be applied
560 :return: The list, it can be empty if no one match the filter.
562 return self
.auth
.get_user_list()
564 def delete(self
, session
, _id
, dry_run
=False):
566 Delete item by its internal _id
568 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
569 :param _id: server internal id
570 :param force: indicates if deletion must be forced in case of conflict
571 :param dry_run: make checking but do not delete
572 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
574 self
.check_conflict_on_del(session
, _id
)
576 v
= self
.auth
.delete_user(_id
)
581 class ProjectTopicAuth(ProjectTopic
):
583 # topic_msg = "projects"
584 # schema_new = project_new_schema
585 # schema_edit = project_edit_schema
587 def __init__(self
, db
, fs
, msg
, auth
):
588 ProjectTopic
.__init
__(self
, db
, fs
, msg
)
591 def check_conflict_on_new(self
, session
, indata
):
593 Check that the data to be inserted is valid
595 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
596 :param indata: data to be inserted
597 :return: None or raises EngineException
599 project
= indata
.get("name")
600 project_list
= list(map(lambda x
: x
["name"], self
.auth
.get_project_list()))
602 if project
in project_list
:
603 raise EngineException("project '{}' exists".format(project
), HTTPStatus
.CONFLICT
)
605 def check_conflict_on_del(self
, session
, _id
):
607 Check if deletion can be done because of dependencies if it is not force. To override
609 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
610 :param _id: internal _id
611 :return: None if ok or raises EngineException with the conflict
613 projects
= self
.auth
.get_project_list()
614 current_project
= [project
for project
in projects
615 if project
["name"] == session
["project_id"]][0]
617 if _id
== current_project
["_id"]:
618 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
620 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
622 Creates a new entry into the authentication backend.
624 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
626 :param rollback: list to append created items at database in case a rollback may to be done
627 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
628 :param indata: data to be inserted
629 :param kwargs: used to override the indata descriptor
630 :param headers: http request headers
631 :return: _id: identity of the inserted data.
634 content
= BaseTopic
._remove
_envelop
(indata
)
636 # Override descriptor with query string kwargs
637 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
638 content
= self
._validate
_input
_new
(content
, session
["force"])
639 self
.check_conflict_on_new(session
, content
)
640 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=session
["public"])
641 _id
= self
.auth
.create_project(content
["name"])
642 rollback
.append({"topic": self
.topic
, "_id": _id
})
643 # self._send_msg("create", content)
645 except ValidationError
as e
:
646 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
648 def show(self
, session
, _id
):
650 Get complete information on an topic
652 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
653 :param _id: server internal id
654 :return: dictionary, raise exception if not found.
656 projects
= [project
for project
in self
.auth
.get_project_list() if project
["_id"] == _id
]
658 if len(projects
) == 1:
660 elif len(projects
) > 1:
661 raise EngineException("Too many projects found", HTTPStatus
.CONFLICT
)
663 raise EngineException("Project not found", HTTPStatus
.NOT_FOUND
)
665 def list(self
, session
, filter_q
=None):
667 Get a list of the topic that matches a filter
669 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
670 :param filter_q: filter of data to be applied
671 :return: The list, it can be empty if no one match the filter.
673 return self
.auth
.get_project_list()
675 def delete(self
, session
, _id
, dry_run
=False):
677 Delete item by its internal _id
679 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
680 :param _id: server internal id
681 :param dry_run: make checking but do not delete
682 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
684 self
.check_conflict_on_del(session
, _id
)
686 v
= self
.auth
.delete_project(_id
)
691 class RoleTopicAuth(BaseTopic
):
692 topic
= "roles_operations"
694 schema_new
= roles_new_schema
695 schema_edit
= roles_edit_schema
698 def __init__(self
, db
, fs
, msg
, auth
, ops
):
699 BaseTopic
.__init
__(self
, db
, fs
, msg
)
701 self
.operations
= ops
704 def validate_role_definition(operations
, role_definitions
):
706 Validates the role definition against the operations defined in
707 the resources to operations files.
709 :param operations: operations list
710 :param role_definitions: role definition to test
711 :return: None if ok, raises ValidationError exception on error
713 for role_def
in role_definitions
.keys():
716 if role_def
[-1] == ".":
717 raise ValidationError("Operation cannot end with \".\"")
719 role_def_matches
= [op
for op
in operations
if op
.startswith(role_def
)]
721 if len(role_def_matches
) == 0:
722 raise ValidationError("No matching operation found.")
724 def _validate_input_new(self
, input, force
=False):
726 Validates input user content for a new entry.
728 :param input: user input content for the new topic
729 :param force: may be used for being more tolerant
730 :return: The same input content, or a changed version of it.
733 validate_input(input, self
.schema_new
)
734 if "definition" in input and input["definition"]:
735 self
.validate_role_definition(self
.operations
, input["definition"])
738 def _validate_input_edit(self
, input, force
=False):
740 Validates input user content for updating an entry.
742 :param input: user input content for the new topic
743 :param force: may be used for being more tolerant
744 :return: The same input content, or a changed version of it.
747 validate_input(input, self
.schema_edit
)
748 if "definition" in input and input["definition"]:
749 self
.validate_role_definition(self
.operations
, input["definition"])
752 def check_conflict_on_new(self
, session
, indata
):
754 Check that the data to be inserted is valid
756 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
757 :param indata: data to be inserted
758 :return: None or raises EngineException
760 role
= indata
.get("name")
761 role_list
= list(map(lambda x
: x
["name"], self
.auth
.get_role_list()))
763 if role
in role_list
:
764 raise EngineException("role '{}' exists".format(role
), HTTPStatus
.CONFLICT
)
766 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
768 Check that the data to be edited/uploaded is valid
770 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
771 :param final_content: data once modified
772 :param edit_content: incremental data that contains the modifications to apply
773 :param _id: internal _id
774 :return: None or raises EngineException
776 roles
= self
.auth
.get_role_list()
777 system_admin_role
= [role
for role
in roles
778 if roles
["name"] == "system_admin"][0]
780 if _id
== system_admin_role
["_id"]:
781 raise EngineException("You cannot edit system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
783 def check_conflict_on_del(self
, session
, _id
):
785 Check if deletion can be done because of dependencies if it is not force. To override
787 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
788 :param _id: internal _id
789 :return: None if ok or raises EngineException with the conflict
791 roles
= self
.auth
.get_role_list()
792 system_admin_role
= [role
for role
in roles
793 if roles
["name"] == "system_admin"][0]
795 if _id
== system_admin_role
["_id"]:
796 raise EngineException("You cannot delete system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
799 def format_on_new(content
, project_id
=None, make_public
=False):
801 Modifies content descriptor to include _admin
803 :param content: descriptor to be modified
804 :param project_id: if included, it add project read/write permissions
805 :param make_public: if included it is generated as public for reading.
806 :return: None, but content is modified
809 if "_admin" not in content
:
810 content
["_admin"] = {}
811 if not content
["_admin"].get("created"):
812 content
["_admin"]["created"] = now
813 content
["_admin"]["modified"] = now
814 content
["root"] = False
816 # Saving the role definition
817 if "definition" in content
and content
["definition"]:
818 for role_def
, value
in content
["definition"].items():
820 content
["root"] = value
822 content
[role_def
.replace(".", ":")] = value
824 # Cleaning undesired values
825 if "definition" in content
:
826 del content
["definition"]
829 def format_on_edit(final_content
, edit_content
):
831 Modifies final_content descriptor to include the modified date.
833 :param final_content: final descriptor generated
834 :param edit_content: alterations to be include
835 :return: None, but final_content is modified
837 final_content
["_admin"]["modified"] = time()
839 ignore_fields
= ["_id", "name", "_admin"]
840 delete_keys
= [key
for key
in final_content
.keys() if key
not in ignore_fields
]
842 for key
in delete_keys
:
843 del final_content
[key
]
845 # Saving the role definition
846 if "definition" in edit_content
and edit_content
["definition"]:
847 for role_def
, value
in edit_content
["definition"].items():
849 final_content
["root"] = value
851 final_content
[role_def
.replace(".", ":")] = value
853 if "root" not in final_content
:
854 final_content
["root"] = False
857 def format_on_show(content
):
859 Modifies the content of the role information to separate the role
860 metadata from the role definition. Eases the reading process of the
863 :param definition: role definition to be processed
865 ignore_fields
= ["_admin", "_id", "name", "root"]
866 content_keys
= list(content
.keys())
867 definition
= dict(content
)
869 for key
in content_keys
:
870 if key
in ignore_fields
:
875 definition
[key
.replace(":", ".")] = definition
[key
]
879 content
["definition"] = definition
881 def show(self
, session
, _id
):
883 Get complete information on an topic
885 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
886 :param _id: server internal id
887 :return: dictionary, raise exception if not found.
889 filter_db
= self
._get
_project
_filter
(session
, write
=False, show_all
=True)
890 filter_db
["_id"] = _id
892 role
= self
.db
.get_one(self
.topic
, filter_db
)
893 new_role
= dict(role
)
894 self
.format_on_show(new_role
)
898 def list(self
, session
, filter_q
=None):
900 Get a list of the topic that matches a filter
902 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
903 :param filter_q: filter of data to be applied
904 :return: The list, it can be empty if no one match the filter.
909 roles
= self
.db
.get_list(self
.topic
, filter_q
)
913 new_role
= dict(role
)
914 self
.format_on_show(new_role
)
915 new_roles
.append(new_role
)
919 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
921 Creates a new entry into database.
923 :param rollback: list to append created items at database in case a rollback may to be done
924 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
925 :param indata: data to be inserted
926 :param kwargs: used to override the indata descriptor
927 :param headers: http request headers
928 :return: _id: identity of the inserted data.
931 content
= BaseTopic
._remove
_envelop
(indata
)
933 # Override descriptor with query string kwargs
934 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
935 content
= self
._validate
_input
_new
(content
, session
["force"])
936 self
.check_conflict_on_new(session
, content
)
937 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=session
["public"])
938 role_name
= content
["name"]
939 role
= self
.auth
.create_role(role_name
)
940 content
["_id"] = role
["_id"]
941 _id
= self
.db
.create(self
.topic
, content
)
942 rollback
.append({"topic": self
.topic
, "_id": _id
})
943 # self._send_msg("create", content)
945 except ValidationError
as e
:
946 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
948 def delete(self
, session
, _id
, dry_run
=False):
950 Delete item by its internal _id
952 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
953 :param _id: server internal id
954 :param dry_run: make checking but do not delete
955 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
957 self
.check_conflict_on_del(session
, _id
)
958 filter_q
= self
._get
_project
_filter
(session
, write
=True, show_all
=True)
959 filter_q
["_id"] = _id
961 self
.auth
.delete_role(_id
)
962 v
= self
.db
.del_one(self
.topic
, filter_q
)
966 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
968 Updates a role entry.
970 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
972 :param indata: data to be inserted
973 :param kwargs: used to override the indata descriptor
975 :return: _id: identity of the inserted data.
977 indata
= self
._remove
_envelop
(indata
)
979 # Override descriptor with query string kwargs
981 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
983 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
986 content
= self
.show(session
, _id
)
987 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
988 self
.format_on_edit(content
, indata
)
989 self
.db
.replace(self
.topic
, _id
, content
)
991 except ValidationError
as e
:
992 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)