Enhancing role filters
[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 user_list = list(map(lambda x: x["username"], self.auth.get_user_list()))
396
397 if "projects" in indata.keys():
398 raise EngineException("Format invalid: the keyword \"projects\" is not allowed for Keystone",
399 HTTPStatus.BAD_REQUEST)
400
401 if username in user_list:
402 raise EngineException("username '{}' exists".format(username), HTTPStatus.CONFLICT)
403
404 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
405 """
406 Check that the data to be edited/uploaded is valid
407
408 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
409 :param final_content: data once modified
410 :param edit_content: incremental data that contains the modifications to apply
411 :param _id: internal _id
412 :return: None or raises EngineException
413 """
414 users = self.auth.get_user_list()
415 admin_user = [user for user in users if user["name"] == "admin"][0]
416
417 if _id == admin_user["_id"] and edit_content["project_role_mappings"]:
418 elem = {
419 "project": "admin",
420 "role": "system_admin"
421 }
422 if elem not in edit_content:
423 raise EngineException("You cannot remove system_admin role from admin user",
424 http_code=HTTPStatus.FORBIDDEN)
425
426 def check_conflict_on_del(self, session, _id, db_content):
427 """
428 Check if deletion can be done because of dependencies if it is not force. To override
429 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
430 :param _id: internal _id
431 :param db_content: The database content of this item _id
432 :return: None if ok or raises EngineException with the conflict
433 """
434 if _id == session["username"]:
435 raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
436
437 @staticmethod
438 def format_on_new(content, project_id=None, make_public=False):
439 """
440 Modifies content descriptor to include _id.
441
442 NOTE: No password salt required because the authentication backend
443 should handle these security concerns.
444
445 :param content: descriptor to be modified
446 :param make_public: if included it is generated as public for reading.
447 :return: None, but content is modified
448 """
449 BaseTopic.format_on_new(content, make_public=False)
450 content["_id"] = content["username"]
451 content["password"] = content["password"]
452
453 @staticmethod
454 def format_on_edit(final_content, edit_content):
455 """
456 Modifies final_content descriptor to include the modified date.
457
458 NOTE: No password salt required because the authentication backend
459 should handle these security concerns.
460
461 :param final_content: final descriptor generated
462 :param edit_content: alterations to be include
463 :return: None, but final_content is modified
464 """
465 BaseTopic.format_on_edit(final_content, edit_content)
466 if "password" in edit_content:
467 final_content["password"] = edit_content["password"]
468 else:
469 final_content["project_role_mappings"] = edit_content["project_role_mappings"]
470
471 @staticmethod
472 def format_on_show(content):
473 """
474 Modifies the content of the role information to separate the role
475 metadata from the role definition.
476 """
477 project_role_mappings = []
478
479 for project in content["projects"]:
480 for role in project["roles"]:
481 project_role_mappings.append([project, role])
482
483 del content["projects"]
484 content["project_role_mappings"] = project_role_mappings
485
486 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
487 """
488 Creates a new entry into the authentication backend.
489
490 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
491
492 :param rollback: list to append created items at database in case a rollback may to be done
493 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
494 :param indata: data to be inserted
495 :param kwargs: used to override the indata descriptor
496 :param headers: http request headers
497 :return: _id: identity of the inserted data.
498 """
499 try:
500 content = BaseTopic._remove_envelop(indata)
501
502 # Override descriptor with query string kwargs
503 BaseTopic._update_input_with_kwargs(content, kwargs)
504 content = self._validate_input_new(content, session["force"])
505 self.check_conflict_on_new(session, content)
506 self.format_on_new(content, session["project_id"], make_public=session["public"])
507 _id = self.auth.create_user(content["username"], content["password"])
508 rollback.append({"topic": self.topic, "_id": _id})
509 del content["password"]
510 # self._send_msg("create", content)
511 return _id
512 except ValidationError as e:
513 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
514
515 def show(self, session, _id):
516 """
517 Get complete information on an topic
518
519 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
520 :param _id: server internal id
521 :return: dictionary, raise exception if not found.
522 """
523 users = [user for user in self.auth.get_user_list() if user["_id"] == _id]
524
525 if len(users) == 1:
526 return self.format_on_show(users[0])
527 elif len(users) > 1:
528 raise EngineException("Too many users found", HTTPStatus.CONFLICT)
529 else:
530 raise EngineException("User not found", HTTPStatus.NOT_FOUND)
531
532 def edit(self, session, _id, indata=None, kwargs=None, content=None):
533 """
534 Updates an user entry.
535
536 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
537 :param _id:
538 :param indata: data to be inserted
539 :param kwargs: used to override the indata descriptor
540 :param content:
541 :return: _id: identity of the inserted data.
542 """
543 indata = self._remove_envelop(indata)
544
545 # Override descriptor with query string kwargs
546 if kwargs:
547 BaseTopic._update_input_with_kwargs(indata, kwargs)
548 try:
549 indata = self._validate_input_edit(indata, force=session["force"])
550
551 if not content:
552 content = self.show(session, _id)
553 self.check_conflict_on_edit(session, content, indata, _id=_id)
554 self.format_on_edit(content, indata)
555
556 if "password" in content:
557 self.auth.change_password(content["name"], content["password"])
558 else:
559 user = self.show(session, _id)
560 original_mapping = user["project_role_mappings"]
561 edit_mapping = content["project_role_mappings"]
562
563 mappings_to_remove = [mapping for mapping in original_mapping
564 if mapping not in edit_mapping]
565
566 mappings_to_add = [mapping for mapping in edit_mapping
567 if mapping not in original_mapping]
568
569 for mapping in mappings_to_remove:
570 self.auth.remove_role_from_user(
571 user["name"],
572 mapping[0],
573 mapping[1]
574 )
575
576 for mapping in mappings_to_add:
577 self.auth.assign_role_to_user(
578 user["name"],
579 mapping[0],
580 mapping[1]
581 )
582
583 return content["_id"]
584 except ValidationError as e:
585 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
586
587 def list(self, session, filter_q=None):
588 """
589 Get a list of the topic that matches a filter
590 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
591 :param filter_q: filter of data to be applied
592 :return: The list, it can be empty if no one match the filter.
593 """
594 if not filter_q:
595 filter_q = {}
596
597 users = [self.format_on_show(user) for user in self.auth.get_user_list(filter_q)]
598
599 return users
600
601 def delete(self, session, _id, dry_run=False):
602 """
603 Delete item by its internal _id
604
605 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
606 :param _id: server internal id
607 :param force: indicates if deletion must be forced in case of conflict
608 :param dry_run: make checking but do not delete
609 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
610 """
611 self.check_conflict_on_del(session, _id, None)
612 if not dry_run:
613 v = self.auth.delete_user(_id)
614 return v
615 return None
616
617
618 class ProjectTopicAuth(ProjectTopic):
619 # topic = "projects"
620 # topic_msg = "projects"
621 # schema_new = project_new_schema
622 # schema_edit = project_edit_schema
623
624 def __init__(self, db, fs, msg, auth):
625 ProjectTopic.__init__(self, db, fs, msg)
626 self.auth = auth
627
628 def check_conflict_on_new(self, session, indata):
629 """
630 Check that the data to be inserted is valid
631
632 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
633 :param indata: data to be inserted
634 :return: None or raises EngineException
635 """
636 project = indata.get("name")
637 project_list = list(map(lambda x: x["name"], self.auth.get_project_list()))
638
639 if project in project_list:
640 raise EngineException("project '{}' exists".format(project), HTTPStatus.CONFLICT)
641
642 def check_conflict_on_del(self, session, _id, db_content):
643 """
644 Check if deletion can be done because of dependencies if it is not force. To override
645
646 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
647 :param _id: internal _id
648 :param db_content: The database content of this item _id
649 :return: None if ok or raises EngineException with the conflict
650 """
651 projects = self.auth.get_project_list()
652 current_project = [project for project in projects
653 if project["name"] == session["project_id"]][0]
654
655 if _id == current_project["_id"]:
656 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
657
658 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
659 """
660 Creates a new entry into the authentication backend.
661
662 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
663
664 :param rollback: list to append created items at database in case a rollback may to be done
665 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
666 :param indata: data to be inserted
667 :param kwargs: used to override the indata descriptor
668 :param headers: http request headers
669 :return: _id: identity of the inserted data.
670 """
671 try:
672 content = BaseTopic._remove_envelop(indata)
673
674 # Override descriptor with query string kwargs
675 BaseTopic._update_input_with_kwargs(content, kwargs)
676 content = self._validate_input_new(content, session["force"])
677 self.check_conflict_on_new(session, content)
678 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
679 _id = self.auth.create_project(content["name"])
680 rollback.append({"topic": self.topic, "_id": _id})
681 # self._send_msg("create", content)
682 return _id
683 except ValidationError as e:
684 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
685
686 def show(self, session, _id):
687 """
688 Get complete information on an topic
689
690 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
691 :param _id: server internal id
692 :return: dictionary, raise exception if not found.
693 """
694 projects = [project for project in self.auth.get_project_list() if project["_id"] == _id]
695
696 if len(projects) == 1:
697 return projects[0]
698 elif len(projects) > 1:
699 raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
700 else:
701 raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
702
703 def list(self, session, filter_q=None):
704 """
705 Get a list of the topic that matches a filter
706
707 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
708 :param filter_q: filter of data to be applied
709 :return: The list, it can be empty if no one match the filter.
710 """
711 if not filter_q:
712 filter_q = {}
713
714 return self.auth.get_project_list(filter_q)
715
716 def delete(self, session, _id, dry_run=False):
717 """
718 Delete item by its internal _id
719
720 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
721 :param _id: server internal id
722 :param dry_run: make checking but do not delete
723 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
724 """
725 self.check_conflict_on_del(session, _id, None)
726 if not dry_run:
727 v = self.auth.delete_project(_id)
728 return v
729 return None
730
731
732 class RoleTopicAuth(BaseTopic):
733 topic = "roles_operations"
734 topic_msg = "roles"
735 schema_new = roles_new_schema
736 schema_edit = roles_edit_schema
737 multiproject = False
738
739 def __init__(self, db, fs, msg, auth, ops):
740 BaseTopic.__init__(self, db, fs, msg)
741 self.auth = auth
742 self.operations = ops
743
744 @staticmethod
745 def validate_role_definition(operations, role_definitions):
746 """
747 Validates the role definition against the operations defined in
748 the resources to operations files.
749
750 :param operations: operations list
751 :param role_definitions: role definition to test
752 :return: None if ok, raises ValidationError exception on error
753 """
754 ignore_fields = ["_id", "_admin", "name"]
755 for role_def in role_definitions.keys():
756 if role_def in ignore_fields:
757 continue
758 if role_def == ".":
759 if isinstance(role_definitions[role_def], bool):
760 continue
761 else:
762 raise ValidationError("Operation authorization \".\" should be True/False.")
763 if role_def[-1] == ".":
764 raise ValidationError("Operation cannot end with \".\"")
765
766 role_def_matches = [op for op in operations if op.startswith(role_def)]
767
768 if len(role_def_matches) == 0:
769 raise ValidationError("No matching operation found.")
770
771 if not isinstance(role_definitions[role_def], bool):
772 raise ValidationError("Operation authorization {} should be True/False.".format(role_def))
773
774 def _validate_input_new(self, input, force=False):
775 """
776 Validates input user content for a new entry.
777
778 :param input: user input content for the new topic
779 :param force: may be used for being more tolerant
780 :return: The same input content, or a changed version of it.
781 """
782 if self.schema_new:
783 validate_input(input, self.schema_new)
784 self.validate_role_definition(self.operations, input)
785
786 return input
787
788 def _validate_input_edit(self, input, force=False):
789 """
790 Validates input user content for updating an entry.
791
792 :param input: user input content for the new topic
793 :param force: may be used for being more tolerant
794 :return: The same input content, or a changed version of it.
795 """
796 if self.schema_edit:
797 validate_input(input, self.schema_edit)
798 self.validate_role_definition(self.operations, input)
799
800 return input
801
802 def check_conflict_on_new(self, session, indata):
803 """
804 Check that the data to be inserted is valid
805
806 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
807 :param indata: data to be inserted
808 :return: None or raises EngineException
809 """
810 role = indata.get("name")
811 role_list = list(map(lambda x: x["name"], self.auth.get_role_list()))
812
813 if role in role_list:
814 raise EngineException("role '{}' exists".format(role), HTTPStatus.CONFLICT)
815
816 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
817 """
818 Check that the data to be edited/uploaded is valid
819
820 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
821 :param final_content: data once modified
822 :param edit_content: incremental data that contains the modifications to apply
823 :param _id: internal _id
824 :return: None or raises EngineException
825 """
826 roles = self.auth.get_role_list()
827 system_admin_role = [role for role in roles
828 if roles["name"] == "system_admin"][0]
829
830 if _id == system_admin_role["_id"]:
831 raise EngineException("You cannot edit system_admin role", http_code=HTTPStatus.FORBIDDEN)
832
833 def check_conflict_on_del(self, session, _id, db_content):
834 """
835 Check if deletion can be done because of dependencies if it is not force. To override
836
837 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
838 :param _id: internal _id
839 :param db_content: The database content of this item _id
840 :return: None if ok or raises EngineException with the conflict
841 """
842 roles = self.auth.get_role_list()
843 system_admin_role = [role for role in roles
844 if roles["name"] == "system_admin"][0]
845
846 if _id == system_admin_role["_id"]:
847 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
848
849 @staticmethod
850 def format_on_new(content, project_id=None, make_public=False):
851 """
852 Modifies content descriptor to include _admin
853
854 :param content: descriptor to be modified
855 :param project_id: if included, it add project read/write permissions
856 :param make_public: if included it is generated as public for reading.
857 :return: None, but content is modified
858 """
859 now = time()
860 if "_admin" not in content:
861 content["_admin"] = {}
862 if not content["_admin"].get("created"):
863 content["_admin"]["created"] = now
864 content["_admin"]["modified"] = now
865 content[":"] = False
866
867 ignore_fields = ["_id", "_admin", "name"]
868 for role_def, value in content.items():
869 if role_def in ignore_fields:
870 continue
871 content[role_def.replace(".", ":")] = value
872 del content[role_def]
873
874 @staticmethod
875 def format_on_edit(final_content, edit_content):
876 """
877 Modifies final_content descriptor to include the modified date.
878
879 :param final_content: final descriptor generated
880 :param edit_content: alterations to be include
881 :return: None, but final_content is modified
882 """
883 final_content["_admin"]["modified"] = time()
884
885 ignore_fields = ["_id", "name", "_admin"]
886 delete_keys = [key for key in final_content.keys() if key not in ignore_fields]
887
888 for key in delete_keys:
889 del final_content[key]
890
891 # Saving the role definition
892 for role_def, value in edit_content.items():
893 final_content[role_def.replace(".", ":")] = value
894
895 if ":" not in final_content.keys():
896 final_content[":"] = False
897
898 @staticmethod
899 def format_on_show(content):
900 """
901 Modifies the content of the role information to separate the role
902 metadata from the role definition. Eases the reading process of the
903 role definition.
904
905 :param definition: role definition to be processed
906 """
907 content_keys = list(content.keys())
908
909 for key in content_keys:
910 if ":" in key:
911 content[key.replace(":", ".")] = content[key]
912 del content[key]
913
914 def show(self, session, _id):
915 """
916 Get complete information on an topic
917
918 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
919 :param _id: server internal id
920 :return: dictionary, raise exception if not found.
921 """
922 filter_db = self._get_project_filter(session)
923 filter_db["_id"] = _id
924
925 role = self.db.get_one(self.topic, filter_db)
926 new_role = dict(role)
927 self.format_on_show(new_role)
928
929 return new_role
930
931 def list(self, session, filter_q=None):
932 """
933 Get a list of the topic that matches a filter
934
935 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
936 :param filter_q: filter of data to be applied
937 :return: The list, it can be empty if no one match the filter.
938 """
939 if not filter_q:
940 filter_q = {}
941
942 if "root" in filter_q:
943 filter_q[":"] = filter_q["root"]
944 del filter_q["root"]
945
946 if len(filter_q) > 0:
947 keys = [key for key in filter_q.keys() if "." in key]
948
949 for key in keys:
950 filter_q[key.replace(".", ":")] = filter_q[key]
951 del filter_q[key]
952
953 roles = self.db.get_list(self.topic, filter_q)
954 new_roles = []
955
956 for role in roles:
957 new_role = dict(role)
958 self.format_on_show(new_role)
959 new_roles.append(new_role)
960
961 return new_roles
962
963 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
964 """
965 Creates a new entry into database.
966
967 :param rollback: list to append created items at database in case a rollback may to be done
968 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
969 :param indata: data to be inserted
970 :param kwargs: used to override the indata descriptor
971 :param headers: http request headers
972 :return: _id: identity of the inserted data.
973 """
974 try:
975 content = BaseTopic._remove_envelop(indata)
976
977 # Override descriptor with query string kwargs
978 BaseTopic._update_input_with_kwargs(content, kwargs)
979 content = self._validate_input_new(content, session["force"])
980 self.check_conflict_on_new(session, content)
981 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
982 role_name = content["name"]
983 role = self.auth.create_role(role_name)
984 content["_id"] = role["_id"]
985 _id = self.db.create(self.topic, content)
986 rollback.append({"topic": self.topic, "_id": _id})
987 # self._send_msg("create", content)
988 return _id
989 except ValidationError as e:
990 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
991
992 def delete(self, session, _id, dry_run=False):
993 """
994 Delete item by its internal _id
995
996 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
997 :param _id: server internal id
998 :param dry_run: make checking but do not delete
999 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1000 """
1001 self.check_conflict_on_del(session, _id, None)
1002 filter_q = self._get_project_filter(session)
1003 filter_q["_id"] = _id
1004 if not dry_run:
1005 self.auth.delete_role(_id)
1006 v = self.db.del_one(self.topic, filter_q)
1007 return v
1008 return None
1009
1010 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1011 """
1012 Updates a role entry.
1013
1014 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1015 :param _id:
1016 :param indata: data to be inserted
1017 :param kwargs: used to override the indata descriptor
1018 :param content:
1019 :return: _id: identity of the inserted data.
1020 """
1021 indata = self._remove_envelop(indata)
1022
1023 # Override descriptor with query string kwargs
1024 if kwargs:
1025 self._update_input_with_kwargs(indata, kwargs)
1026 try:
1027 indata = self._validate_input_edit(indata, force=session["force"])
1028
1029 if not content:
1030 content = self.show(session, _id)
1031 self.check_conflict_on_edit(session, content, indata, _id=_id)
1032 self.format_on_edit(content, indata)
1033 self.db.replace(self.topic, _id, content)
1034 return id
1035 except ValidationError as e:
1036 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)