071ed3b3d91e6b272fe7655ef1cfcae7da64997e
[osm/NBI.git] / osm_nbi / admin_topics.py
1 # -*- coding: utf-8 -*-
2
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
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
12 # implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 # import logging
17 from uuid import uuid4
18 from hashlib import sha256
19 from http import HTTPStatus
20 from time import time
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
28
29 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
30
31
32 class UserTopic(BaseTopic):
33 topic = "users"
34 topic_msg = "users"
35 schema_new = user_new_schema
36 schema_edit = user_edit_schema
37 multiproject = False
38
39 def __init__(self, db, fs, msg):
40 BaseTopic.__init__(self, db, fs, msg)
41
42 @staticmethod
43 def _get_project_filter(session):
44 """
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"
48 :return:
49 """
50 if session["admin"]: # allows all
51 return {}
52 else:
53 return {"username": session["username"]}
54
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)
59 # check projects
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,
64 fail_on_more=False):
65 raise EngineException("project '{}' does not exist".format(p), HTTPStatus.CONFLICT)
66
67 def check_conflict_on_del(self, session, _id, db_content):
68 """
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
74 """
75 if _id == session["username"]:
76 raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
77
78 @staticmethod
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"]
83 salt = uuid4().hex
84 content["_admin"]["salt"] = salt
85 if content.get("password"):
86 content["password"] = sha256(content["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
87 if content.get("project_role_mappings"):
88 projects = [mapping[0] for mapping in content["project_role_mappings"]]
89
90 if content.get("projects"):
91 content["projects"] += projects
92 else:
93 content["projects"] = projects
94
95 @staticmethod
96 def format_on_edit(final_content, edit_content):
97 BaseTopic.format_on_edit(final_content, edit_content)
98 if edit_content.get("password"):
99 salt = uuid4().hex
100 final_content["_admin"]["salt"] = salt
101 final_content["password"] = sha256(edit_content["password"].encode('utf-8') +
102 salt.encode('utf-8')).hexdigest()
103
104 def edit(self, session, _id, indata=None, kwargs=None, content=None):
105 if not session["admin"]:
106 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
107 # Names that look like UUIDs are not allowed
108 name = (indata if indata else kwargs).get("username")
109 if is_valid_uuid(name):
110 raise EngineException("Usernames that look like UUIDs are not allowed",
111 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
112 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
113
114 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
115 if not session["admin"]:
116 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
117 # Names that look like UUIDs are not allowed
118 name = indata["username"] if indata else kwargs["username"]
119 if is_valid_uuid(name):
120 raise EngineException("Usernames that look like UUIDs are not allowed",
121 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
122 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
123
124
125 class ProjectTopic(BaseTopic):
126 topic = "projects"
127 topic_msg = "projects"
128 schema_new = project_new_schema
129 schema_edit = project_edit_schema
130 multiproject = False
131
132 def __init__(self, db, fs, msg):
133 BaseTopic.__init__(self, db, fs, msg)
134
135 @staticmethod
136 def _get_project_filter(session):
137 """
138 Generates a filter dictionary for querying database users.
139 Current policy is admin can show all, non admin, only its own user.
140 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
141 :return:
142 """
143 if session["admin"]: # allows all
144 return {}
145 else:
146 return {"_id.cont": session["project_id"]}
147
148 def check_conflict_on_new(self, session, indata):
149 if not indata.get("name"):
150 raise EngineException("missing 'name'")
151 # check name not exists
152 if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
153 raise EngineException("name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT)
154
155 @staticmethod
156 def format_on_new(content, project_id=None, make_public=False):
157 BaseTopic.format_on_new(content, None)
158 # Removed so that the UUID is kept, to allow Project Name modification
159 # content["_id"] = content["name"]
160
161 def check_conflict_on_del(self, session, _id, db_content):
162 """
163 Check if deletion can be done because of dependencies if it is not force. To override
164 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
165 :param _id: internal _id
166 :param db_content: The database content of this item _id
167 :return: None if ok or raises EngineException with the conflict
168 """
169 if _id in session["project_id"]:
170 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
171 if session["force"]:
172 return
173 _filter = {"projects": _id}
174 if self.db.get_list("users", _filter):
175 raise EngineException("There is some USER that contains this project", http_code=HTTPStatus.CONFLICT)
176
177 def edit(self, session, _id, indata=None, kwargs=None, content=None):
178 if not session["admin"]:
179 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
180 # Names that look like UUIDs are not allowed
181 name = (indata if indata else kwargs).get("name")
182 if is_valid_uuid(name):
183 raise EngineException("Project names that look like UUIDs are not allowed",
184 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
185 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
186
187 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
188 if not session["admin"]:
189 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
190 # Names that look like UUIDs are not allowed
191 name = indata["name"] if indata else kwargs["name"]
192 if is_valid_uuid(name):
193 raise EngineException("Project names that look like UUIDs are not allowed",
194 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
195 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
196
197
198 class VimAccountTopic(BaseTopic):
199 topic = "vim_accounts"
200 topic_msg = "vim_account"
201 schema_new = vim_account_new_schema
202 schema_edit = vim_account_edit_schema
203 vim_config_encrypted = ("admin_password", "nsx_password", "vcenter_password")
204 multiproject = True
205
206 def __init__(self, db, fs, msg):
207 BaseTopic.__init__(self, db, fs, msg)
208
209 def check_conflict_on_new(self, session, indata):
210 self.check_unique_name(session, indata["name"], _id=None)
211
212 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
213 if not session["force"] and edit_content.get("name"):
214 self.check_unique_name(session, edit_content["name"], _id=_id)
215
216 # encrypt passwords
217 schema_version = final_content.get("schema_version")
218 if schema_version:
219 if edit_content.get("vim_password"):
220 final_content["vim_password"] = self.db.encrypt(edit_content["vim_password"],
221 schema_version=schema_version, salt=_id)
222 if edit_content.get("config"):
223 for p in self.vim_config_encrypted:
224 if edit_content["config"].get(p):
225 final_content["config"][p] = self.db.encrypt(edit_content["config"][p],
226 schema_version=schema_version, salt=_id)
227
228 def format_on_new(self, content, project_id=None, make_public=False):
229 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
230 content["schema_version"] = schema_version = "1.1"
231
232 # encrypt passwords
233 if content.get("vim_password"):
234 content["vim_password"] = self.db.encrypt(content["vim_password"], schema_version=schema_version,
235 salt=content["_id"])
236 if content.get("config"):
237 for p in self.vim_config_encrypted:
238 if content["config"].get(p):
239 content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version,
240 salt=content["_id"])
241
242 content["_admin"]["operationalState"] = "PROCESSING"
243
244 def delete(self, session, _id, dry_run=False):
245 """
246 Delete item by its internal _id
247 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
248 :param _id: server internal id
249 :param dry_run: make checking but do not delete
250 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
251 """
252 # TODO add admin to filter, validate rights
253 if dry_run or session["force"]: # delete completely
254 return BaseTopic.delete(self, session, _id, dry_run)
255 else: # if not, sent to kafka
256 v = BaseTopic.delete(self, session, _id, dry_run=True)
257 self.db.set_one("vim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
258 self._send_msg("delete", {"_id": _id})
259 return v # TODO indicate an offline operation to return 202 ACCEPTED
260
261
262 class WimAccountTopic(BaseTopic):
263 topic = "wim_accounts"
264 topic_msg = "wim_account"
265 schema_new = wim_account_new_schema
266 schema_edit = wim_account_edit_schema
267 multiproject = True
268 wim_config_encrypted = ()
269
270 def __init__(self, db, fs, msg):
271 BaseTopic.__init__(self, db, fs, msg)
272
273 def check_conflict_on_new(self, session, indata):
274 self.check_unique_name(session, indata["name"], _id=None)
275
276 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
277 if not session["force"] and edit_content.get("name"):
278 self.check_unique_name(session, edit_content["name"], _id=_id)
279
280 # encrypt passwords
281 schema_version = final_content.get("schema_version")
282 if schema_version:
283 if edit_content.get("wim_password"):
284 final_content["wim_password"] = self.db.encrypt(edit_content["wim_password"],
285 schema_version=schema_version, salt=_id)
286 if edit_content.get("config"):
287 for p in self.wim_config_encrypted:
288 if edit_content["config"].get(p):
289 final_content["config"][p] = self.db.encrypt(edit_content["config"][p],
290 schema_version=schema_version, salt=_id)
291
292 def format_on_new(self, content, project_id=None, make_public=False):
293 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
294 content["schema_version"] = schema_version = "1.1"
295
296 # encrypt passwords
297 if content.get("wim_password"):
298 content["wim_password"] = self.db.encrypt(content["wim_password"], schema_version=schema_version,
299 salt=content["_id"])
300 if content.get("config"):
301 for p in self.wim_config_encrypted:
302 if content["config"].get(p):
303 content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version,
304 salt=content["_id"])
305
306 content["_admin"]["operationalState"] = "PROCESSING"
307
308 def delete(self, session, _id, dry_run=False):
309 """
310 Delete item by its internal _id
311 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
312 :param _id: server internal id
313 :param dry_run: make checking but do not delete
314 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
315 """
316 # TODO add admin to filter, validate rights
317 if dry_run or session["force"]: # delete completely
318 return BaseTopic.delete(self, session, _id, dry_run)
319 else: # if not, sent to kafka
320 v = BaseTopic.delete(self, session, _id, dry_run=True)
321 self.db.set_one("wim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
322 self._send_msg("delete", {"_id": _id})
323 return v # TODO indicate an offline operation to return 202 ACCEPTED
324
325
326 class SdnTopic(BaseTopic):
327 topic = "sdns"
328 topic_msg = "sdn"
329 schema_new = sdn_new_schema
330 schema_edit = sdn_edit_schema
331 multiproject = True
332
333 def __init__(self, db, fs, msg):
334 BaseTopic.__init__(self, db, fs, msg)
335
336 def check_conflict_on_new(self, session, indata):
337 self.check_unique_name(session, indata["name"], _id=None)
338
339 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
340 if not session["force"] and edit_content.get("name"):
341 self.check_unique_name(session, edit_content["name"], _id=_id)
342
343 # encrypt passwords
344 schema_version = final_content.get("schema_version")
345 if schema_version and edit_content.get("password"):
346 final_content["password"] = self.db.encrypt(edit_content["password"], schema_version=schema_version,
347 salt=_id)
348
349 def format_on_new(self, content, project_id=None, make_public=False):
350 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
351 content["schema_version"] = schema_version = "1.1"
352 # encrypt passwords
353 if content.get("password"):
354 content["password"] = self.db.encrypt(content["password"], schema_version=schema_version,
355 salt=content["_id"])
356
357 content["_admin"]["operationalState"] = "PROCESSING"
358
359 def delete(self, session, _id, dry_run=False):
360 """
361 Delete item by its internal _id
362 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
363 :param _id: server internal id
364 :param dry_run: make checking but do not delete
365 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
366 """
367 if dry_run or session["force"]: # delete completely
368 return BaseTopic.delete(self, session, _id, dry_run)
369 else: # if not sent to kafka
370 v = BaseTopic.delete(self, session, _id, dry_run=True)
371 self.db.set_one("sdns", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
372 self._send_msg("delete", {"_id": _id})
373 return v # TODO indicate an offline operation to return 202 ACCEPTED
374
375
376 class UserTopicAuth(UserTopic):
377 # topic = "users"
378 # topic_msg = "users"
379 schema_new = user_new_schema
380 schema_edit = user_edit_schema
381
382 def __init__(self, db, fs, msg, auth):
383 UserTopic.__init__(self, db, fs, msg)
384 self.auth = auth
385
386 def check_conflict_on_new(self, session, indata):
387 """
388 Check that the data to be inserted is valid
389
390 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
391 :param indata: data to be inserted
392 :return: None or raises EngineException
393 """
394 username = indata.get("username")
395 if is_valid_uuid(username):
396 raise EngineException("username '{}' cannot be a uuid format".format(username),
397 HTTPStatus.UNPROCESSABLE_ENTITY)
398
399 # Check that username is not used, regardless keystone already checks this
400 if self.auth.get_user_list(filter_q={"name": username}):
401 raise EngineException("username '{}' is already used".format(username), HTTPStatus.CONFLICT)
402
403 if "projects" in indata.keys():
404 raise EngineException("Format invalid: the keyword \"projects\" is not allowed for keystone authentication",
405 HTTPStatus.BAD_REQUEST)
406
407 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
408 """
409 Check that the data to be edited/uploaded is valid
410
411 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
412 :param final_content: data once modified
413 :param edit_content: incremental data that contains the modifications to apply
414 :param _id: internal _id
415 :return: None or raises EngineException
416 """
417
418 if "username" in edit_content:
419 username = edit_content.get("username")
420 if is_valid_uuid(username):
421 raise EngineException("username '{}' cannot be an uuid format".format(username),
422 HTTPStatus.UNPROCESSABLE_ENTITY)
423
424 # Check that username is not used, regardless keystone already checks this
425 if self.auth.get_user_list(filter_q={"name": username}):
426 raise EngineException("username '{}' is already used".format(username), HTTPStatus.CONFLICT)
427
428 if final_content["username"] == "admin":
429 for mapping in edit_content.get("remove_project_role_mappings", ()):
430 if mapping["project"] == "admin" and mapping.get("role") in (None, "system_admin"):
431 # TODO make this also available for project id and role id
432 raise EngineException("You cannot remove system_admin role from admin user",
433 http_code=HTTPStatus.FORBIDDEN)
434
435 def check_conflict_on_del(self, session, _id, db_content):
436 """
437 Check if deletion can be done because of dependencies if it is not force. To override
438 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
439 :param _id: internal _id
440 :param db_content: The database content of this item _id
441 :return: None if ok or raises EngineException with the conflict
442 """
443 if db_content["username"] == session["username"]:
444 raise EngineException("You cannot delete your own login user ", http_code=HTTPStatus.CONFLICT)
445
446 # @staticmethod
447 # def format_on_new(content, project_id=None, make_public=False):
448 # """
449 # Modifies content descriptor to include _id.
450 #
451 # NOTE: No password salt required because the authentication backend
452 # should handle these security concerns.
453 #
454 # :param content: descriptor to be modified
455 # :param make_public: if included it is generated as public for reading.
456 # :return: None, but content is modified
457 # """
458 # BaseTopic.format_on_new(content, make_public=False)
459 # content["_id"] = content["username"]
460 # content["password"] = content["password"]
461
462 # @staticmethod
463 # def format_on_edit(final_content, edit_content):
464 # """
465 # Modifies final_content descriptor to include the modified date.
466 #
467 # NOTE: No password salt required because the authentication backend
468 # should handle these security concerns.
469 #
470 # :param final_content: final descriptor generated
471 # :param edit_content: alterations to be include
472 # :return: None, but final_content is modified
473 # """
474 # BaseTopic.format_on_edit(final_content, edit_content)
475 # if "password" in edit_content:
476 # final_content["password"] = edit_content["password"]
477 # else:
478 # final_content["project_role_mappings"] = edit_content["project_role_mappings"]
479
480 @staticmethod
481 def format_on_show(content):
482 """
483 Modifies the content of the role information to separate the role
484 metadata from the role definition.
485 """
486 project_role_mappings = []
487
488 for project in content["projects"]:
489 for role in project["roles"]:
490 project_role_mappings.append({"project": project["_id"],
491 "project_name": project["name"],
492 "role": role["_id"],
493 "role_name": role["name"]})
494
495 del content["projects"]
496 content["project_role_mappings"] = project_role_mappings
497
498 return content
499
500 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
501 """
502 Creates a new entry into the authentication backend.
503
504 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
505
506 :param rollback: list to append created items at database in case a rollback may to be done
507 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
508 :param indata: data to be inserted
509 :param kwargs: used to override the indata descriptor
510 :param headers: http request headers
511 :return: _id: identity of the inserted data.
512 """
513 try:
514 content = BaseTopic._remove_envelop(indata)
515
516 # Override descriptor with query string kwargs
517 BaseTopic._update_input_with_kwargs(content, kwargs)
518 content = self._validate_input_new(content, session["force"])
519 self.check_conflict_on_new(session, content)
520 # self.format_on_new(content, session["project_id"], make_public=session["public"])
521 _id = self.auth.create_user(content["username"], content["password"])["_id"]
522
523 if "project_role_mappings" in content.keys():
524 for mapping in content["project_role_mappings"]:
525 self.auth.assign_role_to_user(_id, mapping["project"], mapping["role"])
526
527 rollback.append({"topic": self.topic, "_id": _id})
528 # del content["password"]
529 # self._send_msg("create", content)
530 return _id
531 except ValidationError as e:
532 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
533
534 def show(self, session, _id):
535 """
536 Get complete information on an topic
537
538 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
539 :param _id: server internal id
540 :return: dictionary, raise exception if not found.
541 """
542 # Allow _id to be a name or uuid
543 filter_q = {self.id_field(self.topic, _id): _id}
544 users = self.auth.get_user_list(filter_q)
545
546 if len(users) == 1:
547 return self.format_on_show(users[0])
548 elif len(users) > 1:
549 raise EngineException("Too many users found", HTTPStatus.CONFLICT)
550 else:
551 raise EngineException("User not found", HTTPStatus.NOT_FOUND)
552
553 def edit(self, session, _id, indata=None, kwargs=None, content=None):
554 """
555 Updates an user entry.
556
557 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
558 :param _id:
559 :param indata: data to be inserted
560 :param kwargs: used to override the indata descriptor
561 :param content:
562 :return: _id: identity of the inserted data.
563 """
564 indata = self._remove_envelop(indata)
565
566 # Override descriptor with query string kwargs
567 if kwargs:
568 BaseTopic._update_input_with_kwargs(indata, kwargs)
569 try:
570 indata = self._validate_input_edit(indata, force=session["force"])
571
572 if not content:
573 content = self.show(session, _id)
574 self.check_conflict_on_edit(session, content, indata, _id=_id)
575 # self.format_on_edit(content, indata)
576
577 if "password" in indata or "username" in indata:
578 self.auth.update_user(_id, new_name=indata.get("username"), new_password=indata.get("password"))
579 if not indata.get("remove_project_role_mappings") and not indata.get("add_project_role_mappings") and \
580 not indata.get("project_role_mappings"):
581 return _id
582 if indata.get("project_role_mappings") and \
583 (indata.get("remove_project_role_mappings") or indata.get("add_project_role_mappings")):
584 raise EngineException("Option 'project_role_mappings' is incompatible with 'add_project_role_mappings"
585 "' or 'remove_project_role_mappings'", http_code=HTTPStatus.BAD_REQUEST)
586
587 user = self.show(session, _id)
588 original_mapping = user["project_role_mappings"]
589
590 mappings_to_add = []
591 mappings_to_remove = []
592
593 # remove
594 for to_remove in indata.get("remove_project_role_mappings", ()):
595 for mapping in original_mapping:
596 if to_remove["project"] in (mapping["project"], mapping["project_name"]):
597 if not to_remove.get("role") or to_remove["role"] in (mapping["role"], mapping["role_name"]):
598 mappings_to_remove.append(mapping)
599
600 # add
601 for to_add in indata.get("add_project_role_mappings", ()):
602 for mapping in original_mapping:
603 if to_add["project"] in (mapping["project"], mapping["project_name"]) and \
604 to_add["role"] in (mapping["role"], mapping["role_name"]):
605
606 if mapping in mappings_to_remove: # do not remove
607 mappings_to_remove.remove(mapping)
608 break # do not add, it is already at user
609 else:
610 mappings_to_add.append(to_add)
611
612 # set
613 if indata.get("project_role_mappings"):
614 for to_set in indata["project_role_mappings"]:
615 for mapping in original_mapping:
616 if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
617 to_set["role"] in (mapping["role"], mapping["role_name"]):
618
619 if mapping in mappings_to_remove: # do not remove
620 mappings_to_remove.remove(mapping)
621 break # do not add, it is already at user
622 else:
623 mappings_to_add.append(to_set)
624 for mapping in original_mapping:
625 for to_set in indata["project_role_mappings"]:
626 if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
627 to_set["role"] in (mapping["role"], mapping["role_name"]):
628 break
629 else:
630 # delete
631 if mapping not in mappings_to_remove: # do not remove
632 mappings_to_remove.append(mapping)
633
634 for mapping in mappings_to_remove:
635 self.auth.remove_role_from_user(
636 _id,
637 mapping["project"],
638 mapping["role"]
639 )
640
641 for mapping in mappings_to_add:
642 self.auth.assign_role_to_user(
643 _id,
644 mapping["project"],
645 mapping["role"]
646 )
647
648 return "_id"
649 except ValidationError as e:
650 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
651
652 def list(self, session, filter_q=None):
653 """
654 Get a list of the topic that matches a filter
655 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
656 :param filter_q: filter of data to be applied
657 :return: The list, it can be empty if no one match the filter.
658 """
659 users = [self.format_on_show(user) for user in self.auth.get_user_list(filter_q)]
660
661 return users
662
663 def delete(self, session, _id, dry_run=False):
664 """
665 Delete item by its internal _id
666
667 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
668 :param _id: server internal id
669 :param force: indicates if deletion must be forced in case of conflict
670 :param dry_run: make checking but do not delete
671 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
672 """
673 # Allow _id to be a name or uuid
674 filter_q = {self.id_field(self.topic, _id): _id}
675 user_list = self.auth.get_user_list(filter_q)
676 if not user_list:
677 raise EngineException("User '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
678 _id = user_list[0]["_id"]
679 self.check_conflict_on_del(session, _id, user_list[0])
680 if not dry_run:
681 v = self.auth.delete_user(_id)
682 return v
683 return None
684
685
686 class ProjectTopicAuth(ProjectTopic):
687 # topic = "projects"
688 # topic_msg = "projects"
689 schema_new = project_new_schema
690 schema_edit = project_edit_schema
691
692 def __init__(self, db, fs, msg, auth):
693 ProjectTopic.__init__(self, db, fs, msg)
694 self.auth = auth
695
696 def check_conflict_on_new(self, session, indata):
697 """
698 Check that the data to be inserted is valid
699
700 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
701 :param indata: data to be inserted
702 :return: None or raises EngineException
703 """
704 project_name = indata.get("name")
705 if is_valid_uuid(project_name):
706 raise EngineException("project name '{}' cannot be an uuid format".format(project_name),
707 HTTPStatus.UNPROCESSABLE_ENTITY)
708
709 project_list = self.auth.get_project_list(filter_q={"name": project_name})
710
711 if project_list:
712 raise EngineException("project '{}' exists".format(project_name), HTTPStatus.CONFLICT)
713
714 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
715 """
716 Check that the data to be edited/uploaded is valid
717
718 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
719 :param final_content: data once modified
720 :param edit_content: incremental data that contains the modifications to apply
721 :param _id: internal _id
722 :return: None or raises EngineException
723 """
724
725 project_name = edit_content.get("name")
726 if project_name:
727 if is_valid_uuid(project_name):
728 raise EngineException("project name '{}' cannot be an uuid format".format(project_name),
729 HTTPStatus.UNPROCESSABLE_ENTITY)
730
731 # Check that project name is not used, regardless keystone already checks this
732 if self.auth.get_project_list(filter_q={"name": project_name}):
733 raise EngineException("project '{}' is already used".format(project_name), HTTPStatus.CONFLICT)
734
735 def check_conflict_on_del(self, session, _id, db_content):
736 """
737 Check if deletion can be done because of dependencies if it is not force. To override
738
739 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
740 :param _id: internal _id
741 :param db_content: The database content of this item _id
742 :return: None if ok or raises EngineException with the conflict
743 """
744 # projects = self.auth.get_project_list()
745 # current_project = [project for project in projects
746 # if project["name"] in session["project_id"]][0]
747 # TODO check that any user is using this project, raise CONFLICT exception
748 if _id == session["project_id"]:
749 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
750
751 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
752 """
753 Creates a new entry into the authentication backend.
754
755 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
756
757 :param rollback: list to append created items at database in case a rollback may to be done
758 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
759 :param indata: data to be inserted
760 :param kwargs: used to override the indata descriptor
761 :param headers: http request headers
762 :return: _id: identity of the inserted data.
763 """
764 try:
765 content = BaseTopic._remove_envelop(indata)
766
767 # Override descriptor with query string kwargs
768 BaseTopic._update_input_with_kwargs(content, kwargs)
769 content = self._validate_input_new(content, session["force"])
770 self.check_conflict_on_new(session, content)
771 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
772 _id = self.auth.create_project(content["name"])
773 rollback.append({"topic": self.topic, "_id": _id})
774 # self._send_msg("create", content)
775 return _id
776 except ValidationError as e:
777 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
778
779 def show(self, session, _id):
780 """
781 Get complete information on an topic
782
783 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
784 :param _id: server internal id
785 :return: dictionary, raise exception if not found.
786 """
787 # Allow _id to be a name or uuid
788 filter_q = {self.id_field(self.topic, _id): _id}
789 projects = self.auth.get_project_list(filter_q=filter_q)
790
791 if len(projects) == 1:
792 return projects[0]
793 elif len(projects) > 1:
794 raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
795 else:
796 raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
797
798 def list(self, session, filter_q=None):
799 """
800 Get a list of the topic that matches a filter
801
802 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
803 :param filter_q: filter of data to be applied
804 :return: The list, it can be empty if no one match the filter.
805 """
806 return self.auth.get_project_list(filter_q)
807
808 def delete(self, session, _id, dry_run=False):
809 """
810 Delete item by its internal _id
811
812 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
813 :param _id: server internal id
814 :param dry_run: make checking but do not delete
815 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
816 """
817 # Allow _id to be a name or uuid
818 filter_q = {self.id_field(self.topic, _id): _id}
819 project_list = self.auth.get_project_list(filter_q)
820 if not project_list:
821 raise EngineException("Project '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
822 _id = project_list[0]["_id"]
823 self.check_conflict_on_del(session, _id, project_list[0])
824 if not dry_run:
825 v = self.auth.delete_project(_id)
826 return v
827 return None
828
829 def edit(self, session, _id, indata=None, kwargs=None, content=None):
830 """
831 Updates a project entry.
832
833 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
834 :param _id:
835 :param indata: data to be inserted
836 :param kwargs: used to override the indata descriptor
837 :param content:
838 :return: _id: identity of the inserted data.
839 """
840 indata = self._remove_envelop(indata)
841
842 # Override descriptor with query string kwargs
843 if kwargs:
844 BaseTopic._update_input_with_kwargs(indata, kwargs)
845 try:
846 indata = self._validate_input_edit(indata, force=session["force"])
847
848 if not content:
849 content = self.show(session, _id)
850 self.check_conflict_on_edit(session, content, indata, _id=_id)
851 # self.format_on_edit(content, indata)
852
853 if "name" in indata:
854 self.auth.update_project(content["_id"], indata["name"])
855 except ValidationError as e:
856 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
857
858
859 class RoleTopicAuth(BaseTopic):
860 topic = "roles_operations"
861 topic_msg = None # "roles"
862 schema_new = roles_new_schema
863 schema_edit = roles_edit_schema
864 multiproject = False
865
866 def __init__(self, db, fs, msg, auth, ops):
867 BaseTopic.__init__(self, db, fs, msg)
868 self.auth = auth
869 self.operations = ops
870
871 @staticmethod
872 def validate_role_definition(operations, role_definitions):
873 """
874 Validates the role definition against the operations defined in
875 the resources to operations files.
876
877 :param operations: operations list
878 :param role_definitions: role definition to test
879 :return: None if ok, raises ValidationError exception on error
880 """
881 if not role_definitions.get("permissions"):
882 return
883 ignore_fields = ["admin", "default"]
884 for role_def in role_definitions["permissions"].keys():
885 if role_def in ignore_fields:
886 continue
887 if role_def[-1] == ":":
888 raise ValidationError("Operation cannot end with ':'")
889
890 role_def_matches = [op for op in operations if op.startswith(role_def)]
891
892 if len(role_def_matches) == 0:
893 raise ValidationError("Invalid permission '{}'".format(role_def))
894
895 def _validate_input_new(self, input, force=False):
896 """
897 Validates input user content for a new entry.
898
899 :param input: user input content for the new topic
900 :param force: may be used for being more tolerant
901 :return: The same input content, or a changed version of it.
902 """
903 if self.schema_new:
904 validate_input(input, self.schema_new)
905 self.validate_role_definition(self.operations, input)
906
907 return input
908
909 def _validate_input_edit(self, input, force=False):
910 """
911 Validates input user content for updating an entry.
912
913 :param input: user input content for the new topic
914 :param force: may be used for being more tolerant
915 :return: The same input content, or a changed version of it.
916 """
917 if self.schema_edit:
918 validate_input(input, self.schema_edit)
919 self.validate_role_definition(self.operations, input)
920
921 return input
922
923 def check_conflict_on_new(self, session, indata):
924 """
925 Check that the data to be inserted is valid
926
927 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
928 :param indata: data to be inserted
929 :return: None or raises EngineException
930 """
931 # check name not exists
932 if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
933 raise EngineException("role name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT)
934
935 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
936 """
937 Check that the data to be edited/uploaded is valid
938
939 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
940 :param final_content: data once modified
941 :param edit_content: incremental data that contains the modifications to apply
942 :param _id: internal _id
943 :return: None or raises EngineException
944 """
945 if "default" not in final_content["permissions"]:
946 final_content["permissions"]["default"] = False
947 if "admin" not in final_content["permissions"]:
948 final_content["permissions"]["admin"] = False
949
950 # check name not exists
951 if "name" in edit_content:
952 role_name = edit_content["name"]
953 if self.db.get_one(self.topic, {"name": role_name, "_id.ne": _id}, fail_on_empty=False, fail_on_more=False):
954 raise EngineException("role name '{}' exists".format(role_name), HTTPStatus.CONFLICT)
955
956 def check_conflict_on_del(self, session, _id, db_content):
957 """
958 Check if deletion can be done because of dependencies if it is not force. To override
959
960 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
961 :param _id: internal _id
962 :param db_content: The database content of this item _id
963 :return: None if ok or raises EngineException with the conflict
964 """
965 roles = self.auth.get_role_list()
966 system_admin_role = [role for role in roles
967 if role["name"] == "system_admin"][0]
968
969 if _id == system_admin_role["_id"]:
970 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
971
972 @staticmethod
973 def format_on_new(content, project_id=None, make_public=False):
974 """
975 Modifies content descriptor to include _admin
976
977 :param content: descriptor to be modified
978 :param project_id: if included, it add project read/write permissions
979 :param make_public: if included it is generated as public for reading.
980 :return: None, but content is modified
981 """
982 now = time()
983 if "_admin" not in content:
984 content["_admin"] = {}
985 if not content["_admin"].get("created"):
986 content["_admin"]["created"] = now
987 content["_admin"]["modified"] = now
988
989 if "permissions" not in content:
990 content["permissions"] = {}
991
992 if "default" not in content["permissions"]:
993 content["permissions"]["default"] = False
994 if "admin" not in content["permissions"]:
995 content["permissions"]["admin"] = False
996
997 @staticmethod
998 def format_on_edit(final_content, edit_content):
999 """
1000 Modifies final_content descriptor to include the modified date.
1001
1002 :param final_content: final descriptor generated
1003 :param edit_content: alterations to be include
1004 :return: None, but final_content is modified
1005 """
1006 final_content["_admin"]["modified"] = time()
1007
1008 if "permissions" not in final_content:
1009 final_content["permissions"] = {}
1010
1011 if "default" not in final_content["permissions"]:
1012 final_content["permissions"]["default"] = False
1013 if "admin" not in final_content["permissions"]:
1014 final_content["permissions"]["admin"] = False
1015
1016 # @staticmethod
1017 # def format_on_show(content):
1018 # """
1019 # Modifies the content of the role information to separate the role
1020 # metadata from the role definition. Eases the reading process of the
1021 # role definition.
1022 #
1023 # :param definition: role definition to be processed
1024 # """
1025 # content["_id"] = str(content["_id"])
1026 #
1027 # def show(self, session, _id):
1028 # """
1029 # Get complete information on an topic
1030 #
1031 # :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1032 # :param _id: server internal id
1033 # :return: dictionary, raise exception if not found.
1034 # """
1035 # filter_db = {"_id": _id}
1036 #
1037 # role = self.db.get_one(self.topic, filter_db)
1038 # new_role = dict(role)
1039 # self.format_on_show(new_role)
1040 #
1041 # return new_role
1042
1043 # def list(self, session, filter_q=None):
1044 # """
1045 # Get a list of the topic that matches a filter
1046 #
1047 # :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1048 # :param filter_q: filter of data to be applied
1049 # :return: The list, it can be empty if no one match the filter.
1050 # """
1051 # if not filter_q:
1052 # filter_q = {}
1053 #
1054 # if ":" in filter_q:
1055 # filter_q["root"] = filter_q[":"]
1056 #
1057 # for key in filter_q.keys():
1058 # if key == "name":
1059 # continue
1060 # filter_q[key] = filter_q[key] in ["True", "true"]
1061 #
1062 # roles = self.db.get_list(self.topic, filter_q)
1063 # new_roles = []
1064 #
1065 # for role in roles:
1066 # new_role = dict(role)
1067 # self.format_on_show(new_role)
1068 # new_roles.append(new_role)
1069 #
1070 # return new_roles
1071
1072 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1073 """
1074 Creates a new entry into database.
1075
1076 :param rollback: list to append created items at database in case a rollback may to be done
1077 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1078 :param indata: data to be inserted
1079 :param kwargs: used to override the indata descriptor
1080 :param headers: http request headers
1081 :return: _id: identity of the inserted data.
1082 """
1083 try:
1084 content = self._remove_envelop(indata)
1085
1086 # Override descriptor with query string kwargs
1087 self._update_input_with_kwargs(content, kwargs)
1088 content = self._validate_input_new(content, session["force"])
1089 self.check_conflict_on_new(session, content)
1090 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
1091 role_name = content["name"]
1092 role_id = self.auth.create_role(role_name)
1093 content["_id"] = role_id
1094 _id = self.db.create(self.topic, content)
1095 rollback.append({"topic": self.topic, "_id": _id})
1096 # self._send_msg("create", content)
1097 return _id
1098 except ValidationError as e:
1099 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1100
1101 def delete(self, session, _id, dry_run=False):
1102 """
1103 Delete item by its internal _id
1104
1105 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1106 :param _id: server internal id
1107 :param dry_run: make checking but do not delete
1108 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1109 """
1110 self.check_conflict_on_del(session, _id, None)
1111 filter_q = {"_id": _id}
1112 if not dry_run:
1113 self.auth.delete_role(_id)
1114 v = self.db.del_one(self.topic, filter_q)
1115 return v
1116 return None
1117
1118 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1119 """
1120 Updates a role entry.
1121
1122 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1123 :param _id:
1124 :param indata: data to be inserted
1125 :param kwargs: used to override the indata descriptor
1126 :param content:
1127 :return: _id: identity of the inserted data.
1128 """
1129 _id = super().edit(session, _id, indata, kwargs, content)
1130 if indata.get("name"):
1131 self.auth.update_role(_id, name=indata.get("name"))