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
38 def __init__(self
, db
, fs
, msg
):
39 BaseTopic
.__init
__(self
, db
, fs
, msg
)
42 def _get_project_filter(session
, write
=False, show_all
=True):
44 Generates a filter dictionary for querying database users.
45 Current policy is admin can show all, non admin, only its own user.
46 :param session: contains "username", if user is "admin" and the working "project_id"
47 :param write: if operation is for reading (False) or writing (True)
48 :param show_all: if True it will show public or
51 if session
["admin"]: # allows all
54 return {"username": session
["username"]}
56 def check_conflict_on_new(self
, session
, indata
, force
=False):
57 # check username not exists
58 if self
.db
.get_one(self
.topic
, {"username": indata
.get("username")}, fail_on_empty
=False, fail_on_more
=False):
59 raise EngineException("username '{}' exists".format(indata
["username"]), HTTPStatus
.CONFLICT
)
62 for p
in indata
["projects"]:
65 # To allow project addressing by Name as well as ID
66 if not self
.db
.get_one("projects", {BaseTopic
.id_field("projects", p
): p
}, fail_on_empty
=False,
68 raise EngineException("project '{}' does not exist".format(p
), HTTPStatus
.CONFLICT
)
70 def check_conflict_on_del(self
, session
, _id
, force
=False):
71 if _id
== session
["username"]:
72 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
75 def format_on_new(content
, project_id
=None, make_public
=False):
76 BaseTopic
.format_on_new(content
, make_public
=False)
77 # Removed so that the UUID is kept, to allow User Name modification
78 # content["_id"] = content["username"]
80 content
["_admin"]["salt"] = salt
81 if content
.get("password"):
82 content
["password"] = sha256(content
["password"].encode('utf-8') + salt
.encode('utf-8')).hexdigest()
85 def format_on_edit(final_content
, edit_content
):
86 BaseTopic
.format_on_edit(final_content
, edit_content
)
87 if edit_content
.get("password"):
89 final_content
["_admin"]["salt"] = salt
90 final_content
["password"] = sha256(edit_content
["password"].encode('utf-8') +
91 salt
.encode('utf-8')).hexdigest()
93 def edit(self
, session
, _id
, indata
=None, kwargs
=None, force
=False, content
=None):
94 if not session
["admin"]:
95 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
96 # Names that look like UUIDs are not allowed
97 name
= (indata
if indata
else kwargs
).get("username")
98 if is_valid_uuid(name
):
99 raise EngineException("Usernames that look like UUIDs are not allowed",
100 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
101 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, force
=force
, content
=content
)
103 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
104 if not session
["admin"]:
105 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
106 # Names that look like UUIDs are not allowed
107 name
= indata
["username"] if indata
else kwargs
["username"]
108 if is_valid_uuid(name
):
109 raise EngineException("Usernames that look like UUIDs are not allowed",
110 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
111 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
, force
=force
,
112 make_public
=make_public
)
115 class ProjectTopic(BaseTopic
):
117 topic_msg
= "projects"
118 schema_new
= project_new_schema
119 schema_edit
= project_edit_schema
121 def __init__(self
, db
, fs
, msg
):
122 BaseTopic
.__init
__(self
, db
, fs
, msg
)
124 def check_conflict_on_new(self
, session
, indata
, force
=False):
125 if not indata
.get("name"):
126 raise EngineException("missing 'name'")
127 # check name not exists
128 if self
.db
.get_one(self
.topic
, {"name": indata
.get("name")}, fail_on_empty
=False, fail_on_more
=False):
129 raise EngineException("name '{}' exists".format(indata
["name"]), HTTPStatus
.CONFLICT
)
132 def format_on_new(content
, project_id
=None, make_public
=False):
133 BaseTopic
.format_on_new(content
, None)
134 # Removed so that the UUID is kept, to allow Project Name modification
135 # content["_id"] = content["name"]
137 def check_conflict_on_del(self
, session
, _id
, force
=False):
138 if _id
== session
["project_id"]:
139 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
142 _filter
= {"projects": _id
}
143 if self
.db
.get_list("users", _filter
):
144 raise EngineException("There is some USER that contains this project", http_code
=HTTPStatus
.CONFLICT
)
146 def edit(self
, session
, _id
, indata
=None, kwargs
=None, force
=False, content
=None):
147 if not session
["admin"]:
148 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
149 # Names that look like UUIDs are not allowed
150 name
= (indata
if indata
else kwargs
).get("name")
151 if is_valid_uuid(name
):
152 raise EngineException("Project names that look like UUIDs are not allowed",
153 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
154 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, force
=force
, content
=content
)
156 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
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
["name"] if indata
else kwargs
["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
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
, force
=force
,
165 make_public
=make_public
)
168 class VimAccountTopic(BaseTopic
):
169 topic
= "vim_accounts"
170 topic_msg
= "vim_account"
171 schema_new
= vim_account_new_schema
172 schema_edit
= vim_account_edit_schema
173 vim_config_encrypted
= ("admin_password", "nsx_password", "vcenter_password")
175 def __init__(self
, db
, fs
, msg
):
176 BaseTopic
.__init
__(self
, db
, fs
, msg
)
178 def check_conflict_on_new(self
, session
, indata
, force
=False):
179 self
.check_unique_name(session
, indata
["name"], _id
=None)
181 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
182 if not force
and edit_content
.get("name"):
183 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
186 schema_version
= final_content
.get("schema_version")
188 if edit_content
.get("vim_password"):
189 final_content
["vim_password"] = self
.db
.encrypt(edit_content
["vim_password"],
190 schema_version
=schema_version
, salt
=_id
)
191 if edit_content
.get("config"):
192 for p
in self
.vim_config_encrypted
:
193 if edit_content
["config"].get(p
):
194 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
195 schema_version
=schema_version
, salt
=_id
)
197 def format_on_new(self
, content
, project_id
=None, make_public
=False):
198 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
199 content
["schema_version"] = schema_version
= "1.1"
202 if content
.get("vim_password"):
203 content
["vim_password"] = self
.db
.encrypt(content
["vim_password"], schema_version
=schema_version
,
205 if content
.get("config"):
206 for p
in self
.vim_config_encrypted
:
207 if content
["config"].get(p
):
208 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
211 content
["_admin"]["operationalState"] = "PROCESSING"
213 def delete(self
, session
, _id
, force
=False, dry_run
=False):
215 Delete item by its internal _id
216 :param session: contains the used login username, working project, and admin rights
217 :param _id: server internal id
218 :param force: indicates if deletion must be forced in case of conflict
219 :param dry_run: make checking but do not delete
220 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
222 # TODO add admin to filter, validate rights
223 if dry_run
or force
: # delete completely
224 return BaseTopic
.delete(self
, session
, _id
, force
, dry_run
)
225 else: # if not, sent to kafka
226 v
= BaseTopic
.delete(self
, session
, _id
, force
, dry_run
=True)
227 self
.db
.set_one("vim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
228 self
._send
_msg
("delete", {"_id": _id
})
229 return v
# TODO indicate an offline operation to return 202 ACCEPTED
232 class WimAccountTopic(BaseTopic
):
233 topic
= "wim_accounts"
234 topic_msg
= "wim_account"
235 schema_new
= wim_account_new_schema
236 schema_edit
= wim_account_edit_schema
237 wim_config_encrypted
= ()
239 def __init__(self
, db
, fs
, msg
):
240 BaseTopic
.__init
__(self
, db
, fs
, msg
)
242 def check_conflict_on_new(self
, session
, indata
, force
=False):
243 self
.check_unique_name(session
, indata
["name"], _id
=None)
245 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
246 if not force
and edit_content
.get("name"):
247 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
250 schema_version
= final_content
.get("schema_version")
252 if edit_content
.get("wim_password"):
253 final_content
["wim_password"] = self
.db
.encrypt(edit_content
["wim_password"],
254 schema_version
=schema_version
, salt
=_id
)
255 if edit_content
.get("config"):
256 for p
in self
.wim_config_encrypted
:
257 if edit_content
["config"].get(p
):
258 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
259 schema_version
=schema_version
, salt
=_id
)
261 def format_on_new(self
, content
, project_id
=None, make_public
=False):
262 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
263 content
["schema_version"] = schema_version
= "1.1"
266 if content
.get("wim_password"):
267 content
["wim_password"] = self
.db
.encrypt(content
["wim_password"], schema_version
=schema_version
,
269 if content
.get("config"):
270 for p
in self
.wim_config_encrypted
:
271 if content
["config"].get(p
):
272 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
275 content
["_admin"]["operationalState"] = "PROCESSING"
277 def delete(self
, session
, _id
, force
=False, dry_run
=False):
279 Delete item by its internal _id
280 :param session: contains the used login username, working project, and admin rights
281 :param _id: server internal id
282 :param force: indicates if deletion must be forced in case of conflict
283 :param dry_run: make checking but do not delete
284 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
286 # TODO add admin to filter, validate rights
287 if dry_run
or force
: # delete completely
288 return BaseTopic
.delete(self
, session
, _id
, force
, dry_run
)
289 else: # if not, sent to kafka
290 v
= BaseTopic
.delete(self
, session
, _id
, force
, dry_run
=True)
291 self
.db
.set_one("wim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
292 self
._send
_msg
("delete", {"_id": _id
})
293 return v
# TODO indicate an offline operation to return 202 ACCEPTED
296 class SdnTopic(BaseTopic
):
299 schema_new
= sdn_new_schema
300 schema_edit
= sdn_edit_schema
302 def __init__(self
, db
, fs
, msg
):
303 BaseTopic
.__init
__(self
, db
, fs
, msg
)
305 def check_conflict_on_new(self
, session
, indata
, force
=False):
306 self
.check_unique_name(session
, indata
["name"], _id
=None)
308 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
309 if not force
and edit_content
.get("name"):
310 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
313 schema_version
= final_content
.get("schema_version")
314 if schema_version
and edit_content
.get("password"):
315 final_content
["password"] = self
.db
.encrypt(edit_content
["password"], schema_version
=schema_version
,
318 def format_on_new(self
, content
, project_id
=None, make_public
=False):
319 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
320 content
["schema_version"] = schema_version
= "1.1"
322 if content
.get("password"):
323 content
["password"] = self
.db
.encrypt(content
["password"], schema_version
=schema_version
,
326 content
["_admin"]["operationalState"] = "PROCESSING"
328 def delete(self
, session
, _id
, force
=False, dry_run
=False):
330 Delete item by its internal _id
331 :param session: contains the used login username, working project, and admin rights
332 :param _id: server internal id
333 :param force: indicates if deletion must be forced in case of conflict
334 :param dry_run: make checking but do not delete
335 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
337 if dry_run
or force
: # delete completely
338 return BaseTopic
.delete(self
, session
, _id
, force
, dry_run
)
339 else: # if not sent to kafka
340 v
= BaseTopic
.delete(self
, session
, _id
, force
, dry_run
=True)
341 self
.db
.set_one("sdns", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
342 self
._send
_msg
("delete", {"_id": _id
})
343 return v
# TODO indicate an offline operation to return 202 ACCEPTED
346 class UserTopicAuth(UserTopic
):
349 schema_new
= user_new_schema
350 schema_edit
= user_edit_schema
352 def __init__(self
, db
, fs
, msg
, auth
):
353 UserTopic
.__init
__(self
, db
, fs
, msg
)
356 def check_conflict_on_new(self
, session
, indata
, force
=False):
358 Check that the data to be inserted is valid
360 :param session: contains "username", if user is "admin" and the working "project_id"
361 :param indata: data to be inserted
362 :param force: boolean. With force it is more tolerant
363 :return: None or raises EngineException
365 username
= indata
.get("username")
366 user_list
= list(map(lambda x
: x
["username"], self
.auth
.get_user_list()))
368 if username
in user_list
:
369 raise EngineException("username '{}' exists".format(username
), HTTPStatus
.CONFLICT
)
371 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
373 Check that the data to be edited/uploaded is valid
375 :param session: contains "username", if user is "admin" and the working "project_id"
376 :param final_content: data once modified
377 :param edit_content: incremental data that contains the modifications to apply
378 :param _id: internal _id
379 :param force: boolean. With force it is more tolerant
380 :return: None or raises EngineException
382 users
= self
.auth
.get_user_list()
383 admin_user
= [user
for user
in users
if user
["name"] == "admin"][0]
385 if _id
== admin_user
["_id"] and edit_content
["project_role_mappings"]:
388 "role": "system_admin"
390 if elem
not in edit_content
:
391 raise EngineException("You cannot remove system_admin role from admin user",
392 http_code
=HTTPStatus
.FORBIDDEN
)
394 def check_conflict_on_del(self
, session
, _id
, force
=False):
396 Check if deletion can be done because of dependencies if it is not force. To override
398 :param session: contains "username", if user is "admin" and the working "project_id"
399 :param _id: internal _id
400 :param force: Avoid this checking
401 :return: None if ok or raises EngineException with the conflict
403 if _id
== session
["username"]:
404 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
407 def format_on_new(content
, project_id
=None, make_public
=False):
409 Modifies content descriptor to include _id.
411 NOTE: No password salt required because the authentication backend
412 should handle these security concerns.
414 :param content: descriptor to be modified
415 :param make_public: if included it is generated as public for reading.
416 :return: None, but content is modified
418 BaseTopic
.format_on_new(content
, make_public
=False)
419 content
["_id"] = content
["username"]
420 content
["password"] = content
["password"]
423 def format_on_edit(final_content
, edit_content
):
425 Modifies final_content descriptor to include the modified date.
427 NOTE: No password salt required because the authentication backend
428 should handle these security concerns.
430 :param final_content: final descriptor generated
431 :param edit_content: alterations to be include
432 :return: None, but final_content is modified
434 BaseTopic
.format_on_edit(final_content
, edit_content
)
435 if "password" in edit_content
:
436 final_content
["password"] = edit_content
["password"]
438 final_content
["project_role_mappings"] = edit_content
["project_role_mappings"]
440 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
442 Creates a new entry into the authentication backend.
444 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
446 :param rollback: list to append created items at database in case a rollback may to be done
447 :param session: contains the used login username and working project
448 :param indata: data to be inserted
449 :param kwargs: used to override the indata descriptor
450 :param headers: http request headers
451 :param force: If True avoid some dependence checks
452 :param make_public: Make the created item public to all projects
453 :return: _id: identity of the inserted data.
456 content
= BaseTopic
._remove
_envelop
(indata
)
458 # Override descriptor with query string kwargs
459 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
460 content
= self
._validate
_input
_new
(content
, force
)
461 self
.check_conflict_on_new(session
, content
, force
=force
)
462 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=make_public
)
463 _id
= self
.auth
.create_user(content
["username"], content
["password"])
464 rollback
.append({"topic": self
.topic
, "_id": _id
})
465 del content
["password"]
466 # self._send_msg("create", content)
468 except ValidationError
as e
:
469 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
471 def show(self
, session
, _id
):
473 Get complete information on an topic
475 :param session: contains the used login username and working project
476 :param _id: server internal id
477 :return: dictionary, raise exception if not found.
479 users
= [user
for user
in self
.auth
.get_user_list() if user
["_id"] == _id
]
484 raise EngineException("Too many users found", HTTPStatus
.CONFLICT
)
486 raise EngineException("User not found", HTTPStatus
.NOT_FOUND
)
488 def edit(self
, session
, _id
, indata
=None, kwargs
=None, force
=False, content
=None):
490 Updates an user entry.
492 :param session: contains the used login username and working project
494 :param indata: data to be inserted
495 :param kwargs: used to override the indata descriptor
496 :param force: If True avoid some dependence checks
498 :return: _id: identity of the inserted data.
500 indata
= self
._remove
_envelop
(indata
)
502 # Override descriptor with query string kwargs
504 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
506 indata
= self
._validate
_input
_edit
(indata
, force
=force
)
509 content
= self
.show(session
, _id
)
510 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
, force
=force
)
511 self
.format_on_edit(content
, indata
)
513 if "password" in content
:
514 self
.auth
.change_password(content
["name"], content
["password"])
516 users
= self
.auth
.get_user_list()
517 user
= [user
for user
in users
if user
["_id"] == content
["_id"]][0]
518 original_mapping
= []
519 edit_mapping
= content
["project_role_mappings"]
521 for project
in user
["projects"]:
522 for role
in project
["roles"]:
523 original_mapping
+= {
524 "project": project
["name"],
528 mappings_to_remove
= [mapping
for mapping
in original_mapping
529 if mapping
not in edit_mapping
]
531 mappings_to_add
= [mapping
for mapping
in edit_mapping
532 if mapping
not in original_mapping
]
534 for mapping
in mappings_to_remove
:
535 self
.auth
.remove_role_from_user(
541 for mapping
in mappings_to_add
:
542 self
.auth
.assign_role_to_user(
548 return content
["_id"]
549 except ValidationError
as e
:
550 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
552 def list(self
, session
, filter_q
=None):
554 Get a list of the topic that matches a filter
555 :param session: contains the used login username and working project
556 :param filter_q: filter of data to be applied
557 :return: The list, it can be empty if no one match the filter.
559 return self
.auth
.get_user_list()
561 def delete(self
, session
, _id
, force
=False, dry_run
=False):
563 Delete item by its internal _id
565 :param session: contains the used login username, working project, and admin rights
566 :param _id: server internal id
567 :param force: indicates if deletion must be forced in case of conflict
568 :param dry_run: make checking but do not delete
569 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
571 self
.check_conflict_on_del(session
, _id
, force
)
573 v
= self
.auth
.delete_user(_id
)
578 class ProjectTopicAuth(ProjectTopic
):
580 topic_msg
= "projects"
581 schema_new
= project_new_schema
582 schema_edit
= project_edit_schema
584 def __init__(self
, db
, fs
, msg
, auth
):
585 ProjectTopic
.__init
__(self
, db
, fs
, msg
)
588 def check_conflict_on_new(self
, session
, indata
, force
=False):
590 Check that the data to be inserted is valid
592 :param session: contains "username", if user is "admin" and the working "project_id"
593 :param indata: data to be inserted
594 :param force: boolean. With force it is more tolerant
595 :return: None or raises EngineException
597 project
= indata
.get("name")
598 project_list
= list(map(lambda x
: x
["name"], self
.auth
.get_project_list()))
600 if project
in project_list
:
601 raise EngineException("project '{}' exists".format(project
), HTTPStatus
.CONFLICT
)
603 def check_conflict_on_del(self
, session
, _id
, force
=False):
605 Check if deletion can be done because of dependencies if it is not force. To override
607 :param session: contains "username", if user is "admin" and the working "project_id"
608 :param _id: internal _id
609 :param force: Avoid this checking
610 :return: None if ok or raises EngineException with the conflict
612 projects
= self
.auth
.get_project_list()
613 current_project
= [project
for project
in projects
614 if project
["name"] == session
["project_id"]][0]
616 if _id
== current_project
["_id"]:
617 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
619 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
621 Creates a new entry into the authentication backend.
623 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
625 :param rollback: list to append created items at database in case a rollback may to be done
626 :param session: contains the used login username and working project
627 :param indata: data to be inserted
628 :param kwargs: used to override the indata descriptor
629 :param headers: http request headers
630 :param force: If True avoid some dependence checks
631 :param make_public: Make the created item public to all projects
632 :return: _id: identity of the inserted data.
635 content
= BaseTopic
._remove
_envelop
(indata
)
637 # Override descriptor with query string kwargs
638 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
639 content
= self
._validate
_input
_new
(content
, force
)
640 self
.check_conflict_on_new(session
, content
, force
=force
)
641 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=make_public
)
642 _id
= self
.auth
.create_project(content
["name"])
643 rollback
.append({"topic": self
.topic
, "_id": _id
})
644 # self._send_msg("create", content)
646 except ValidationError
as e
:
647 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
649 def show(self
, session
, _id
):
651 Get complete information on an topic
653 :param session: contains the used login username and working project
654 :param _id: server internal id
655 :return: dictionary, raise exception if not found.
657 projects
= [project
for project
in self
.auth
.get_project_list() if project
["_id"] == _id
]
659 if len(projects
) == 1:
661 elif len(projects
) > 1:
662 raise EngineException("Too many projects found", HTTPStatus
.CONFLICT
)
664 raise EngineException("Project not found", HTTPStatus
.NOT_FOUND
)
666 def list(self
, session
, filter_q
=None):
668 Get a list of the topic that matches a filter
670 :param session: contains the used login username and working project
671 :param filter_q: filter of data to be applied
672 :return: The list, it can be empty if no one match the filter.
674 return self
.auth
.get_project_list()
676 def delete(self
, session
, _id
, force
=False, dry_run
=False):
678 Delete item by its internal _id
680 :param session: contains the used login username, working project, and admin rights
681 :param _id: server internal id
682 :param force: indicates if deletion must be forced in case of conflict
683 :param dry_run: make checking but do not delete
684 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
686 self
.check_conflict_on_del(session
, _id
, force
)
688 v
= self
.auth
.delete_project(_id
)
693 class RoleTopicAuth(BaseTopic
):
694 topic
= "roles_operations"
696 schema_new
= roles_new_schema
697 schema_edit
= roles_edit_schema
699 def __init__(self
, db
, fs
, msg
, auth
, ops
):
700 BaseTopic
.__init
__(self
, db
, fs
, msg
)
702 self
.operations
= ops
705 def validate_role_definition(operations
, role_definitions
):
707 Validates the role definition against the operations defined in
708 the resources to operations files.
710 :param operations: operations list
711 :param role_definitions: role definition to test
712 :return: None if ok, raises ValidationError exception on error
714 for role_def
in role_definitions
.keys():
717 if role_def
[-1] == ".":
718 raise ValidationError("Operation cannot end with \".\"")
720 role_def_matches
= [op
for op
in operations
if op
.startswith(role_def
)]
722 if len(role_def_matches
) == 0:
723 raise ValidationError("No matching operation found.")
725 def _validate_input_new(self
, input, force
=False):
727 Validates input user content for a new entry.
729 :param input: user input content for the new topic
730 :param force: may be used for being more tolerant
731 :return: The same input content, or a changed version of it.
734 validate_input(input, self
.schema_new
)
735 if "definition" in input and input["definition"]:
736 self
.validate_role_definition(self
.operations
, input["definition"])
739 def _validate_input_edit(self
, input, force
=False):
741 Validates input user content for updating an entry.
743 :param input: user input content for the new topic
744 :param force: may be used for being more tolerant
745 :return: The same input content, or a changed version of it.
748 validate_input(input, self
.schema_edit
)
749 if "definition" in input and input["definition"]:
750 self
.validate_role_definition(self
.operations
, input["definition"])
753 def check_conflict_on_new(self
, session
, indata
, force
=False):
755 Check that the data to be inserted is valid
757 :param session: contains "username", if user is "admin" and the working "project_id"
758 :param indata: data to be inserted
759 :param force: boolean. With force it is more tolerant
760 :return: None or raises EngineException
762 role
= indata
.get("name")
763 role_list
= list(map(lambda x
: x
["name"], self
.auth
.get_role_list()))
765 if role
in role_list
:
766 raise EngineException("role '{}' exists".format(role
), HTTPStatus
.CONFLICT
)
768 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
, force
=False):
770 Check that the data to be edited/uploaded is valid
772 :param session: contains "username", if user is "admin" and the working "project_id"
773 :param final_content: data once modified
774 :param edit_content: incremental data that contains the modifications to apply
775 :param _id: internal _id
776 :param force: boolean. With force it is more tolerant
777 :return: None or raises EngineException
779 roles
= self
.auth
.get_role_list()
780 system_admin_role
= [role
for role
in roles
781 if roles
["name"] == "system_admin"][0]
783 if _id
== system_admin_role
["_id"]:
784 raise EngineException("You cannot edit system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
786 def check_conflict_on_del(self
, session
, _id
, force
=False):
788 Check if deletion can be done because of dependencies if it is not force. To override
790 :param session: contains "username", if user is "admin" and the working "project_id"
791 :param _id: internal _id
792 :param force: Avoid this checking
793 :return: None if ok or raises EngineException with the conflict
795 roles
= self
.auth
.get_role_list()
796 system_admin_role
= [role
for role
in roles
797 if roles
["name"] == "system_admin"][0]
799 if _id
== system_admin_role
["_id"]:
800 raise EngineException("You cannot delete system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
803 def format_on_new(content
, project_id
=None, make_public
=False):
805 Modifies content descriptor to include _admin
807 :param content: descriptor to be modified
808 :param project_id: if included, it add project read/write permissions
809 :param make_public: if included it is generated as public for reading.
810 :return: None, but content is modified
813 if "_admin" not in content
:
814 content
["_admin"] = {}
815 if not content
["_admin"].get("created"):
816 content
["_admin"]["created"] = now
817 content
["_admin"]["modified"] = now
818 content
["root"] = False
820 # Saving the role definition
821 if "definition" in content
and content
["definition"]:
822 for role_def
, value
in content
["definition"].items():
824 content
["root"] = value
826 content
[role_def
.replace(".", ":")] = value
828 # Cleaning undesired values
829 if "definition" in content
:
830 del content
["definition"]
833 def format_on_edit(final_content
, edit_content
):
835 Modifies final_content descriptor to include the modified date.
837 :param final_content: final descriptor generated
838 :param edit_content: alterations to be include
839 :return: None, but final_content is modified
841 final_content
["_admin"]["modified"] = time()
843 ignore_fields
= ["_id", "name", "_admin"]
844 delete_keys
= [key
for key
in final_content
.keys() if key
not in ignore_fields
]
846 for key
in delete_keys
:
847 del final_content
[key
]
849 # Saving the role definition
850 if "definition" in edit_content
and edit_content
["definition"]:
851 for role_def
, value
in edit_content
["definition"].items():
853 final_content
["root"] = value
855 final_content
[role_def
.replace(".", ":")] = value
857 if "root" not in final_content
:
858 final_content
["root"] = False
861 def format_on_show(content
):
863 Modifies the content of the role information to separate the role
864 metadata from the role definition. Eases the reading process of the
867 :param definition: role definition to be processed
869 ignore_fields
= ["_admin", "_id", "name", "root"]
870 content_keys
= list(content
.keys())
871 definition
= dict(content
)
873 for key
in content_keys
:
874 if key
in ignore_fields
:
879 definition
[key
.replace(":", ".")] = definition
[key
]
883 content
["definition"] = definition
885 def show(self
, session
, _id
):
887 Get complete information on an topic
889 :param session: contains the used login username and working project
890 :param _id: server internal id
891 :return: dictionary, raise exception if not found.
893 filter_db
= self
._get
_project
_filter
(session
, write
=False, show_all
=True)
894 filter_db
["_id"] = _id
896 role
= self
.db
.get_one(self
.topic
, filter_db
)
897 new_role
= dict(role
)
898 self
.format_on_show(new_role
)
902 def list(self
, session
, filter_q
=None):
904 Get a list of the topic that matches a filter
906 :param session: contains the used login username and working project
907 :param filter_q: filter of data to be applied
908 :return: The list, it can be empty if no one match the filter.
913 roles
= self
.db
.get_list(self
.topic
, filter_q
)
917 new_role
= dict(role
)
918 self
.format_on_show(new_role
)
919 new_roles
.append(new_role
)
923 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None, force
=False, make_public
=False):
925 Creates a new entry into database.
927 :param rollback: list to append created items at database in case a rollback may to be done
928 :param session: contains the used login username and working project
929 :param indata: data to be inserted
930 :param kwargs: used to override the indata descriptor
931 :param headers: http request headers
932 :param force: If True avoid some dependence checks
933 :param make_public: Make the created item public to all projects
934 :return: _id: identity of the inserted data.
937 content
= BaseTopic
._remove
_envelop
(indata
)
939 # Override descriptor with query string kwargs
940 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
941 content
= self
._validate
_input
_new
(content
, force
)
942 self
.check_conflict_on_new(session
, content
, force
=force
)
943 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=make_public
)
944 role_name
= content
["name"]
945 role
= self
.auth
.create_role(role_name
)
946 content
["_id"] = role
["_id"]
947 _id
= self
.db
.create(self
.topic
, content
)
948 rollback
.append({"topic": self
.topic
, "_id": _id
})
949 # self._send_msg("create", content)
951 except ValidationError
as e
:
952 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
954 def delete(self
, session
, _id
, force
=False, dry_run
=False):
956 Delete item by its internal _id
958 :param session: contains the used login username, working project, and admin rights
959 :param _id: server internal id
960 :param force: indicates if deletion must be forced in case of conflict
961 :param dry_run: make checking but do not delete
962 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
964 self
.check_conflict_on_del(session
, _id
, force
)
965 filter_q
= self
._get
_project
_filter
(session
, write
=True, show_all
=True)
966 filter_q
["_id"] = _id
968 self
.auth
.delete_role(_id
)
969 v
= self
.db
.del_one(self
.topic
, filter_q
)
973 def edit(self
, session
, _id
, indata
=None, kwargs
=None, force
=False, content
=None):
975 Updates a role entry.
977 :param session: contains the used login username and working project
979 :param indata: data to be inserted
980 :param kwargs: used to override the indata descriptor
981 :param force: If True avoid some dependence checks
983 :return: _id: identity of the inserted data.
985 indata
= self
._remove
_envelop
(indata
)
987 # Override descriptor with query string kwargs
989 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
991 indata
= self
._validate
_input
_edit
(indata
, force
=force
)
994 content
= self
.show(session
, _id
)
995 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
, force
=force
)
996 self
.format_on_edit(content
, indata
)
997 self
.db
.replace(self
.topic
, _id
, content
)
999 except ValidationError
as e
:
1000 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)