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