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