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