Code Coverage

Cobertura Coverage Report > osm_nbi >

admin_topics.py

Trend

Classes100%
 
Lines72%
   
Conditionals100%
 

File Coverage summary

NameClassesLinesConditionals
admin_topics.py
100%
1/1
72%
566/784
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
admin_topics.py
72%
566/784
N/A

Source

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