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