Addition of PaaS
[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 osm_nbi.validation import (
22 user_new_schema,
23 user_edit_schema,
24 project_new_schema,
25 project_edit_schema,
26 vim_account_new_schema,
27 vim_account_edit_schema,
28 sdn_new_schema,
29 sdn_edit_schema,
30 wim_account_new_schema,
31 wim_account_edit_schema,
32 roles_new_schema,
33 roles_edit_schema,
34 k8scluster_new_schema,
35 k8scluster_edit_schema,
36 k8srepo_new_schema,
37 k8srepo_edit_schema,
38 vca_new_schema,
39 vca_edit_schema,
40 paas_new_schema,
41 paas_edit_schema,
42 osmrepo_new_schema,
43 osmrepo_edit_schema,
44 validate_input,
45 ValidationError,
46 is_valid_uuid,
47 ) # To check that User/Project Names don't look like UUIDs
48 from osm_nbi.base_topic import BaseTopic, EngineException
49 from osm_nbi.authconn import AuthconnNotFoundException, AuthconnConflictException
50 from osm_common.dbbase import deep_update_rfc7396
51 import copy
52
53 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
54
55
56 class UserTopic(BaseTopic):
57 topic = "users"
58 topic_msg = "users"
59 schema_new = user_new_schema
60 schema_edit = user_edit_schema
61 multiproject = False
62
63 def __init__(self, db, fs, msg, auth):
64 BaseTopic.__init__(self, db, fs, msg, auth)
65
66 @staticmethod
67 def _get_project_filter(session):
68 """
69 Generates a filter dictionary for querying database users.
70 Current policy is admin can show all, non admin, only its own user.
71 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
72 :return:
73 """
74 if session["admin"]: # allows all
75 return {}
76 else:
77 return {"username": session["username"]}
78
79 def check_conflict_on_new(self, session, indata):
80 # check username not exists
81 if self.db.get_one(
82 self.topic,
83 {"username": indata.get("username")},
84 fail_on_empty=False,
85 fail_on_more=False,
86 ):
87 raise EngineException(
88 "username '{}' exists".format(indata["username"]), HTTPStatus.CONFLICT
89 )
90 # check projects
91 if not session["force"]:
92 for p in indata.get("projects") or []:
93 # To allow project addressing by Name as well as ID
94 if not self.db.get_one(
95 "projects",
96 {BaseTopic.id_field("projects", p): p},
97 fail_on_empty=False,
98 fail_on_more=False,
99 ):
100 raise EngineException(
101 "project '{}' does not exist".format(p), HTTPStatus.CONFLICT
102 )
103
104 def check_conflict_on_del(self, session, _id, db_content):
105 """
106 Check if deletion can be done because of dependencies if it is not force. To override
107 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
108 :param _id: internal _id
109 :param db_content: The database content of this item _id
110 :return: None if ok or raises EngineException with the conflict
111 """
112 if _id == session["username"]:
113 raise EngineException(
114 "You cannot delete your own user", http_code=HTTPStatus.CONFLICT
115 )
116
117 @staticmethod
118 def format_on_new(content, project_id=None, make_public=False):
119 BaseTopic.format_on_new(content, make_public=False)
120 # Removed so that the UUID is kept, to allow User Name modification
121 # content["_id"] = content["username"]
122 salt = uuid4().hex
123 content["_admin"]["salt"] = salt
124 if content.get("password"):
125 content["password"] = sha256(
126 content["password"].encode("utf-8") + salt.encode("utf-8")
127 ).hexdigest()
128 if content.get("project_role_mappings"):
129 projects = [
130 mapping["project"] for mapping in content["project_role_mappings"]
131 ]
132
133 if content.get("projects"):
134 content["projects"] += projects
135 else:
136 content["projects"] = projects
137
138 @staticmethod
139 def format_on_edit(final_content, edit_content):
140 BaseTopic.format_on_edit(final_content, edit_content)
141 if edit_content.get("password"):
142 salt = uuid4().hex
143 final_content["_admin"]["salt"] = salt
144 final_content["password"] = sha256(
145 edit_content["password"].encode("utf-8") + salt.encode("utf-8")
146 ).hexdigest()
147 return None
148
149 def edit(self, session, _id, indata=None, kwargs=None, content=None):
150 if not session["admin"]:
151 raise EngineException(
152 "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED
153 )
154 # Names that look like UUIDs are not allowed
155 name = (indata if indata else kwargs).get("username")
156 if is_valid_uuid(name):
157 raise EngineException(
158 "Usernames that look like UUIDs are not allowed",
159 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
160 )
161 return BaseTopic.edit(
162 self, session, _id, indata=indata, kwargs=kwargs, content=content
163 )
164
165 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
166 if not session["admin"]:
167 raise EngineException(
168 "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED
169 )
170 # Names that look like UUIDs are not allowed
171 name = indata["username"] if indata else kwargs["username"]
172 if is_valid_uuid(name):
173 raise EngineException(
174 "Usernames that look like UUIDs are not allowed",
175 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
176 )
177 return BaseTopic.new(
178 self, rollback, session, indata=indata, kwargs=kwargs, headers=headers
179 )
180
181
182 class ProjectTopic(BaseTopic):
183 topic = "projects"
184 topic_msg = "projects"
185 schema_new = project_new_schema
186 schema_edit = project_edit_schema
187 multiproject = False
188
189 def __init__(self, db, fs, msg, auth):
190 BaseTopic.__init__(self, db, fs, msg, auth)
191
192 @staticmethod
193 def _get_project_filter(session):
194 """
195 Generates a filter dictionary for querying database users.
196 Current policy is admin can show all, non admin, only its own user.
197 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
198 :return:
199 """
200 if session["admin"]: # allows all
201 return {}
202 else:
203 return {"_id.cont": session["project_id"]}
204
205 def check_conflict_on_new(self, session, indata):
206 if not indata.get("name"):
207 raise EngineException("missing 'name'")
208 # check name not exists
209 if self.db.get_one(
210 self.topic,
211 {"name": indata.get("name")},
212 fail_on_empty=False,
213 fail_on_more=False,
214 ):
215 raise EngineException(
216 "name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT
217 )
218
219 @staticmethod
220 def format_on_new(content, project_id=None, make_public=False):
221 BaseTopic.format_on_new(content, None)
222 # Removed so that the UUID is kept, to allow Project Name modification
223 # content["_id"] = content["name"]
224
225 def check_conflict_on_del(self, session, _id, db_content):
226 """
227 Check if deletion can be done because of dependencies if it is not force. To override
228 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
229 :param _id: internal _id
230 :param db_content: The database content of this item _id
231 :return: None if ok or raises EngineException with the conflict
232 """
233 if _id in session["project_id"]:
234 raise EngineException(
235 "You cannot delete your own project", http_code=HTTPStatus.CONFLICT
236 )
237 if session["force"]:
238 return
239 _filter = {"projects": _id}
240 if self.db.get_list("users", _filter):
241 raise EngineException(
242 "There is some USER that contains this project",
243 http_code=HTTPStatus.CONFLICT,
244 )
245
246 def edit(self, session, _id, indata=None, kwargs=None, content=None):
247 if not session["admin"]:
248 raise EngineException(
249 "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED
250 )
251 # Names that look like UUIDs are not allowed
252 name = (indata if indata else kwargs).get("name")
253 if is_valid_uuid(name):
254 raise EngineException(
255 "Project names that look like UUIDs are not allowed",
256 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
257 )
258 return BaseTopic.edit(
259 self, session, _id, indata=indata, kwargs=kwargs, content=content
260 )
261
262 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
263 if not session["admin"]:
264 raise EngineException(
265 "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED
266 )
267 # Names that look like UUIDs are not allowed
268 name = indata["name"] if indata else kwargs["name"]
269 if is_valid_uuid(name):
270 raise EngineException(
271 "Project names that look like UUIDs are not allowed",
272 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
273 )
274 return BaseTopic.new(
275 self, rollback, session, indata=indata, kwargs=kwargs, headers=headers
276 )
277
278
279 class CommonVimWimSdn(BaseTopic):
280 """Common class for VIM, WIM SDN just to unify methods that are equal to all of them"""
281
282 config_to_encrypt = (
283 {}
284 ) # what keys at config must be encrypted because contains passwords
285 password_to_encrypt = "" # key that contains a password
286
287 @staticmethod
288 def _create_operation(op_type, params=None):
289 """
290 Creates a dictionary with the information to an operation, similar to ns-lcm-op
291 :param op_type: can be create, edit, delete
292 :param params: operation input parameters
293 :return: new dictionary with
294 """
295 now = time()
296 return {
297 "lcmOperationType": op_type,
298 "operationState": "PROCESSING",
299 "startTime": now,
300 "statusEnteredTime": now,
301 "detailed-status": "",
302 "operationParams": params,
303 }
304
305 def check_conflict_on_new(self, session, indata):
306 """
307 Check that the data to be inserted is valid. It is checked that name is unique
308 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
309 :param indata: data to be inserted
310 :return: None or raises EngineException
311 """
312 self.check_unique_name(session, indata["name"], _id=None)
313
314 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
315 """
316 Check that the data to be edited/uploaded is valid. It is checked that name is unique
317 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
318 :param final_content: data once modified. This method may change it.
319 :param edit_content: incremental data that contains the modifications to apply
320 :param _id: internal _id
321 :return: None or raises EngineException
322 """
323 if not session["force"] and edit_content.get("name"):
324 self.check_unique_name(session, edit_content["name"], _id=_id)
325
326 return final_content
327
328 def format_on_edit(self, final_content, edit_content):
329 """
330 Modifies final_content inserting admin information upon edition
331 :param final_content: final content to be stored at database
332 :param edit_content: user requested update content
333 :return: operation id
334 """
335 super().format_on_edit(final_content, edit_content)
336
337 # encrypt passwords
338 schema_version = final_content.get("schema_version")
339 if schema_version:
340 if edit_content.get(self.password_to_encrypt):
341 final_content[self.password_to_encrypt] = self.db.encrypt(
342 edit_content[self.password_to_encrypt],
343 schema_version=schema_version,
344 salt=final_content["_id"],
345 )
346 config_to_encrypt_keys = self.config_to_encrypt.get(
347 schema_version
348 ) or self.config_to_encrypt.get("default")
349 if edit_content.get("config") and config_to_encrypt_keys:
350
351 for p in config_to_encrypt_keys:
352 if edit_content["config"].get(p):
353 final_content["config"][p] = self.db.encrypt(
354 edit_content["config"][p],
355 schema_version=schema_version,
356 salt=final_content["_id"],
357 )
358
359 # create edit operation
360 final_content["_admin"]["operations"].append(self._create_operation("edit"))
361 return "{}:{}".format(
362 final_content["_id"], len(final_content["_admin"]["operations"]) - 1
363 )
364
365 def format_on_new(self, content, project_id=None, make_public=False):
366 """
367 Modifies content descriptor to include _admin and insert create operation
368 :param content: descriptor to be modified
369 :param project_id: if included, it add project read/write permissions. Can be None or a list
370 :param make_public: if included it is generated as public for reading.
371 :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified
372 """
373 super().format_on_new(content, project_id=project_id, make_public=make_public)
374 content["schema_version"] = schema_version = "1.11"
375 self._encrypt_password(content, schema_version)
376 self._encrypt_config_fields(content, schema_version)
377 content["_admin"]["operationalState"] = "PROCESSING"
378 self._insert_create_operation(content)
379 return "{}:0".format(content["_id"])
380
381 def _encrypt_password(self, content, schema_version):
382 if content.get(self.password_to_encrypt):
383 content[self.password_to_encrypt] = self.db.encrypt(
384 content[self.password_to_encrypt],
385 schema_version=schema_version,
386 salt=content["_id"],
387 )
388
389 def _encrypt_config_fields(self, content, schema_version):
390 config_to_encrypt_keys = self.config_to_encrypt.get(
391 schema_version
392 ) or self.config_to_encrypt.get("default")
393 if content.get("config") and config_to_encrypt_keys:
394 for p in config_to_encrypt_keys:
395 if content["config"].get(p):
396 content["config"][p] = self.db.encrypt(
397 content["config"][p],
398 schema_version=schema_version,
399 salt=content["_id"],
400 )
401
402 def _insert_create_operation(self, content):
403 # create operation
404 content["_admin"]["operations"] = [self._create_operation("create")]
405 content["_admin"]["current_operation"] = None
406 # create Resource in Openstack based VIM
407 if content.get("vim_type"):
408 if content["vim_type"] == "openstack":
409 compute = {
410 "ram": {"total": None, "used": None},
411 "vcpus": {"total": None, "used": None},
412 "instances": {"total": None, "used": None},
413 }
414 storage = {
415 "volumes": {"total": None, "used": None},
416 "snapshots": {"total": None, "used": None},
417 "storage": {"total": None, "used": None},
418 }
419 network = {
420 "networks": {"total": None, "used": None},
421 "subnets": {"total": None, "used": None},
422 "floating_ips": {"total": None, "used": None},
423 }
424 content["resources"] = {
425 "compute": compute,
426 "storage": storage,
427 "network": network,
428 }
429
430 def delete(self, session, _id, dry_run=False, not_send_msg=None):
431 """
432 Delete item by its internal _id
433 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
434 :param _id: server internal id
435 :param dry_run: make checking but do not delete
436 :param not_send_msg: To not send message (False) or store content (list) instead
437 :return: operation id if it is ordered to delete. None otherwise
438 """
439
440 filter_q = self._get_project_filter(session)
441 filter_q["_id"] = _id
442 db_content = self.db.get_one(self.topic, filter_q)
443
444 self.check_conflict_on_del(session, _id, db_content)
445 if dry_run:
446 return None
447
448 # remove reference from project_read if there are more projects referencing it. If it last one,
449 # do not remove reference, but order via kafka to delete it
450 if session["project_id"] and session["project_id"]:
451 other_projects_referencing = next(
452 (
453 p
454 for p in db_content["_admin"]["projects_read"]
455 if p not in session["project_id"] and p != "ANY"
456 ),
457 None,
458 )
459
460 # check if there are projects referencing it (apart from ANY, that means, public)....
461 if other_projects_referencing:
462 # remove references but not delete
463 update_dict_pull = {
464 "_admin.projects_read": session["project_id"],
465 "_admin.projects_write": session["project_id"],
466 }
467 self.db.set_one(
468 self.topic, filter_q, update_dict=None, pull_list=update_dict_pull
469 )
470 return None
471 else:
472 can_write = next(
473 (
474 p
475 for p in db_content["_admin"]["projects_write"]
476 if p == "ANY" or p in session["project_id"]
477 ),
478 None,
479 )
480 if not can_write:
481 raise EngineException(
482 "You have not write permission to delete it",
483 http_code=HTTPStatus.UNAUTHORIZED,
484 )
485
486 # It must be deleted
487 if session["force"]:
488 self.db.del_one(self.topic, {"_id": _id})
489 op_id = None
490 self._send_msg(
491 "deleted", {"_id": _id, "op_id": op_id}, not_send_msg=not_send_msg
492 )
493 else:
494 update_dict = {"_admin.to_delete": True}
495 self.db.set_one(
496 self.topic,
497 {"_id": _id},
498 update_dict=update_dict,
499 push={"_admin.operations": self._create_operation("delete")},
500 )
501 # the number of operations is the operation_id. db_content does not contains the new operation inserted,
502 # so the -1 is not needed
503 op_id = "{}:{}".format(
504 db_content["_id"], len(db_content["_admin"]["operations"])
505 )
506 self._send_msg(
507 "delete", {"_id": _id, "op_id": op_id}, not_send_msg=not_send_msg
508 )
509 return op_id
510
511
512 class VimAccountTopic(CommonVimWimSdn):
513 topic = "vim_accounts"
514 topic_msg = "vim_account"
515 schema_new = vim_account_new_schema
516 schema_edit = vim_account_edit_schema
517 multiproject = True
518 password_to_encrypt = "vim_password"
519 config_to_encrypt = {
520 "1.1": ("admin_password", "nsx_password", "vcenter_password"),
521 "default": (
522 "admin_password",
523 "nsx_password",
524 "vcenter_password",
525 "vrops_password",
526 ),
527 }
528
529 def check_conflict_on_del(self, session, _id, db_content):
530 """
531 Check if deletion can be done because of dependencies if it is not force. To override
532 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
533 :param _id: internal _id
534 :param db_content: The database content of this item _id
535 :return: None if ok or raises EngineException with the conflict
536 """
537 if session["force"]:
538 return
539 # check if used by VNF
540 if self.db.get_list("vnfrs", {"vim-account-id": _id}):
541 raise EngineException(
542 "There is at least one VNF using this VIM account",
543 http_code=HTTPStatus.CONFLICT,
544 )
545 super().check_conflict_on_del(session, _id, db_content)
546
547
548 class WimAccountTopic(CommonVimWimSdn):
549 topic = "wim_accounts"
550 topic_msg = "wim_account"
551 schema_new = wim_account_new_schema
552 schema_edit = wim_account_edit_schema
553 multiproject = True
554 password_to_encrypt = "password"
555 config_to_encrypt = {}
556
557
558 class SdnTopic(CommonVimWimSdn):
559 topic = "sdns"
560 topic_msg = "sdn"
561 quota_name = "sdn_controllers"
562 schema_new = sdn_new_schema
563 schema_edit = sdn_edit_schema
564 multiproject = True
565 password_to_encrypt = "password"
566 config_to_encrypt = {}
567
568 def _obtain_url(self, input, create):
569 if input.get("ip") or input.get("port"):
570 if not input.get("ip") or not input.get("port") or input.get("url"):
571 raise ValidationError(
572 "You must provide both 'ip' and 'port' (deprecated); or just 'url' (prefered)"
573 )
574 input["url"] = "http://{}:{}/".format(input["ip"], input["port"])
575 del input["ip"]
576 del input["port"]
577 elif create and not input.get("url"):
578 raise ValidationError("You must provide 'url'")
579 return input
580
581 def _validate_input_new(self, input, force=False):
582 input = super()._validate_input_new(input, force)
583 return self._obtain_url(input, True)
584
585 def _validate_input_edit(self, input, content, force=False):
586 input = super()._validate_input_edit(input, content, force)
587 return self._obtain_url(input, False)
588
589
590 class K8sClusterTopic(CommonVimWimSdn):
591 topic = "k8sclusters"
592 topic_msg = "k8scluster"
593 schema_new = k8scluster_new_schema
594 schema_edit = k8scluster_edit_schema
595 multiproject = True
596 password_to_encrypt = None
597 config_to_encrypt = {}
598
599 def format_on_new(self, content, project_id=None, make_public=False):
600 oid = super().format_on_new(content, project_id, make_public)
601 self.db.encrypt_decrypt_fields(
602 content["credentials"],
603 "encrypt",
604 ["password", "secret"],
605 schema_version=content["schema_version"],
606 salt=content["_id"],
607 )
608 # Add Helm/Juju Repo lists
609 repos = {"helm-chart": [], "juju-bundle": []}
610 for proj in content["_admin"]["projects_read"]:
611 if proj != "ANY":
612 for repo in self.db.get_list(
613 "k8srepos", {"_admin.projects_read": proj}
614 ):
615 if repo["_id"] not in repos[repo["type"]]:
616 repos[repo["type"]].append(repo["_id"])
617 for k in repos:
618 content["_admin"][k.replace("-", "_") + "_repos"] = repos[k]
619 return oid
620
621 def format_on_edit(self, final_content, edit_content):
622 if final_content.get("schema_version") and edit_content.get("credentials"):
623 self.db.encrypt_decrypt_fields(
624 edit_content["credentials"],
625 "encrypt",
626 ["password", "secret"],
627 schema_version=final_content["schema_version"],
628 salt=final_content["_id"],
629 )
630 deep_update_rfc7396(
631 final_content["credentials"], edit_content["credentials"]
632 )
633 oid = super().format_on_edit(final_content, edit_content)
634 return oid
635
636 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
637 final_content = super(CommonVimWimSdn, self).check_conflict_on_edit(
638 session, final_content, edit_content, _id
639 )
640 final_content = super().check_conflict_on_edit(
641 session, final_content, edit_content, _id
642 )
643 # Update Helm/Juju Repo lists
644 repos = {"helm-chart": [], "juju-bundle": []}
645 for proj in session.get("set_project", []):
646 if proj != "ANY":
647 for repo in self.db.get_list(
648 "k8srepos", {"_admin.projects_read": proj}
649 ):
650 if repo["_id"] not in repos[repo["type"]]:
651 repos[repo["type"]].append(repo["_id"])
652 for k in repos:
653 rlist = k.replace("-", "_") + "_repos"
654 if rlist not in final_content["_admin"]:
655 final_content["_admin"][rlist] = []
656 final_content["_admin"][rlist] += repos[k]
657 return final_content
658
659 def check_conflict_on_del(self, session, _id, db_content):
660 """
661 Check if deletion can be done because of dependencies if it is not force. To override
662 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
663 :param _id: internal _id
664 :param db_content: The database content of this item _id
665 :return: None if ok or raises EngineException with the conflict
666 """
667 if session["force"]:
668 return
669 # check if used by VNF
670 filter_q = {"kdur.k8s-cluster.id": _id}
671 if session["project_id"]:
672 filter_q["_admin.projects_read.cont"] = session["project_id"]
673 if self.db.get_list("vnfrs", filter_q):
674 raise EngineException(
675 "There is at least one VNF using this k8scluster",
676 http_code=HTTPStatus.CONFLICT,
677 )
678 super().check_conflict_on_del(session, _id, db_content)
679
680
681 class VcaTopic(CommonVimWimSdn):
682 topic = "vca"
683 topic_msg = "vca"
684 schema_new = vca_new_schema
685 schema_edit = vca_edit_schema
686 multiproject = True
687 password_to_encrypt = None
688
689 def format_on_new(self, content, project_id=None, make_public=False):
690 oid = super().format_on_new(content, project_id, make_public)
691 content["schema_version"] = schema_version = "1.11"
692 for key in ["secret", "cacert"]:
693 content[key] = self.db.encrypt(
694 content[key], schema_version=schema_version, salt=content["_id"]
695 )
696 return oid
697
698 def format_on_edit(self, final_content, edit_content):
699 oid = super().format_on_edit(final_content, edit_content)
700 schema_version = final_content.get("schema_version")
701 for key in ["secret", "cacert"]:
702 if key in edit_content:
703 final_content[key] = self.db.encrypt(
704 edit_content[key],
705 schema_version=schema_version,
706 salt=final_content["_id"],
707 )
708 return oid
709
710 def check_conflict_on_del(self, session, _id, db_content):
711 """
712 Check if deletion can be done because of dependencies if it is not force. To override
713 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
714 :param _id: internal _id
715 :param db_content: The database content of this item _id
716 :return: None if ok or raises EngineException with the conflict
717 """
718 if session["force"]:
719 return
720 # check if used by VNF
721 filter_q = {"vca": _id}
722 if session["project_id"]:
723 filter_q["_admin.projects_read.cont"] = session["project_id"]
724 if self.db.get_list("vim_accounts", filter_q):
725 raise EngineException(
726 "There is at least one VIM account using this vca",
727 http_code=HTTPStatus.CONFLICT,
728 )
729 super().check_conflict_on_del(session, _id, db_content)
730
731
732 class PaasTopic(CommonVimWimSdn):
733 topic = "paas"
734 topic_msg = "paas"
735 schema_new = paas_new_schema
736 schema_edit = paas_edit_schema
737 multiproject = True
738 password_to_encrypt = "secret"
739 config_to_encrypt = {}
740
741 def format_on_edit(self, final_content, edit_content):
742 oid = super().format_on_edit(final_content, edit_content)
743 final_content["_admin"]["operationalState"] = "PROCESSING"
744 final_content["_admin"]["detailed-status"] = "Editing"
745 return oid
746
747 def _check_if_used_by_ns(self):
748 pass
749
750 def check_conflict_on_del(self, session, _id, db_content):
751 """
752 Check if deletion can be done because of dependencies if it is not force.
753 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
754 :param _id: internal _id
755 :param db_content: The database content of this item _id
756 :return: None if ok or raises EngineException with the conflict
757 """
758 if session["force"]:
759 return
760 self._check_if_used_by_ns()
761
762 super().check_conflict_on_del(session, _id, db_content)
763
764
765 class K8sRepoTopic(CommonVimWimSdn):
766 topic = "k8srepos"
767 topic_msg = "k8srepo"
768 schema_new = k8srepo_new_schema
769 schema_edit = k8srepo_edit_schema
770 multiproject = True
771 password_to_encrypt = None
772 config_to_encrypt = {}
773
774 def format_on_new(self, content, project_id=None, make_public=False):
775 oid = super().format_on_new(content, project_id, make_public)
776 # Update Helm/Juju Repo lists
777 repo_list = content["type"].replace("-", "_") + "_repos"
778 for proj in content["_admin"]["projects_read"]:
779 if proj != "ANY":
780 self.db.set_list(
781 "k8sclusters",
782 {
783 "_admin.projects_read": proj,
784 "_admin." + repo_list + ".ne": content["_id"],
785 },
786 {},
787 push={"_admin." + repo_list: content["_id"]},
788 )
789 return oid
790
791 def delete(self, session, _id, dry_run=False, not_send_msg=None):
792 type = self.db.get_one("k8srepos", {"_id": _id})["type"]
793 oid = super().delete(session, _id, dry_run, not_send_msg)
794 if oid:
795 # Remove from Helm/Juju Repo lists
796 repo_list = type.replace("-", "_") + "_repos"
797 self.db.set_list(
798 "k8sclusters",
799 {"_admin." + repo_list: _id},
800 {},
801 pull={"_admin." + repo_list: _id},
802 )
803 return oid
804
805
806 class OsmRepoTopic(BaseTopic):
807 topic = "osmrepos"
808 topic_msg = "osmrepos"
809 schema_new = osmrepo_new_schema
810 schema_edit = osmrepo_edit_schema
811 multiproject = True
812 # TODO: Implement user/password
813
814
815 class UserTopicAuth(UserTopic):
816 # topic = "users"
817 topic_msg = "users"
818 schema_new = user_new_schema
819 schema_edit = user_edit_schema
820
821 def __init__(self, db, fs, msg, auth):
822 UserTopic.__init__(self, db, fs, msg, auth)
823 # self.auth = auth
824
825 def check_conflict_on_new(self, session, indata):
826 """
827 Check that the data to be inserted is valid
828
829 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
830 :param indata: data to be inserted
831 :return: None or raises EngineException
832 """
833 username = indata.get("username")
834 if is_valid_uuid(username):
835 raise EngineException(
836 "username '{}' cannot have a uuid format".format(username),
837 HTTPStatus.UNPROCESSABLE_ENTITY,
838 )
839
840 # Check that username is not used, regardless keystone already checks this
841 if self.auth.get_user_list(filter_q={"name": username}):
842 raise EngineException(
843 "username '{}' is already used".format(username), HTTPStatus.CONFLICT
844 )
845
846 if "projects" in indata.keys():
847 # convert to new format project_role_mappings
848 role = self.auth.get_role_list({"name": "project_admin"})
849 if not role:
850 role = self.auth.get_role_list()
851 if not role:
852 raise AuthconnNotFoundException(
853 "Can't find default role for user '{}'".format(username)
854 )
855 rid = role[0]["_id"]
856 if not indata.get("project_role_mappings"):
857 indata["project_role_mappings"] = []
858 for project in indata["projects"]:
859 pid = self.auth.get_project(project)["_id"]
860 prm = {"project": pid, "role": rid}
861 if prm not in indata["project_role_mappings"]:
862 indata["project_role_mappings"].append(prm)
863 # raise EngineException("Format invalid: the keyword 'projects' is not allowed for keystone authentication",
864 # HTTPStatus.BAD_REQUEST)
865
866 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
867 """
868 Check that the data to be edited/uploaded is valid
869
870 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
871 :param final_content: data once modified
872 :param edit_content: incremental data that contains the modifications to apply
873 :param _id: internal _id
874 :return: None or raises EngineException
875 """
876
877 if "username" in edit_content:
878 username = edit_content.get("username")
879 if is_valid_uuid(username):
880 raise EngineException(
881 "username '{}' cannot have an uuid format".format(username),
882 HTTPStatus.UNPROCESSABLE_ENTITY,
883 )
884
885 # Check that username is not used, regardless keystone already checks this
886 if self.auth.get_user_list(filter_q={"name": username}):
887 raise EngineException(
888 "username '{}' is already used".format(username),
889 HTTPStatus.CONFLICT,
890 )
891
892 if final_content["username"] == "admin":
893 for mapping in edit_content.get("remove_project_role_mappings", ()):
894 if mapping["project"] == "admin" and mapping.get("role") in (
895 None,
896 "system_admin",
897 ):
898 # TODO make this also available for project id and role id
899 raise EngineException(
900 "You cannot remove system_admin role from admin user",
901 http_code=HTTPStatus.FORBIDDEN,
902 )
903
904 return final_content
905
906 def check_conflict_on_del(self, session, _id, db_content):
907 """
908 Check if deletion can be done because of dependencies if it is not force. To override
909 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
910 :param _id: internal _id
911 :param db_content: The database content of this item _id
912 :return: None if ok or raises EngineException with the conflict
913 """
914 if db_content["username"] == session["username"]:
915 raise EngineException(
916 "You cannot delete your own login user ", http_code=HTTPStatus.CONFLICT
917 )
918 # TODO: Check that user is not logged in ? How? (Would require listing current tokens)
919
920 @staticmethod
921 def format_on_show(content):
922 """
923 Modifies the content of the role information to separate the role
924 metadata from the role definition.
925 """
926 project_role_mappings = []
927
928 if "projects" in content:
929 for project in content["projects"]:
930 for role in project["roles"]:
931 project_role_mappings.append(
932 {
933 "project": project["_id"],
934 "project_name": project["name"],
935 "role": role["_id"],
936 "role_name": role["name"],
937 }
938 )
939 del content["projects"]
940 content["project_role_mappings"] = project_role_mappings
941
942 return content
943
944 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
945 """
946 Creates a new entry into the authentication backend.
947
948 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
949
950 :param rollback: list to append created items at database in case a rollback may to be done
951 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
952 :param indata: data to be inserted
953 :param kwargs: used to override the indata descriptor
954 :param headers: http request headers
955 :return: _id: identity of the inserted data, operation _id (None)
956 """
957 try:
958 content = BaseTopic._remove_envelop(indata)
959
960 # Override descriptor with query string kwargs
961 BaseTopic._update_input_with_kwargs(content, kwargs)
962 content = self._validate_input_new(content, session["force"])
963 self.check_conflict_on_new(session, content)
964 # self.format_on_new(content, session["project_id"], make_public=session["public"])
965 now = time()
966 content["_admin"] = {"created": now, "modified": now}
967 prms = []
968 for prm in content.get("project_role_mappings", []):
969 proj = self.auth.get_project(prm["project"], not session["force"])
970 role = self.auth.get_role(prm["role"], not session["force"])
971 pid = proj["_id"] if proj else None
972 rid = role["_id"] if role else None
973 prl = {"project": pid, "role": rid}
974 if prl not in prms:
975 prms.append(prl)
976 content["project_role_mappings"] = prms
977 # _id = self.auth.create_user(content["username"], content["password"])["_id"]
978 _id = self.auth.create_user(content)["_id"]
979
980 rollback.append({"topic": self.topic, "_id": _id})
981 # del content["password"]
982 self._send_msg("created", content, not_send_msg=None)
983 return _id, None
984 except ValidationError as e:
985 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
986
987 def show(self, session, _id, filter_q=None, api_req=False):
988 """
989 Get complete information on an topic
990
991 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
992 :param _id: server internal id or username
993 :param filter_q: dict: query parameter
994 :param api_req: True if this call is serving an external API request. False if serving internal request.
995 :return: dictionary, raise exception if not found.
996 """
997 # Allow _id to be a name or uuid
998 filter_q = {"username": _id}
999 # users = self.auth.get_user_list(filter_q)
1000 users = self.list(session, filter_q) # To allow default filtering (Bug 853)
1001 if len(users) == 1:
1002 return users[0]
1003 elif len(users) > 1:
1004 raise EngineException(
1005 "Too many users found for '{}'".format(_id), HTTPStatus.CONFLICT
1006 )
1007 else:
1008 raise EngineException(
1009 "User '{}' not found".format(_id), HTTPStatus.NOT_FOUND
1010 )
1011
1012 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1013 """
1014 Updates an user entry.
1015
1016 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1017 :param _id:
1018 :param indata: data to be inserted
1019 :param kwargs: used to override the indata descriptor
1020 :param content:
1021 :return: _id: identity of the inserted data.
1022 """
1023 indata = self._remove_envelop(indata)
1024
1025 # Override descriptor with query string kwargs
1026 if kwargs:
1027 BaseTopic._update_input_with_kwargs(indata, kwargs)
1028 try:
1029 if not content:
1030 content = self.show(session, _id)
1031 indata = self._validate_input_edit(indata, content, force=session["force"])
1032 content = self.check_conflict_on_edit(session, content, indata, _id=_id)
1033 # self.format_on_edit(content, indata)
1034
1035 if not (
1036 "password" in indata
1037 or "username" in indata
1038 or indata.get("remove_project_role_mappings")
1039 or indata.get("add_project_role_mappings")
1040 or indata.get("project_role_mappings")
1041 or indata.get("projects")
1042 or indata.get("add_projects")
1043 ):
1044 return _id
1045 if indata.get("project_role_mappings") and (
1046 indata.get("remove_project_role_mappings")
1047 or indata.get("add_project_role_mappings")
1048 ):
1049 raise EngineException(
1050 "Option 'project_role_mappings' is incompatible with 'add_project_role_mappings"
1051 "' or 'remove_project_role_mappings'",
1052 http_code=HTTPStatus.BAD_REQUEST,
1053 )
1054
1055 if indata.get("projects") or indata.get("add_projects"):
1056 role = self.auth.get_role_list({"name": "project_admin"})
1057 if not role:
1058 role = self.auth.get_role_list()
1059 if not role:
1060 raise AuthconnNotFoundException(
1061 "Can't find a default role for user '{}'".format(
1062 content["username"]
1063 )
1064 )
1065 rid = role[0]["_id"]
1066 if "add_project_role_mappings" not in indata:
1067 indata["add_project_role_mappings"] = []
1068 if "remove_project_role_mappings" not in indata:
1069 indata["remove_project_role_mappings"] = []
1070 if isinstance(indata.get("projects"), dict):
1071 # backward compatible
1072 for k, v in indata["projects"].items():
1073 if k.startswith("$") and v is None:
1074 indata["remove_project_role_mappings"].append(
1075 {"project": k[1:]}
1076 )
1077 elif k.startswith("$+"):
1078 indata["add_project_role_mappings"].append(
1079 {"project": v, "role": rid}
1080 )
1081 del indata["projects"]
1082 for proj in indata.get("projects", []) + indata.get("add_projects", []):
1083 indata["add_project_role_mappings"].append(
1084 {"project": proj, "role": rid}
1085 )
1086
1087 # user = self.show(session, _id) # Already in 'content'
1088 original_mapping = content["project_role_mappings"]
1089
1090 mappings_to_add = []
1091 mappings_to_remove = []
1092
1093 # remove
1094 for to_remove in indata.get("remove_project_role_mappings", ()):
1095 for mapping in original_mapping:
1096 if to_remove["project"] in (
1097 mapping["project"],
1098 mapping["project_name"],
1099 ):
1100 if not to_remove.get("role") or to_remove["role"] in (
1101 mapping["role"],
1102 mapping["role_name"],
1103 ):
1104 mappings_to_remove.append(mapping)
1105
1106 # add
1107 for to_add in indata.get("add_project_role_mappings", ()):
1108 for mapping in original_mapping:
1109 if to_add["project"] in (
1110 mapping["project"],
1111 mapping["project_name"],
1112 ) and to_add["role"] in (mapping["role"], mapping["role_name"]):
1113
1114 if mapping in mappings_to_remove: # do not remove
1115 mappings_to_remove.remove(mapping)
1116 break # do not add, it is already at user
1117 else:
1118 pid = self.auth.get_project(to_add["project"])["_id"]
1119 rid = self.auth.get_role(to_add["role"])["_id"]
1120 mappings_to_add.append({"project": pid, "role": rid})
1121
1122 # set
1123 if indata.get("project_role_mappings"):
1124 for to_set in indata["project_role_mappings"]:
1125 for mapping in original_mapping:
1126 if to_set["project"] in (
1127 mapping["project"],
1128 mapping["project_name"],
1129 ) and to_set["role"] in (mapping["role"], mapping["role_name"]):
1130 if mapping in mappings_to_remove: # do not remove
1131 mappings_to_remove.remove(mapping)
1132 break # do not add, it is already at user
1133 else:
1134 pid = self.auth.get_project(to_set["project"])["_id"]
1135 rid = self.auth.get_role(to_set["role"])["_id"]
1136 mappings_to_add.append({"project": pid, "role": rid})
1137 for mapping in original_mapping:
1138 for to_set in indata["project_role_mappings"]:
1139 if to_set["project"] in (
1140 mapping["project"],
1141 mapping["project_name"],
1142 ) and to_set["role"] in (mapping["role"], mapping["role_name"]):
1143 break
1144 else:
1145 # delete
1146 if mapping not in mappings_to_remove: # do not remove
1147 mappings_to_remove.append(mapping)
1148
1149 self.auth.update_user(
1150 {
1151 "_id": _id,
1152 "username": indata.get("username"),
1153 "password": indata.get("password"),
1154 "old_password": indata.get("old_password"),
1155 "add_project_role_mappings": mappings_to_add,
1156 "remove_project_role_mappings": mappings_to_remove,
1157 }
1158 )
1159 data_to_send = {"_id": _id, "changes": indata}
1160 self._send_msg("edited", data_to_send, not_send_msg=None)
1161
1162 # return _id
1163 except ValidationError as e:
1164 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1165
1166 def list(self, session, filter_q=None, api_req=False):
1167 """
1168 Get a list of the topic that matches a filter
1169 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1170 :param filter_q: filter of data to be applied
1171 :param api_req: True if this call is serving an external API request. False if serving internal request.
1172 :return: The list, it can be empty if no one match the filter.
1173 """
1174 user_list = self.auth.get_user_list(filter_q)
1175 if not session["allow_show_user_project_role"]:
1176 # Bug 853 - Default filtering
1177 user_list = [
1178 usr for usr in user_list if usr["username"] == session["username"]
1179 ]
1180 return user_list
1181
1182 def delete(self, session, _id, dry_run=False, not_send_msg=None):
1183 """
1184 Delete item by its internal _id
1185
1186 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1187 :param _id: server internal id
1188 :param force: indicates if deletion must be forced in case of conflict
1189 :param dry_run: make checking but do not delete
1190 :param not_send_msg: To not send message (False) or store content (list) instead
1191 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1192 """
1193 # Allow _id to be a name or uuid
1194 user = self.auth.get_user(_id)
1195 uid = user["_id"]
1196 self.check_conflict_on_del(session, uid, user)
1197 if not dry_run:
1198 v = self.auth.delete_user(uid)
1199 self._send_msg("deleted", user, not_send_msg=not_send_msg)
1200 return v
1201 return None
1202
1203
1204 class ProjectTopicAuth(ProjectTopic):
1205 # topic = "projects"
1206 topic_msg = "project"
1207 schema_new = project_new_schema
1208 schema_edit = project_edit_schema
1209
1210 def __init__(self, db, fs, msg, auth):
1211 ProjectTopic.__init__(self, db, fs, msg, auth)
1212 # self.auth = auth
1213
1214 def check_conflict_on_new(self, session, indata):
1215 """
1216 Check that the data to be inserted is valid
1217
1218 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1219 :param indata: data to be inserted
1220 :return: None or raises EngineException
1221 """
1222 project_name = indata.get("name")
1223 if is_valid_uuid(project_name):
1224 raise EngineException(
1225 "project name '{}' cannot have an uuid format".format(project_name),
1226 HTTPStatus.UNPROCESSABLE_ENTITY,
1227 )
1228
1229 project_list = self.auth.get_project_list(filter_q={"name": project_name})
1230
1231 if project_list:
1232 raise EngineException(
1233 "project '{}' exists".format(project_name), HTTPStatus.CONFLICT
1234 )
1235
1236 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
1237 """
1238 Check that the data to be edited/uploaded is valid
1239
1240 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1241 :param final_content: data once modified
1242 :param edit_content: incremental data that contains the modifications to apply
1243 :param _id: internal _id
1244 :return: None or raises EngineException
1245 """
1246
1247 project_name = edit_content.get("name")
1248 if project_name != final_content["name"]: # It is a true renaming
1249 if is_valid_uuid(project_name):
1250 raise EngineException(
1251 "project name '{}' cannot have an uuid format".format(project_name),
1252 HTTPStatus.UNPROCESSABLE_ENTITY,
1253 )
1254
1255 if final_content["name"] == "admin":
1256 raise EngineException(
1257 "You cannot rename project 'admin'", http_code=HTTPStatus.CONFLICT
1258 )
1259
1260 # Check that project name is not used, regardless keystone already checks this
1261 if project_name and self.auth.get_project_list(
1262 filter_q={"name": project_name}
1263 ):
1264 raise EngineException(
1265 "project '{}' is already used".format(project_name),
1266 HTTPStatus.CONFLICT,
1267 )
1268 return final_content
1269
1270 def check_conflict_on_del(self, session, _id, db_content):
1271 """
1272 Check if deletion can be done because of dependencies if it is not force. To override
1273
1274 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1275 :param _id: internal _id
1276 :param db_content: The database content of this item _id
1277 :return: None if ok or raises EngineException with the conflict
1278 """
1279
1280 def check_rw_projects(topic, title, id_field):
1281 for desc in self.db.get_list(topic):
1282 if (
1283 _id
1284 in desc["_admin"]["projects_read"]
1285 + desc["_admin"]["projects_write"]
1286 ):
1287 raise EngineException(
1288 "Project '{}' ({}) is being used by {} '{}'".format(
1289 db_content["name"], _id, title, desc[id_field]
1290 ),
1291 HTTPStatus.CONFLICT,
1292 )
1293
1294 if _id in session["project_id"]:
1295 raise EngineException(
1296 "You cannot delete your own project", http_code=HTTPStatus.CONFLICT
1297 )
1298
1299 if db_content["name"] == "admin":
1300 raise EngineException(
1301 "You cannot delete project 'admin'", http_code=HTTPStatus.CONFLICT
1302 )
1303
1304 # If any user is using this project, raise CONFLICT exception
1305 if not session["force"]:
1306 for user in self.auth.get_user_list():
1307 for prm in user.get("project_role_mappings"):
1308 if prm["project"] == _id:
1309 raise EngineException(
1310 "Project '{}' ({}) is being used by user '{}'".format(
1311 db_content["name"], _id, user["username"]
1312 ),
1313 HTTPStatus.CONFLICT,
1314 )
1315
1316 # If any VNFD, NSD, NST, PDU, etc. is using this project, raise CONFLICT exception
1317 if not session["force"]:
1318 check_rw_projects("vnfds", "VNF Descriptor", "id")
1319 check_rw_projects("nsds", "NS Descriptor", "id")
1320 check_rw_projects("nsts", "NS Template", "id")
1321 check_rw_projects("pdus", "PDU Descriptor", "name")
1322
1323 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1324 """
1325 Creates a new entry into the authentication backend.
1326
1327 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
1328
1329 :param rollback: list to append created items at database in case a rollback may to be done
1330 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1331 :param indata: data to be inserted
1332 :param kwargs: used to override the indata descriptor
1333 :param headers: http request headers
1334 :return: _id: identity of the inserted data, operation _id (None)
1335 """
1336 try:
1337 content = BaseTopic._remove_envelop(indata)
1338
1339 # Override descriptor with query string kwargs
1340 BaseTopic._update_input_with_kwargs(content, kwargs)
1341 content = self._validate_input_new(content, session["force"])
1342 self.check_conflict_on_new(session, content)
1343 self.format_on_new(
1344 content, project_id=session["project_id"], make_public=session["public"]
1345 )
1346 _id = self.auth.create_project(content)
1347 rollback.append({"topic": self.topic, "_id": _id})
1348 self._send_msg("created", content, not_send_msg=None)
1349 return _id, None
1350 except ValidationError as e:
1351 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1352
1353 def show(self, session, _id, filter_q=None, api_req=False):
1354 """
1355 Get complete information on an topic
1356
1357 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1358 :param _id: server internal id
1359 :param filter_q: dict: query parameter
1360 :param api_req: True if this call is serving an external API request. False if serving internal request.
1361 :return: dictionary, raise exception if not found.
1362 """
1363 # Allow _id to be a name or uuid
1364 filter_q = {self.id_field(self.topic, _id): _id}
1365 # projects = self.auth.get_project_list(filter_q=filter_q)
1366 projects = self.list(session, filter_q) # To allow default filtering (Bug 853)
1367 if len(projects) == 1:
1368 return projects[0]
1369 elif len(projects) > 1:
1370 raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
1371 else:
1372 raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
1373
1374 def list(self, session, filter_q=None, api_req=False):
1375 """
1376 Get a list of the topic that matches a filter
1377
1378 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1379 :param filter_q: filter of data to be applied
1380 :return: The list, it can be empty if no one match the filter.
1381 """
1382 project_list = self.auth.get_project_list(filter_q)
1383 if not session["allow_show_user_project_role"]:
1384 # Bug 853 - Default filtering
1385 user = self.auth.get_user(session["username"])
1386 projects = [prm["project"] for prm in user["project_role_mappings"]]
1387 project_list = [proj for proj in project_list if proj["_id"] in projects]
1388 return project_list
1389
1390 def delete(self, session, _id, dry_run=False, not_send_msg=None):
1391 """
1392 Delete item by its internal _id
1393
1394 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1395 :param _id: server internal id
1396 :param dry_run: make checking but do not delete
1397 :param not_send_msg: To not send message (False) or store content (list) instead
1398 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1399 """
1400 # Allow _id to be a name or uuid
1401 proj = self.auth.get_project(_id)
1402 pid = proj["_id"]
1403 self.check_conflict_on_del(session, pid, proj)
1404 if not dry_run:
1405 v = self.auth.delete_project(pid)
1406 self._send_msg("deleted", proj, not_send_msg=None)
1407 return v
1408 return None
1409
1410 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1411 """
1412 Updates a project entry.
1413
1414 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1415 :param _id:
1416 :param indata: data to be inserted
1417 :param kwargs: used to override the indata descriptor
1418 :param content:
1419 :return: _id: identity of the inserted data.
1420 """
1421 indata = self._remove_envelop(indata)
1422
1423 # Override descriptor with query string kwargs
1424 if kwargs:
1425 BaseTopic._update_input_with_kwargs(indata, kwargs)
1426 try:
1427 if not content:
1428 content = self.show(session, _id)
1429 indata = self._validate_input_edit(indata, content, force=session["force"])
1430 content = self.check_conflict_on_edit(session, content, indata, _id=_id)
1431 self.format_on_edit(content, indata)
1432 content_original = copy.deepcopy(content)
1433 deep_update_rfc7396(content, indata)
1434 self.auth.update_project(content["_id"], content)
1435 proj_data = {"_id": _id, "changes": indata, "original": content_original}
1436 self._send_msg("edited", proj_data, not_send_msg=None)
1437 except ValidationError as e:
1438 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1439
1440
1441 class RoleTopicAuth(BaseTopic):
1442 topic = "roles"
1443 topic_msg = None # "roles"
1444 schema_new = roles_new_schema
1445 schema_edit = roles_edit_schema
1446 multiproject = False
1447
1448 def __init__(self, db, fs, msg, auth):
1449 BaseTopic.__init__(self, db, fs, msg, auth)
1450 # self.auth = auth
1451 self.operations = auth.role_permissions
1452 # self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles"
1453
1454 @staticmethod
1455 def validate_role_definition(operations, role_definitions):
1456 """
1457 Validates the role definition against the operations defined in
1458 the resources to operations files.
1459
1460 :param operations: operations list
1461 :param role_definitions: role definition to test
1462 :return: None if ok, raises ValidationError exception on error
1463 """
1464 if not role_definitions.get("permissions"):
1465 return
1466 ignore_fields = ["admin", "default"]
1467 for role_def in role_definitions["permissions"].keys():
1468 if role_def in ignore_fields:
1469 continue
1470 if role_def[-1] == ":":
1471 raise ValidationError("Operation cannot end with ':'")
1472
1473 match = next(
1474 (
1475 op
1476 for op in operations
1477 if op == role_def or op.startswith(role_def + ":")
1478 ),
1479 None,
1480 )
1481
1482 if not match:
1483 raise ValidationError("Invalid permission '{}'".format(role_def))
1484
1485 def _validate_input_new(self, input, force=False):
1486 """
1487 Validates input user content for a new entry.
1488
1489 :param input: user input content for the new topic
1490 :param force: may be used for being more tolerant
1491 :return: The same input content, or a changed version of it.
1492 """
1493 if self.schema_new:
1494 validate_input(input, self.schema_new)
1495 self.validate_role_definition(self.operations, input)
1496
1497 return input
1498
1499 def _validate_input_edit(self, input, content, force=False):
1500 """
1501 Validates input user content for updating an entry.
1502
1503 :param input: user input content for the new topic
1504 :param force: may be used for being more tolerant
1505 :return: The same input content, or a changed version of it.
1506 """
1507 if self.schema_edit:
1508 validate_input(input, self.schema_edit)
1509 self.validate_role_definition(self.operations, input)
1510
1511 return input
1512
1513 def check_conflict_on_new(self, session, indata):
1514 """
1515 Check that the data to be inserted is valid
1516
1517 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1518 :param indata: data to be inserted
1519 :return: None or raises EngineException
1520 """
1521 # check name is not uuid
1522 role_name = indata.get("name")
1523 if is_valid_uuid(role_name):
1524 raise EngineException(
1525 "role name '{}' cannot have an uuid format".format(role_name),
1526 HTTPStatus.UNPROCESSABLE_ENTITY,
1527 )
1528 # check name not exists
1529 name = indata["name"]
1530 # if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
1531 if self.auth.get_role_list({"name": name}):
1532 raise EngineException(
1533 "role name '{}' exists".format(name), HTTPStatus.CONFLICT
1534 )
1535
1536 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
1537 """
1538 Check that the data to be edited/uploaded is valid
1539
1540 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1541 :param final_content: data once modified
1542 :param edit_content: incremental data that contains the modifications to apply
1543 :param _id: internal _id
1544 :return: None or raises EngineException
1545 """
1546 if "default" not in final_content["permissions"]:
1547 final_content["permissions"]["default"] = False
1548 if "admin" not in final_content["permissions"]:
1549 final_content["permissions"]["admin"] = False
1550
1551 # check name is not uuid
1552 role_name = edit_content.get("name")
1553 if is_valid_uuid(role_name):
1554 raise EngineException(
1555 "role name '{}' cannot have an uuid format".format(role_name),
1556 HTTPStatus.UNPROCESSABLE_ENTITY,
1557 )
1558
1559 # Check renaming of admin roles
1560 role = self.auth.get_role(_id)
1561 if role["name"] in ["system_admin", "project_admin"]:
1562 raise EngineException(
1563 "You cannot rename role '{}'".format(role["name"]),
1564 http_code=HTTPStatus.FORBIDDEN,
1565 )
1566
1567 # check name not exists
1568 if "name" in edit_content:
1569 role_name = edit_content["name"]
1570 # if self.db.get_one(self.topic, {"name":role_name,"_id.ne":_id}, fail_on_empty=False, fail_on_more=False):
1571 roles = self.auth.get_role_list({"name": role_name})
1572 if roles and roles[0][BaseTopic.id_field("roles", _id)] != _id:
1573 raise EngineException(
1574 "role name '{}' exists".format(role_name), HTTPStatus.CONFLICT
1575 )
1576
1577 return final_content
1578
1579 def check_conflict_on_del(self, session, _id, db_content):
1580 """
1581 Check if deletion can be done because of dependencies if it is not force. To override
1582
1583 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1584 :param _id: internal _id
1585 :param db_content: The database content of this item _id
1586 :return: None if ok or raises EngineException with the conflict
1587 """
1588 role = self.auth.get_role(_id)
1589 if role["name"] in ["system_admin", "project_admin"]:
1590 raise EngineException(
1591 "You cannot delete role '{}'".format(role["name"]),
1592 http_code=HTTPStatus.FORBIDDEN,
1593 )
1594
1595 # If any user is using this role, raise CONFLICT exception
1596 if not session["force"]:
1597 for user in self.auth.get_user_list():
1598 for prm in user.get("project_role_mappings"):
1599 if prm["role"] == _id:
1600 raise EngineException(
1601 "Role '{}' ({}) is being used by user '{}'".format(
1602 role["name"], _id, user["username"]
1603 ),
1604 HTTPStatus.CONFLICT,
1605 )
1606
1607 @staticmethod
1608 def format_on_new(content, project_id=None, make_public=False): # TO BE REMOVED ?
1609 """
1610 Modifies content descriptor to include _admin
1611
1612 :param content: descriptor to be modified
1613 :param project_id: if included, it add project read/write permissions
1614 :param make_public: if included it is generated as public for reading.
1615 :return: None, but content is modified
1616 """
1617 now = time()
1618 if "_admin" not in content:
1619 content["_admin"] = {}
1620 if not content["_admin"].get("created"):
1621 content["_admin"]["created"] = now
1622 content["_admin"]["modified"] = now
1623
1624 if "permissions" not in content:
1625 content["permissions"] = {}
1626
1627 if "default" not in content["permissions"]:
1628 content["permissions"]["default"] = False
1629 if "admin" not in content["permissions"]:
1630 content["permissions"]["admin"] = False
1631
1632 @staticmethod
1633 def format_on_edit(final_content, edit_content):
1634 """
1635 Modifies final_content descriptor to include the modified date.
1636
1637 :param final_content: final descriptor generated
1638 :param edit_content: alterations to be include
1639 :return: None, but final_content is modified
1640 """
1641 if "_admin" in final_content:
1642 final_content["_admin"]["modified"] = time()
1643
1644 if "permissions" not in final_content:
1645 final_content["permissions"] = {}
1646
1647 if "default" not in final_content["permissions"]:
1648 final_content["permissions"]["default"] = False
1649 if "admin" not in final_content["permissions"]:
1650 final_content["permissions"]["admin"] = False
1651 return None
1652
1653 def show(self, session, _id, filter_q=None, api_req=False):
1654 """
1655 Get complete information on an topic
1656
1657 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1658 :param _id: server internal id
1659 :param filter_q: dict: query parameter
1660 :param api_req: True if this call is serving an external API request. False if serving internal request.
1661 :return: dictionary, raise exception if not found.
1662 """
1663 filter_q = {BaseTopic.id_field(self.topic, _id): _id}
1664 # roles = self.auth.get_role_list(filter_q)
1665 roles = self.list(session, filter_q) # To allow default filtering (Bug 853)
1666 if not roles:
1667 raise AuthconnNotFoundException(
1668 "Not found any role with filter {}".format(filter_q)
1669 )
1670 elif len(roles) > 1:
1671 raise AuthconnConflictException(
1672 "Found more than one role with filter {}".format(filter_q)
1673 )
1674 return roles[0]
1675
1676 def list(self, session, filter_q=None, api_req=False):
1677 """
1678 Get a list of the topic that matches a filter
1679
1680 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1681 :param filter_q: filter of data to be applied
1682 :return: The list, it can be empty if no one match the filter.
1683 """
1684 role_list = self.auth.get_role_list(filter_q)
1685 if not session["allow_show_user_project_role"]:
1686 # Bug 853 - Default filtering
1687 user = self.auth.get_user(session["username"])
1688 roles = [prm["role"] for prm in user["project_role_mappings"]]
1689 role_list = [role for role in role_list if role["_id"] in roles]
1690 return role_list
1691
1692 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1693 """
1694 Creates a new entry into database.
1695
1696 :param rollback: list to append created items at database in case a rollback may to be done
1697 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1698 :param indata: data to be inserted
1699 :param kwargs: used to override the indata descriptor
1700 :param headers: http request headers
1701 :return: _id: identity of the inserted data, operation _id (None)
1702 """
1703 try:
1704 content = self._remove_envelop(indata)
1705
1706 # Override descriptor with query string kwargs
1707 self._update_input_with_kwargs(content, kwargs)
1708 content = self._validate_input_new(content, session["force"])
1709 self.check_conflict_on_new(session, content)
1710 self.format_on_new(
1711 content, project_id=session["project_id"], make_public=session["public"]
1712 )
1713 # role_name = content["name"]
1714 rid = self.auth.create_role(content)
1715 content["_id"] = rid
1716 # _id = self.db.create(self.topic, content)
1717 rollback.append({"topic": self.topic, "_id": rid})
1718 # self._send_msg("created", content, not_send_msg=not_send_msg)
1719 return rid, None
1720 except ValidationError as e:
1721 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1722
1723 def delete(self, session, _id, dry_run=False, not_send_msg=None):
1724 """
1725 Delete item by its internal _id
1726
1727 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1728 :param _id: server internal id
1729 :param dry_run: make checking but do not delete
1730 :param not_send_msg: To not send message (False) or store content (list) instead
1731 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1732 """
1733 filter_q = {BaseTopic.id_field(self.topic, _id): _id}
1734 roles = self.auth.get_role_list(filter_q)
1735 if not roles:
1736 raise AuthconnNotFoundException(
1737 "Not found any role with filter {}".format(filter_q)
1738 )
1739 elif len(roles) > 1:
1740 raise AuthconnConflictException(
1741 "Found more than one role with filter {}".format(filter_q)
1742 )
1743 rid = roles[0]["_id"]
1744 self.check_conflict_on_del(session, rid, None)
1745 # filter_q = {"_id": _id}
1746 # filter_q = {BaseTopic.id_field(self.topic, _id): _id} # To allow role addressing by name
1747 if not dry_run:
1748 v = self.auth.delete_role(rid)
1749 # v = self.db.del_one(self.topic, filter_q)
1750 return v
1751 return None
1752
1753 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1754 """
1755 Updates a role entry.
1756
1757 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1758 :param _id:
1759 :param indata: data to be inserted
1760 :param kwargs: used to override the indata descriptor
1761 :param content:
1762 :return: _id: identity of the inserted data.
1763 """
1764 if kwargs:
1765 self._update_input_with_kwargs(indata, kwargs)
1766 try:
1767 if not content:
1768 content = self.show(session, _id)
1769 indata = self._validate_input_edit(indata, content, force=session["force"])
1770 deep_update_rfc7396(content, indata)
1771 content = self.check_conflict_on_edit(session, content, indata, _id=_id)
1772 self.format_on_edit(content, indata)
1773 self.auth.update_role(content)
1774 except ValidationError as e:
1775 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)