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