Adding filter to UserTopicAuth
[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 return self.auth.get_project_list()
677
678 def delete(self, session, _id, dry_run=False):
679 """
680 Delete item by its internal _id
681
682 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
683 :param _id: server internal id
684 :param dry_run: make checking but do not delete
685 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
686 """
687 self.check_conflict_on_del(session, _id)
688 if not dry_run:
689 v = self.auth.delete_project(_id)
690 return v
691 return None
692
693
694 class RoleTopicAuth(BaseTopic):
695 topic = "roles_operations"
696 topic_msg = "roles"
697 schema_new = roles_new_schema
698 schema_edit = roles_edit_schema
699 multiproject = False
700
701 def __init__(self, db, fs, msg, auth, ops):
702 BaseTopic.__init__(self, db, fs, msg)
703 self.auth = auth
704 self.operations = ops
705
706 @staticmethod
707 def validate_role_definition(operations, role_definitions):
708 """
709 Validates the role definition against the operations defined in
710 the resources to operations files.
711
712 :param operations: operations list
713 :param role_definitions: role definition to test
714 :return: None if ok, raises ValidationError exception on error
715 """
716 for role_def in role_definitions.keys():
717 if role_def == ".":
718 continue
719 if role_def[-1] == ".":
720 raise ValidationError("Operation cannot end with \".\"")
721
722 role_def_matches = [op for op in operations if op.startswith(role_def)]
723
724 if len(role_def_matches) == 0:
725 raise ValidationError("No matching operation found.")
726
727 def _validate_input_new(self, input, force=False):
728 """
729 Validates input user content for a new entry.
730
731 :param input: user input content for the new topic
732 :param force: may be used for being more tolerant
733 :return: The same input content, or a changed version of it.
734 """
735 if self.schema_new:
736 validate_input(input, self.schema_new)
737 if "definition" in input and input["definition"]:
738 self.validate_role_definition(self.operations, input["definition"])
739 return input
740
741 def _validate_input_edit(self, input, force=False):
742 """
743 Validates input user content for updating an entry.
744
745 :param input: user input content for the new topic
746 :param force: may be used for being more tolerant
747 :return: The same input content, or a changed version of it.
748 """
749 if self.schema_edit:
750 validate_input(input, self.schema_edit)
751 if "definition" in input and input["definition"]:
752 self.validate_role_definition(self.operations, input["definition"])
753 return input
754
755 def check_conflict_on_new(self, session, indata):
756 """
757 Check that the data to be inserted is valid
758
759 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
760 :param indata: data to be inserted
761 :return: None or raises EngineException
762 """
763 role = indata.get("name")
764 role_list = list(map(lambda x: x["name"], self.auth.get_role_list()))
765
766 if role in role_list:
767 raise EngineException("role '{}' exists".format(role), HTTPStatus.CONFLICT)
768
769 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
770 """
771 Check that the data to be edited/uploaded is valid
772
773 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
774 :param final_content: data once modified
775 :param edit_content: incremental data that contains the modifications to apply
776 :param _id: internal _id
777 :return: None or raises EngineException
778 """
779 roles = self.auth.get_role_list()
780 system_admin_role = [role for role in roles
781 if roles["name"] == "system_admin"][0]
782
783 if _id == system_admin_role["_id"]:
784 raise EngineException("You cannot edit system_admin role", http_code=HTTPStatus.FORBIDDEN)
785
786 def check_conflict_on_del(self, session, _id):
787 """
788 Check if deletion can be done because of dependencies if it is not force. To override
789
790 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
791 :param _id: internal _id
792 :return: None if ok or raises EngineException with the conflict
793 """
794 roles = self.auth.get_role_list()
795 system_admin_role = [role for role in roles
796 if roles["name"] == "system_admin"][0]
797
798 if _id == system_admin_role["_id"]:
799 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
800
801 @staticmethod
802 def format_on_new(content, project_id=None, make_public=False):
803 """
804 Modifies content descriptor to include _admin
805
806 :param content: descriptor to be modified
807 :param project_id: if included, it add project read/write permissions
808 :param make_public: if included it is generated as public for reading.
809 :return: None, but content is modified
810 """
811 now = time()
812 if "_admin" not in content:
813 content["_admin"] = {}
814 if not content["_admin"].get("created"):
815 content["_admin"]["created"] = now
816 content["_admin"]["modified"] = now
817 content["root"] = False
818
819 # Saving the role definition
820 if "definition" in content and content["definition"]:
821 for role_def, value in content["definition"].items():
822 if role_def == ".":
823 content["root"] = value
824 else:
825 content[role_def.replace(".", ":")] = value
826
827 # Cleaning undesired values
828 if "definition" in content:
829 del content["definition"]
830
831 @staticmethod
832 def format_on_edit(final_content, edit_content):
833 """
834 Modifies final_content descriptor to include the modified date.
835
836 :param final_content: final descriptor generated
837 :param edit_content: alterations to be include
838 :return: None, but final_content is modified
839 """
840 final_content["_admin"]["modified"] = time()
841
842 ignore_fields = ["_id", "name", "_admin"]
843 delete_keys = [key for key in final_content.keys() if key not in ignore_fields]
844
845 for key in delete_keys:
846 del final_content[key]
847
848 # Saving the role definition
849 if "definition" in edit_content and edit_content["definition"]:
850 for role_def, value in edit_content["definition"].items():
851 if role_def == ".":
852 final_content["root"] = value
853 else:
854 final_content[role_def.replace(".", ":")] = value
855
856 if "root" not in final_content:
857 final_content["root"] = False
858
859 @staticmethod
860 def format_on_show(content):
861 """
862 Modifies the content of the role information to separate the role
863 metadata from the role definition. Eases the reading process of the
864 role definition.
865
866 :param definition: role definition to be processed
867 """
868 ignore_fields = ["_admin", "_id", "name", "root"]
869 content_keys = list(content.keys())
870 definition = dict(content)
871
872 for key in content_keys:
873 if key in ignore_fields:
874 del definition[key]
875 if ":" not in key:
876 del content[key]
877 continue
878 definition[key.replace(":", ".")] = definition[key]
879 del definition[key]
880 del content[key]
881
882 content["definition"] = definition
883
884 def show(self, session, _id):
885 """
886 Get complete information on an topic
887
888 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
889 :param _id: server internal id
890 :return: dictionary, raise exception if not found.
891 """
892 filter_db = self._get_project_filter(session, write=False, show_all=True)
893 filter_db["_id"] = _id
894
895 role = self.db.get_one(self.topic, filter_db)
896 new_role = dict(role)
897 self.format_on_show(new_role)
898
899 return new_role
900
901 def list(self, session, filter_q=None):
902 """
903 Get a list of the topic that matches a filter
904
905 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
906 :param filter_q: filter of data to be applied
907 :return: The list, it can be empty if no one match the filter.
908 """
909 if not filter_q:
910 filter_q = {}
911
912 roles = self.db.get_list(self.topic, filter_q)
913 new_roles = []
914
915 for role in roles:
916 new_role = dict(role)
917 self.format_on_show(new_role)
918 new_roles.append(new_role)
919
920 return new_roles
921
922 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
923 """
924 Creates a new entry into database.
925
926 :param rollback: list to append created items at database in case a rollback may to be done
927 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
928 :param indata: data to be inserted
929 :param kwargs: used to override the indata descriptor
930 :param headers: http request headers
931 :return: _id: identity of the inserted data.
932 """
933 try:
934 content = BaseTopic._remove_envelop(indata)
935
936 # Override descriptor with query string kwargs
937 BaseTopic._update_input_with_kwargs(content, kwargs)
938 content = self._validate_input_new(content, session["force"])
939 self.check_conflict_on_new(session, content)
940 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
941 role_name = content["name"]
942 role = self.auth.create_role(role_name)
943 content["_id"] = role["_id"]
944 _id = self.db.create(self.topic, content)
945 rollback.append({"topic": self.topic, "_id": _id})
946 # self._send_msg("create", content)
947 return _id
948 except ValidationError as e:
949 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
950
951 def delete(self, session, _id, dry_run=False):
952 """
953 Delete item by its internal _id
954
955 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
956 :param _id: server internal id
957 :param dry_run: make checking but do not delete
958 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
959 """
960 self.check_conflict_on_del(session, _id)
961 filter_q = self._get_project_filter(session, write=True, show_all=True)
962 filter_q["_id"] = _id
963 if not dry_run:
964 self.auth.delete_role(_id)
965 v = self.db.del_one(self.topic, filter_q)
966 return v
967 return None
968
969 def edit(self, session, _id, indata=None, kwargs=None, content=None):
970 """
971 Updates a role entry.
972
973 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
974 :param _id:
975 :param indata: data to be inserted
976 :param kwargs: used to override the indata descriptor
977 :param content:
978 :return: _id: identity of the inserted data.
979 """
980 indata = self._remove_envelop(indata)
981
982 # Override descriptor with query string kwargs
983 if kwargs:
984 BaseTopic._update_input_with_kwargs(indata, kwargs)
985 try:
986 indata = self._validate_input_edit(indata, force=session["force"])
987
988 if not content:
989 content = self.show(session, _id)
990 self.check_conflict_on_edit(session, content, indata, _id=_id)
991 self.format_on_edit(content, indata)
992 self.db.replace(self.topic, _id, content)
993 return id
994 except ValidationError as e:
995 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)