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