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