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
, db_content
):
69 Check if deletion can be done because of dependencies if it is not force. To override
70 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
71 :param _id: internal _id
72 :param db_content: The database content of this item _id
73 :return: None if ok or raises EngineException with the conflict
75 if _id
== session
["username"]:
76 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
79 def format_on_new(content
, project_id
=None, make_public
=False):
80 BaseTopic
.format_on_new(content
, make_public
=False)
81 # Removed so that the UUID is kept, to allow User Name modification
82 # content["_id"] = content["username"]
84 content
["_admin"]["salt"] = salt
85 if content
.get("password"):
86 content
["password"] = sha256(content
["password"].encode('utf-8') + salt
.encode('utf-8')).hexdigest()
89 def format_on_edit(final_content
, edit_content
):
90 BaseTopic
.format_on_edit(final_content
, edit_content
)
91 if edit_content
.get("password"):
93 final_content
["_admin"]["salt"] = salt
94 final_content
["password"] = sha256(edit_content
["password"].encode('utf-8') +
95 salt
.encode('utf-8')).hexdigest()
97 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
98 if not session
["admin"]:
99 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
100 # Names that look like UUIDs are not allowed
101 name
= (indata
if indata
else kwargs
).get("username")
102 if is_valid_uuid(name
):
103 raise EngineException("Usernames that look like UUIDs are not allowed",
104 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
105 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, content
=content
)
107 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
108 if not session
["admin"]:
109 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
110 # Names that look like UUIDs are not allowed
111 name
= indata
["username"] if indata
else kwargs
["username"]
112 if is_valid_uuid(name
):
113 raise EngineException("Usernames that look like UUIDs are not allowed",
114 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
115 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
)
118 class ProjectTopic(BaseTopic
):
120 topic_msg
= "projects"
121 schema_new
= project_new_schema
122 schema_edit
= project_edit_schema
125 def __init__(self
, db
, fs
, msg
):
126 BaseTopic
.__init
__(self
, db
, fs
, msg
)
129 def _get_project_filter(session
):
131 Generates a filter dictionary for querying database users.
132 Current policy is admin can show all, non admin, only its own user.
133 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
136 if session
["admin"]: # allows all
139 return {"_id.cont": session
["project_id"]}
141 def check_conflict_on_new(self
, session
, indata
):
142 if not indata
.get("name"):
143 raise EngineException("missing 'name'")
144 # check name not exists
145 if self
.db
.get_one(self
.topic
, {"name": indata
.get("name")}, fail_on_empty
=False, fail_on_more
=False):
146 raise EngineException("name '{}' exists".format(indata
["name"]), HTTPStatus
.CONFLICT
)
149 def format_on_new(content
, project_id
=None, make_public
=False):
150 BaseTopic
.format_on_new(content
, None)
151 # Removed so that the UUID is kept, to allow Project Name modification
152 # content["_id"] = content["name"]
154 def check_conflict_on_del(self
, session
, _id
, db_content
):
156 Check if deletion can be done because of dependencies if it is not force. To override
157 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
158 :param _id: internal _id
159 :param db_content: The database content of this item _id
160 :return: None if ok or raises EngineException with the conflict
162 if _id
in session
["project_id"]:
163 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
166 _filter
= {"projects": _id
}
167 if self
.db
.get_list("users", _filter
):
168 raise EngineException("There is some USER that contains this project", http_code
=HTTPStatus
.CONFLICT
)
170 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
171 if not session
["admin"]:
172 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
173 # Names that look like UUIDs are not allowed
174 name
= (indata
if indata
else kwargs
).get("name")
175 if is_valid_uuid(name
):
176 raise EngineException("Project names that look like UUIDs are not allowed",
177 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
178 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, content
=content
)
180 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
181 if not session
["admin"]:
182 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
183 # Names that look like UUIDs are not allowed
184 name
= indata
["name"] if indata
else kwargs
["name"]
185 if is_valid_uuid(name
):
186 raise EngineException("Project names that look like UUIDs are not allowed",
187 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
188 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
)
191 class VimAccountTopic(BaseTopic
):
192 topic
= "vim_accounts"
193 topic_msg
= "vim_account"
194 schema_new
= vim_account_new_schema
195 schema_edit
= vim_account_edit_schema
196 vim_config_encrypted
= ("admin_password", "nsx_password", "vcenter_password")
199 def __init__(self
, db
, fs
, msg
):
200 BaseTopic
.__init
__(self
, db
, fs
, msg
)
202 def check_conflict_on_new(self
, session
, indata
):
203 self
.check_unique_name(session
, indata
["name"], _id
=None)
205 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
206 if not session
["force"] and edit_content
.get("name"):
207 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
210 schema_version
= final_content
.get("schema_version")
212 if edit_content
.get("vim_password"):
213 final_content
["vim_password"] = self
.db
.encrypt(edit_content
["vim_password"],
214 schema_version
=schema_version
, salt
=_id
)
215 if edit_content
.get("config"):
216 for p
in self
.vim_config_encrypted
:
217 if edit_content
["config"].get(p
):
218 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
219 schema_version
=schema_version
, salt
=_id
)
221 def format_on_new(self
, content
, project_id
=None, make_public
=False):
222 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
223 content
["schema_version"] = schema_version
= "1.1"
226 if content
.get("vim_password"):
227 content
["vim_password"] = self
.db
.encrypt(content
["vim_password"], schema_version
=schema_version
,
229 if content
.get("config"):
230 for p
in self
.vim_config_encrypted
:
231 if content
["config"].get(p
):
232 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
235 content
["_admin"]["operationalState"] = "PROCESSING"
237 def delete(self
, session
, _id
, dry_run
=False):
239 Delete item by its internal _id
240 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
241 :param _id: server internal id
242 :param dry_run: make checking but do not delete
243 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
245 # TODO add admin to filter, validate rights
246 if dry_run
or session
["force"]: # delete completely
247 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
248 else: # if not, sent to kafka
249 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
250 self
.db
.set_one("vim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
251 self
._send
_msg
("delete", {"_id": _id
})
252 return v
# TODO indicate an offline operation to return 202 ACCEPTED
255 class WimAccountTopic(BaseTopic
):
256 topic
= "wim_accounts"
257 topic_msg
= "wim_account"
258 schema_new
= wim_account_new_schema
259 schema_edit
= wim_account_edit_schema
261 wim_config_encrypted
= ()
263 def __init__(self
, db
, fs
, msg
):
264 BaseTopic
.__init
__(self
, db
, fs
, msg
)
266 def check_conflict_on_new(self
, session
, indata
):
267 self
.check_unique_name(session
, indata
["name"], _id
=None)
269 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
270 if not session
["force"] and edit_content
.get("name"):
271 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
274 schema_version
= final_content
.get("schema_version")
276 if edit_content
.get("wim_password"):
277 final_content
["wim_password"] = self
.db
.encrypt(edit_content
["wim_password"],
278 schema_version
=schema_version
, salt
=_id
)
279 if edit_content
.get("config"):
280 for p
in self
.wim_config_encrypted
:
281 if edit_content
["config"].get(p
):
282 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
283 schema_version
=schema_version
, salt
=_id
)
285 def format_on_new(self
, content
, project_id
=None, make_public
=False):
286 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
287 content
["schema_version"] = schema_version
= "1.1"
290 if content
.get("wim_password"):
291 content
["wim_password"] = self
.db
.encrypt(content
["wim_password"], schema_version
=schema_version
,
293 if content
.get("config"):
294 for p
in self
.wim_config_encrypted
:
295 if content
["config"].get(p
):
296 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
299 content
["_admin"]["operationalState"] = "PROCESSING"
301 def delete(self
, session
, _id
, dry_run
=False):
303 Delete item by its internal _id
304 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
305 :param _id: server internal id
306 :param dry_run: make checking but do not delete
307 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
309 # TODO add admin to filter, validate rights
310 if dry_run
or session
["force"]: # delete completely
311 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
312 else: # if not, sent to kafka
313 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
314 self
.db
.set_one("wim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
315 self
._send
_msg
("delete", {"_id": _id
})
316 return v
# TODO indicate an offline operation to return 202 ACCEPTED
319 class SdnTopic(BaseTopic
):
322 schema_new
= sdn_new_schema
323 schema_edit
= sdn_edit_schema
326 def __init__(self
, db
, fs
, msg
):
327 BaseTopic
.__init
__(self
, db
, fs
, msg
)
329 def check_conflict_on_new(self
, session
, indata
):
330 self
.check_unique_name(session
, indata
["name"], _id
=None)
332 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
333 if not session
["force"] and edit_content
.get("name"):
334 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
337 schema_version
= final_content
.get("schema_version")
338 if schema_version
and edit_content
.get("password"):
339 final_content
["password"] = self
.db
.encrypt(edit_content
["password"], schema_version
=schema_version
,
342 def format_on_new(self
, content
, project_id
=None, make_public
=False):
343 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
344 content
["schema_version"] = schema_version
= "1.1"
346 if content
.get("password"):
347 content
["password"] = self
.db
.encrypt(content
["password"], schema_version
=schema_version
,
350 content
["_admin"]["operationalState"] = "PROCESSING"
352 def delete(self
, session
, _id
, dry_run
=False):
354 Delete item by its internal _id
355 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
356 :param _id: server internal id
357 :param dry_run: make checking but do not delete
358 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
360 if dry_run
or session
["force"]: # delete completely
361 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
362 else: # if not sent to kafka
363 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
364 self
.db
.set_one("sdns", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
365 self
._send
_msg
("delete", {"_id": _id
})
366 return v
# TODO indicate an offline operation to return 202 ACCEPTED
369 class UserTopicAuth(UserTopic
):
371 # topic_msg = "users"
372 # schema_new = user_new_schema
373 # schema_edit = user_edit_schema
375 def __init__(self
, db
, fs
, msg
, auth
):
376 UserTopic
.__init
__(self
, db
, fs
, msg
)
379 def check_conflict_on_new(self
, session
, indata
):
381 Check that the data to be inserted is valid
383 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
384 :param indata: data to be inserted
385 :return: None or raises EngineException
387 username
= indata
.get("username")
388 user_list
= list(map(lambda x
: x
["username"], self
.auth
.get_user_list()))
390 if username
in user_list
:
391 raise EngineException("username '{}' exists".format(username
), HTTPStatus
.CONFLICT
)
393 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
395 Check that the data to be edited/uploaded is valid
397 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
398 :param final_content: data once modified
399 :param edit_content: incremental data that contains the modifications to apply
400 :param _id: internal _id
401 :return: None or raises EngineException
403 users
= self
.auth
.get_user_list()
404 admin_user
= [user
for user
in users
if user
["name"] == "admin"][0]
406 if _id
== admin_user
["_id"] and edit_content
["project_role_mappings"]:
409 "role": "system_admin"
411 if elem
not in edit_content
:
412 raise EngineException("You cannot remove system_admin role from admin user",
413 http_code
=HTTPStatus
.FORBIDDEN
)
415 def check_conflict_on_del(self
, session
, _id
, db_content
):
417 Check if deletion can be done because of dependencies if it is not force. To override
418 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
419 :param _id: internal _id
420 :param db_content: The database content of this item _id
421 :return: None if ok or raises EngineException with the conflict
423 if _id
== session
["username"]:
424 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
427 def format_on_new(content
, project_id
=None, make_public
=False):
429 Modifies content descriptor to include _id.
431 NOTE: No password salt required because the authentication backend
432 should handle these security concerns.
434 :param content: descriptor to be modified
435 :param make_public: if included it is generated as public for reading.
436 :return: None, but content is modified
438 BaseTopic
.format_on_new(content
, make_public
=False)
439 content
["_id"] = content
["username"]
440 content
["password"] = content
["password"]
443 def format_on_edit(final_content
, edit_content
):
445 Modifies final_content descriptor to include the modified date.
447 NOTE: No password salt required because the authentication backend
448 should handle these security concerns.
450 :param final_content: final descriptor generated
451 :param edit_content: alterations to be include
452 :return: None, but final_content is modified
454 BaseTopic
.format_on_edit(final_content
, edit_content
)
455 if "password" in edit_content
:
456 final_content
["password"] = edit_content
["password"]
458 final_content
["project_role_mappings"] = edit_content
["project_role_mappings"]
460 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
462 Creates a new entry into the authentication backend.
464 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
466 :param rollback: list to append created items at database in case a rollback may to be done
467 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
468 :param indata: data to be inserted
469 :param kwargs: used to override the indata descriptor
470 :param headers: http request headers
471 :return: _id: identity of the inserted data.
474 content
= BaseTopic
._remove
_envelop
(indata
)
476 # Override descriptor with query string kwargs
477 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
478 content
= self
._validate
_input
_new
(content
, session
["force"])
479 self
.check_conflict_on_new(session
, content
)
480 self
.format_on_new(content
, session
["project_id"], make_public
=session
["public"])
481 _id
= self
.auth
.create_user(content
["username"], content
["password"])
482 rollback
.append({"topic": self
.topic
, "_id": _id
})
483 del content
["password"]
484 # self._send_msg("create", content)
486 except ValidationError
as e
:
487 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
489 def show(self
, session
, _id
):
491 Get complete information on an topic
493 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
494 :param _id: server internal id
495 :return: dictionary, raise exception if not found.
497 users
= [user
for user
in self
.auth
.get_user_list() if user
["_id"] == _id
]
502 raise EngineException("Too many users found", HTTPStatus
.CONFLICT
)
504 raise EngineException("User not found", HTTPStatus
.NOT_FOUND
)
506 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
508 Updates an user entry.
510 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
512 :param indata: data to be inserted
513 :param kwargs: used to override the indata descriptor
515 :return: _id: identity of the inserted data.
517 indata
= self
._remove
_envelop
(indata
)
519 # Override descriptor with query string kwargs
521 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
523 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
526 content
= self
.show(session
, _id
)
527 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
528 self
.format_on_edit(content
, indata
)
530 if "password" in content
:
531 self
.auth
.change_password(content
["name"], content
["password"])
533 users
= self
.auth
.get_user_list()
534 user
= [user
for user
in users
if user
["_id"] == content
["_id"]][0]
535 original_mapping
= []
536 edit_mapping
= content
["project_role_mappings"]
538 for project
in user
["projects"]:
539 for role
in project
["roles"]:
540 original_mapping
+= {
541 "project": project
["name"],
545 mappings_to_remove
= [mapping
for mapping
in original_mapping
546 if mapping
not in edit_mapping
]
548 mappings_to_add
= [mapping
for mapping
in edit_mapping
549 if mapping
not in original_mapping
]
551 for mapping
in mappings_to_remove
:
552 self
.auth
.remove_role_from_user(
558 for mapping
in mappings_to_add
:
559 self
.auth
.assign_role_to_user(
565 return content
["_id"]
566 except ValidationError
as e
:
567 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
569 def list(self
, session
, filter_q
=None):
571 Get a list of the topic that matches a filter
572 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
573 :param filter_q: filter of data to be applied
574 :return: The list, it can be empty if no one match the filter.
579 return self
.auth
.get_user_list(filter_q
)
581 def delete(self
, session
, _id
, dry_run
=False):
583 Delete item by its internal _id
585 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
586 :param _id: server internal id
587 :param force: indicates if deletion must be forced in case of conflict
588 :param dry_run: make checking but do not delete
589 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
591 self
.check_conflict_on_del(session
, _id
, None)
593 v
= self
.auth
.delete_user(_id
)
598 class ProjectTopicAuth(ProjectTopic
):
600 # topic_msg = "projects"
601 # schema_new = project_new_schema
602 # schema_edit = project_edit_schema
604 def __init__(self
, db
, fs
, msg
, auth
):
605 ProjectTopic
.__init
__(self
, db
, fs
, msg
)
608 def check_conflict_on_new(self
, session
, indata
):
610 Check that the data to be inserted is valid
612 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
613 :param indata: data to be inserted
614 :return: None or raises EngineException
616 project
= indata
.get("name")
617 project_list
= list(map(lambda x
: x
["name"], self
.auth
.get_project_list()))
619 if project
in project_list
:
620 raise EngineException("project '{}' exists".format(project
), HTTPStatus
.CONFLICT
)
622 def check_conflict_on_del(self
, session
, _id
, db_content
):
624 Check if deletion can be done because of dependencies if it is not force. To override
626 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
627 :param _id: internal _id
628 :param db_content: The database content of this item _id
629 :return: None if ok or raises EngineException with the conflict
631 projects
= self
.auth
.get_project_list()
632 current_project
= [project
for project
in projects
633 if project
["name"] == session
["project_id"]][0]
635 if _id
== current_project
["_id"]:
636 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
638 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
640 Creates a new entry into the authentication backend.
642 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
644 :param rollback: list to append created items at database in case a rollback may to be done
645 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
646 :param indata: data to be inserted
647 :param kwargs: used to override the indata descriptor
648 :param headers: http request headers
649 :return: _id: identity of the inserted data.
652 content
= BaseTopic
._remove
_envelop
(indata
)
654 # Override descriptor with query string kwargs
655 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
656 content
= self
._validate
_input
_new
(content
, session
["force"])
657 self
.check_conflict_on_new(session
, content
)
658 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=session
["public"])
659 _id
= self
.auth
.create_project(content
["name"])
660 rollback
.append({"topic": self
.topic
, "_id": _id
})
661 # self._send_msg("create", content)
663 except ValidationError
as e
:
664 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
666 def show(self
, session
, _id
):
668 Get complete information on an topic
670 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
671 :param _id: server internal id
672 :return: dictionary, raise exception if not found.
674 projects
= [project
for project
in self
.auth
.get_project_list() if project
["_id"] == _id
]
676 if len(projects
) == 1:
678 elif len(projects
) > 1:
679 raise EngineException("Too many projects found", HTTPStatus
.CONFLICT
)
681 raise EngineException("Project not found", HTTPStatus
.NOT_FOUND
)
683 def list(self
, session
, filter_q
=None):
685 Get a list of the topic that matches a filter
687 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
688 :param filter_q: filter of data to be applied
689 :return: The list, it can be empty if no one match the filter.
694 return self
.auth
.get_project_list(filter_q
)
696 def delete(self
, session
, _id
, dry_run
=False):
698 Delete item by its internal _id
700 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
701 :param _id: server internal id
702 :param dry_run: make checking but do not delete
703 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
705 self
.check_conflict_on_del(session
, _id
, None)
707 v
= self
.auth
.delete_project(_id
)
712 class RoleTopicAuth(BaseTopic
):
713 topic
= "roles_operations"
715 schema_new
= roles_new_schema
716 schema_edit
= roles_edit_schema
719 def __init__(self
, db
, fs
, msg
, auth
, ops
):
720 BaseTopic
.__init
__(self
, db
, fs
, msg
)
722 self
.operations
= ops
725 def validate_role_definition(operations
, role_definitions
):
727 Validates the role definition against the operations defined in
728 the resources to operations files.
730 :param operations: operations list
731 :param role_definitions: role definition to test
732 :return: None if ok, raises ValidationError exception on error
734 for role_def
in role_definitions
.keys():
737 if role_def
[-1] == ".":
738 raise ValidationError("Operation cannot end with \".\"")
740 role_def_matches
= [op
for op
in operations
if op
.startswith(role_def
)]
742 if len(role_def_matches
) == 0:
743 raise ValidationError("No matching operation found.")
745 def _validate_input_new(self
, input, force
=False):
747 Validates input user content for a new entry.
749 :param input: user input content for the new topic
750 :param force: may be used for being more tolerant
751 :return: The same input content, or a changed version of it.
754 validate_input(input, self
.schema_new
)
755 if "definition" in input and input["definition"]:
756 self
.validate_role_definition(self
.operations
, input["definition"])
759 def _validate_input_edit(self
, input, force
=False):
761 Validates input user content for updating an entry.
763 :param input: user input content for the new topic
764 :param force: may be used for being more tolerant
765 :return: The same input content, or a changed version of it.
768 validate_input(input, self
.schema_edit
)
769 if "definition" in input and input["definition"]:
770 self
.validate_role_definition(self
.operations
, input["definition"])
773 def check_conflict_on_new(self
, session
, indata
):
775 Check that the data to be inserted is valid
777 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
778 :param indata: data to be inserted
779 :return: None or raises EngineException
781 role
= indata
.get("name")
782 role_list
= list(map(lambda x
: x
["name"], self
.auth
.get_role_list()))
784 if role
in role_list
:
785 raise EngineException("role '{}' exists".format(role
), HTTPStatus
.CONFLICT
)
787 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
789 Check that the data to be edited/uploaded is valid
791 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
792 :param final_content: data once modified
793 :param edit_content: incremental data that contains the modifications to apply
794 :param _id: internal _id
795 :return: None or raises EngineException
797 roles
= self
.auth
.get_role_list()
798 system_admin_role
= [role
for role
in roles
799 if roles
["name"] == "system_admin"][0]
801 if _id
== system_admin_role
["_id"]:
802 raise EngineException("You cannot edit system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
804 def check_conflict_on_del(self
, session
, _id
, db_content
):
806 Check if deletion can be done because of dependencies if it is not force. To override
808 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
809 :param _id: internal _id
810 :param db_content: The database content of this item _id
811 :return: None if ok or raises EngineException with the conflict
813 roles
= self
.auth
.get_role_list()
814 system_admin_role
= [role
for role
in roles
815 if roles
["name"] == "system_admin"][0]
817 if _id
== system_admin_role
["_id"]:
818 raise EngineException("You cannot delete system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
821 def format_on_new(content
, project_id
=None, make_public
=False):
823 Modifies content descriptor to include _admin
825 :param content: descriptor to be modified
826 :param project_id: if included, it add project read/write permissions
827 :param make_public: if included it is generated as public for reading.
828 :return: None, but content is modified
831 if "_admin" not in content
:
832 content
["_admin"] = {}
833 if not content
["_admin"].get("created"):
834 content
["_admin"]["created"] = now
835 content
["_admin"]["modified"] = now
836 content
["root"] = False
838 # Saving the role definition
839 if "definition" in content
and content
["definition"]:
840 for role_def
, value
in content
["definition"].items():
842 content
["root"] = value
844 content
[role_def
.replace(".", ":")] = value
846 # Cleaning undesired values
847 if "definition" in content
:
848 del content
["definition"]
851 def format_on_edit(final_content
, edit_content
):
853 Modifies final_content descriptor to include the modified date.
855 :param final_content: final descriptor generated
856 :param edit_content: alterations to be include
857 :return: None, but final_content is modified
859 final_content
["_admin"]["modified"] = time()
861 ignore_fields
= ["_id", "name", "_admin"]
862 delete_keys
= [key
for key
in final_content
.keys() if key
not in ignore_fields
]
864 for key
in delete_keys
:
865 del final_content
[key
]
867 # Saving the role definition
868 if "definition" in edit_content
and edit_content
["definition"]:
869 for role_def
, value
in edit_content
["definition"].items():
871 final_content
["root"] = value
873 final_content
[role_def
.replace(".", ":")] = value
875 if "root" not in final_content
:
876 final_content
["root"] = False
879 def format_on_show(content
):
881 Modifies the content of the role information to separate the role
882 metadata from the role definition. Eases the reading process of the
885 :param definition: role definition to be processed
887 ignore_fields
= ["_admin", "_id", "name", "root"]
888 content_keys
= list(content
.keys())
889 definition
= dict(content
)
891 for key
in content_keys
:
892 if key
in ignore_fields
:
897 definition
[key
.replace(":", ".")] = definition
[key
]
901 content
["definition"] = definition
903 def show(self
, session
, _id
):
905 Get complete information on an topic
907 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
908 :param _id: server internal id
909 :return: dictionary, raise exception if not found.
911 filter_db
= self
._get
_project
_filter
(session
, write
=False, show_all
=True)
912 filter_db
["_id"] = _id
914 role
= self
.db
.get_one(self
.topic
, filter_db
)
915 new_role
= dict(role
)
916 self
.format_on_show(new_role
)
920 def list(self
, session
, filter_q
=None):
922 Get a list of the topic that matches a filter
924 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
925 :param filter_q: filter of data to be applied
926 :return: The list, it can be empty if no one match the filter.
931 roles
= self
.db
.get_list(self
.topic
, filter_q
)
935 new_role
= dict(role
)
936 self
.format_on_show(new_role
)
937 new_roles
.append(new_role
)
941 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
943 Creates a new entry into database.
945 :param rollback: list to append created items at database in case a rollback may to be done
946 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
947 :param indata: data to be inserted
948 :param kwargs: used to override the indata descriptor
949 :param headers: http request headers
950 :return: _id: identity of the inserted data.
953 content
= BaseTopic
._remove
_envelop
(indata
)
955 # Override descriptor with query string kwargs
956 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
957 content
= self
._validate
_input
_new
(content
, session
["force"])
958 self
.check_conflict_on_new(session
, content
)
959 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=session
["public"])
960 role_name
= content
["name"]
961 role
= self
.auth
.create_role(role_name
)
962 content
["_id"] = role
["_id"]
963 _id
= self
.db
.create(self
.topic
, content
)
964 rollback
.append({"topic": self
.topic
, "_id": _id
})
965 # self._send_msg("create", content)
967 except ValidationError
as e
:
968 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
970 def delete(self
, session
, _id
, dry_run
=False):
972 Delete item by its internal _id
974 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
975 :param _id: server internal id
976 :param dry_run: make checking but do not delete
977 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
979 self
.check_conflict_on_del(session
, _id
, None)
980 filter_q
= self
._get
_project
_filter
(session
)
981 filter_q
["_id"] = _id
983 self
.auth
.delete_role(_id
)
984 v
= self
.db
.del_one(self
.topic
, filter_q
)
988 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
990 Updates a role entry.
992 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
994 :param indata: data to be inserted
995 :param kwargs: used to override the indata descriptor
997 :return: _id: identity of the inserted data.
999 indata
= self
._remove
_envelop
(indata
)
1001 # Override descriptor with query string kwargs
1003 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
1005 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
1008 content
= self
.show(session
, _id
)
1009 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
1010 self
.format_on_edit(content
, indata
)
1011 self
.db
.replace(self
.topic
, _id
, content
)
1013 except ValidationError
as e
:
1014 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)