cbe2f4c82e88c745bcdaa3f6646738c6d2de42e3
[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 = "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 ignore_fields = ["_id", "_admin", "name"]
882 for role_def in role_definitions.keys():
883 if role_def in ignore_fields:
884 continue
885 if role_def == "root":
886 if isinstance(role_definitions[role_def], bool):
887 continue
888 else:
889 raise ValidationError("Operation authorization \".\" should be True/False.")
890 if role_def[-1] == ":":
891 raise ValidationError("Operation cannot end with \".\"")
892
893 role_def_matches = [op for op in operations if op.startswith(role_def)]
894
895 if len(role_def_matches) == 0:
896 raise ValidationError("No matching operation found.")
897
898 if not isinstance(role_definitions[role_def], bool):
899 raise ValidationError("Operation authorization {} should be True/False.".format(role_def))
900
901 def _validate_input_new(self, input, force=False):
902 """
903 Validates input user content for a new entry.
904
905 :param input: user input content for the new topic
906 :param force: may be used for being more tolerant
907 :return: The same input content, or a changed version of it.
908 """
909 if self.schema_new:
910 validate_input(input, self.schema_new)
911 self.validate_role_definition(self.operations, input)
912
913 return input
914
915 def _validate_input_edit(self, input, force=False):
916 """
917 Validates input user content for updating an entry.
918
919 :param input: user input content for the new topic
920 :param force: may be used for being more tolerant
921 :return: The same input content, or a changed version of it.
922 """
923 if self.schema_edit:
924 validate_input(input, self.schema_edit)
925 self.validate_role_definition(self.operations, input)
926
927 return input
928
929 def check_conflict_on_new(self, session, indata):
930 """
931 Check that the data to be inserted is valid
932
933 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
934 :param indata: data to be inserted
935 :return: None or raises EngineException
936 """
937 role = indata.get("name")
938 role_list = list(map(lambda x: x["name"], self.auth.get_role_list()))
939
940 if role in role_list:
941 raise EngineException("role '{}' exists".format(role), HTTPStatus.CONFLICT)
942
943 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
944 """
945 Check that the data to be edited/uploaded is valid
946
947 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
948 :param final_content: data once modified
949 :param edit_content: incremental data that contains the modifications to apply
950 :param _id: internal _id
951 :return: None or raises EngineException
952 """
953 roles = self.auth.get_role_list()
954 system_admin_role = [role for role in roles
955 if role["name"] == "system_admin"][0]
956
957 if _id == system_admin_role["_id"]:
958 raise EngineException("You cannot edit system_admin role", http_code=HTTPStatus.FORBIDDEN)
959
960 def check_conflict_on_del(self, session, _id, db_content):
961 """
962 Check if deletion can be done because of dependencies if it is not force. To override
963
964 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
965 :param _id: internal _id
966 :param db_content: The database content of this item _id
967 :return: None if ok or raises EngineException with the conflict
968 """
969 roles = self.auth.get_role_list()
970 system_admin_role = [role for role in roles
971 if role["name"] == "system_admin"][0]
972
973 if _id == system_admin_role["_id"]:
974 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
975
976 @staticmethod
977 def format_on_new(content, project_id=None, make_public=False):
978 """
979 Modifies content descriptor to include _admin
980
981 :param content: descriptor to be modified
982 :param project_id: if included, it add project read/write permissions
983 :param make_public: if included it is generated as public for reading.
984 :return: None, but content is modified
985 """
986 now = time()
987 if "_admin" not in content:
988 content["_admin"] = {}
989 if not content["_admin"].get("created"):
990 content["_admin"]["created"] = now
991 content["_admin"]["modified"] = now
992
993 if ":" in content.keys():
994 content["root"] = content[":"]
995 del content[":"]
996
997 if "root" not in content.keys():
998 content["root"] = False
999
1000 @staticmethod
1001 def format_on_edit(final_content, edit_content):
1002 """
1003 Modifies final_content descriptor to include the modified date.
1004
1005 :param final_content: final descriptor generated
1006 :param edit_content: alterations to be include
1007 :return: None, but final_content is modified
1008 """
1009 final_content["_admin"]["modified"] = time()
1010
1011 ignore_fields = ["_id", "name", "_admin"]
1012 delete_keys = [key for key in final_content.keys() if key not in ignore_fields]
1013
1014 for key in delete_keys:
1015 del final_content[key]
1016
1017 # Saving the role definition
1018 for role_def, value in edit_content.items():
1019 final_content[role_def] = value
1020
1021 if ":" in final_content.keys():
1022 final_content["root"] = final_content[":"]
1023 del final_content[":"]
1024
1025 if "root" not in final_content.keys():
1026 final_content["root"] = False
1027
1028 @staticmethod
1029 def format_on_show(content):
1030 """
1031 Modifies the content of the role information to separate the role
1032 metadata from the role definition. Eases the reading process of the
1033 role definition.
1034
1035 :param definition: role definition to be processed
1036 """
1037 content["_id"] = str(content["_id"])
1038
1039 def show(self, session, _id):
1040 """
1041 Get complete information on an topic
1042
1043 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1044 :param _id: server internal id
1045 :return: dictionary, raise exception if not found.
1046 """
1047 filter_db = {"_id": _id}
1048
1049 role = self.db.get_one(self.topic, filter_db)
1050 new_role = dict(role)
1051 self.format_on_show(new_role)
1052
1053 return new_role
1054
1055 def list(self, session, filter_q=None):
1056 """
1057 Get a list of the topic that matches a filter
1058
1059 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1060 :param filter_q: filter of data to be applied
1061 :return: The list, it can be empty if no one match the filter.
1062 """
1063 if not filter_q:
1064 filter_q = {}
1065
1066 if ":" in filter_q:
1067 filter_q["root"] = filter_q[":"]
1068
1069 for key in filter_q.keys():
1070 if key == "name":
1071 continue
1072 filter_q[key] = filter_q[key] in ["True", "true"]
1073
1074 roles = self.db.get_list(self.topic, filter_q)
1075 new_roles = []
1076
1077 for role in roles:
1078 new_role = dict(role)
1079 self.format_on_show(new_role)
1080 new_roles.append(new_role)
1081
1082 return new_roles
1083
1084 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1085 """
1086 Creates a new entry into database.
1087
1088 :param rollback: list to append created items at database in case a rollback may to be done
1089 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1090 :param indata: data to be inserted
1091 :param kwargs: used to override the indata descriptor
1092 :param headers: http request headers
1093 :return: _id: identity of the inserted data.
1094 """
1095 try:
1096 content = BaseTopic._remove_envelop(indata)
1097
1098 # Override descriptor with query string kwargs
1099 BaseTopic._update_input_with_kwargs(content, kwargs)
1100 content = self._validate_input_new(content, session["force"])
1101 self.check_conflict_on_new(session, content)
1102 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
1103 role_name = content["name"]
1104 role = self.auth.create_role(role_name)
1105 content["_id"] = role["_id"]
1106 _id = self.db.create(self.topic, content)
1107 rollback.append({"topic": self.topic, "_id": _id})
1108 # self._send_msg("create", content)
1109 return _id
1110 except ValidationError as e:
1111 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1112
1113 def delete(self, session, _id, dry_run=False):
1114 """
1115 Delete item by its internal _id
1116
1117 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1118 :param _id: server internal id
1119 :param dry_run: make checking but do not delete
1120 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1121 """
1122 self.check_conflict_on_del(session, _id, None)
1123 filter_q = {"_id": _id}
1124 if not dry_run:
1125 self.auth.delete_role(_id)
1126 v = self.db.del_one(self.topic, filter_q)
1127 return v
1128 return None
1129
1130 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1131 """
1132 Updates a role entry.
1133
1134 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1135 :param _id:
1136 :param indata: data to be inserted
1137 :param kwargs: used to override the indata descriptor
1138 :param content:
1139 :return: _id: identity of the inserted data.
1140 """
1141 indata = self._remove_envelop(indata)
1142
1143 # Override descriptor with query string kwargs
1144 if kwargs:
1145 self._update_input_with_kwargs(indata, kwargs)
1146 try:
1147 indata = self._validate_input_edit(indata, force=session["force"])
1148
1149 if not content:
1150 content = self.show(session, _id)
1151 self.check_conflict_on_edit(session, content, indata, _id=_id)
1152 self.format_on_edit(content, indata)
1153 self.db.replace(self.topic, _id, content)
1154 return id
1155 except ValidationError as e:
1156 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)