07f18da6c6728593e2d81aefb3b48427af33b401
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 ignore_fields
= ["_id", "name"]
735 for role_def
in role_definitions
.keys():
736 if role_def
in ignore_fields
:
739 if isinstance(role_definitions
[role_def
], bool):
742 raise ValidationError("Operation authorization \".\" should be True/False.")
743 if role_def
[-1] == ".":
744 raise ValidationError("Operation cannot end with \".\"")
746 role_def_matches
= [op
for op
in operations
if op
.startswith(role_def
)]
748 if len(role_def_matches
) == 0:
749 raise ValidationError("No matching operation found.")
751 if not isinstance(role_definitions
[role_def
], bool):
752 raise ValidationError("Operation authorization {} should be True/False.".format(role_def
))
754 def _validate_input_new(self
, input, force
=False):
756 Validates input user content for a new entry.
758 :param input: user input content for the new topic
759 :param force: may be used for being more tolerant
760 :return: The same input content, or a changed version of it.
763 validate_input(input, self
.schema_new
)
764 self
.validate_role_definition(self
.operations
, input)
768 def _validate_input_edit(self
, input, force
=False):
770 Validates input user content for updating an entry.
772 :param input: user input content for the new topic
773 :param force: may be used for being more tolerant
774 :return: The same input content, or a changed version of it.
777 validate_input(input, self
.schema_edit
)
778 self
.validate_role_definition(self
.operations
, input)
782 def check_conflict_on_new(self
, session
, indata
):
784 Check that the data to be inserted is valid
786 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
787 :param indata: data to be inserted
788 :return: None or raises EngineException
790 role
= indata
.get("name")
791 role_list
= list(map(lambda x
: x
["name"], self
.auth
.get_role_list()))
793 if role
in role_list
:
794 raise EngineException("role '{}' exists".format(role
), HTTPStatus
.CONFLICT
)
796 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
798 Check that the data to be edited/uploaded is valid
800 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
801 :param final_content: data once modified
802 :param edit_content: incremental data that contains the modifications to apply
803 :param _id: internal _id
804 :return: None or raises EngineException
806 roles
= self
.auth
.get_role_list()
807 system_admin_role
= [role
for role
in roles
808 if roles
["name"] == "system_admin"][0]
810 if _id
== system_admin_role
["_id"]:
811 raise EngineException("You cannot edit system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
813 def check_conflict_on_del(self
, session
, _id
, db_content
):
815 Check if deletion can be done because of dependencies if it is not force. To override
817 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
818 :param _id: internal _id
819 :param db_content: The database content of this item _id
820 :return: None if ok or raises EngineException with the conflict
822 roles
= self
.auth
.get_role_list()
823 system_admin_role
= [role
for role
in roles
824 if roles
["name"] == "system_admin"][0]
826 if _id
== system_admin_role
["_id"]:
827 raise EngineException("You cannot delete system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
830 def format_on_new(content
, project_id
=None, make_public
=False):
832 Modifies content descriptor to include _admin
834 :param content: descriptor to be modified
835 :param project_id: if included, it add project read/write permissions
836 :param make_public: if included it is generated as public for reading.
837 :return: None, but content is modified
840 if "_admin" not in content
:
841 content
["_admin"] = {}
842 if not content
["_admin"].get("created"):
843 content
["_admin"]["created"] = now
844 content
["_admin"]["modified"] = now
847 ignore_fields
= ["_id", "_admin", "name"]
848 for role_def
, value
in content
.items():
849 if role_def
in ignore_fields
:
851 content
[role_def
.replace(".", ":")] = value
852 del content
[role_def
]
855 def format_on_edit(final_content
, edit_content
):
857 Modifies final_content descriptor to include the modified date.
859 :param final_content: final descriptor generated
860 :param edit_content: alterations to be include
861 :return: None, but final_content is modified
863 final_content
["_admin"]["modified"] = time()
865 ignore_fields
= ["_id", "name", "_admin"]
866 delete_keys
= [key
for key
in final_content
.keys() if key
not in ignore_fields
]
868 for key
in delete_keys
:
869 del final_content
[key
]
871 # Saving the role definition
872 for role_def
, value
in edit_content
.items():
873 final_content
[role_def
.replace(".", ":")] = value
875 if ":" not in final_content
.keys():
876 final_content
[":"] = 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 content_keys
= list(content
.keys())
889 for key
in content_keys
:
891 content
[key
.replace(":", ".")] = content
[key
]
894 def show(self
, session
, _id
):
896 Get complete information on an topic
898 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
899 :param _id: server internal id
900 :return: dictionary, raise exception if not found.
902 filter_db
= self
._get
_project
_filter
(session
, write
=False, show_all
=True)
903 filter_db
["_id"] = _id
905 role
= self
.db
.get_one(self
.topic
, filter_db
)
906 new_role
= dict(role
)
907 self
.format_on_show(new_role
)
911 def list(self
, session
, filter_q
=None):
913 Get a list of the topic that matches a filter
915 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
916 :param filter_q: filter of data to be applied
917 :return: The list, it can be empty if no one match the filter.
922 roles
= self
.db
.get_list(self
.topic
, filter_q
)
926 new_role
= dict(role
)
927 self
.format_on_show(new_role
)
928 new_roles
.append(new_role
)
932 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
934 Creates a new entry into database.
936 :param rollback: list to append created items at database in case a rollback may to be done
937 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
938 :param indata: data to be inserted
939 :param kwargs: used to override the indata descriptor
940 :param headers: http request headers
941 :return: _id: identity of the inserted data.
944 content
= BaseTopic
._remove
_envelop
(indata
)
946 # Override descriptor with query string kwargs
947 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
948 content
= self
._validate
_input
_new
(content
, session
["force"])
949 self
.check_conflict_on_new(session
, content
)
950 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=session
["public"])
951 role_name
= content
["name"]
952 role
= self
.auth
.create_role(role_name
)
953 content
["_id"] = role
["_id"]
954 _id
= self
.db
.create(self
.topic
, content
)
955 rollback
.append({"topic": self
.topic
, "_id": _id
})
956 # self._send_msg("create", content)
958 except ValidationError
as e
:
959 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
961 def delete(self
, session
, _id
, dry_run
=False):
963 Delete item by its internal _id
965 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
966 :param _id: server internal id
967 :param dry_run: make checking but do not delete
968 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
970 self
.check_conflict_on_del(session
, _id
, None)
971 filter_q
= self
._get
_project
_filter
(session
)
972 filter_q
["_id"] = _id
974 self
.auth
.delete_role(_id
)
975 v
= self
.db
.del_one(self
.topic
, filter_q
)
979 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
981 Updates a role entry.
983 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
985 :param indata: data to be inserted
986 :param kwargs: used to override the indata descriptor
988 :return: _id: identity of the inserted data.
990 indata
= self
._remove
_envelop
(indata
)
992 # Override descriptor with query string kwargs
994 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
996 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
999 content
= self
.show(session
, _id
)
1000 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
1001 self
.format_on_edit(content
, indata
)
1002 self
.db
.replace(self
.topic
, _id
, content
)
1004 except ValidationError
as e
:
1005 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)