1 # -*- coding: utf-8 -*-
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 from uuid
import uuid4
18 from hashlib
import sha256
19 from http
import HTTPStatus
21 from osm_nbi
.validation
import (
26 vim_account_new_schema
,
27 vim_account_edit_schema
,
30 wim_account_new_schema
,
31 wim_account_edit_schema
,
34 k8scluster_new_schema
,
35 k8scluster_edit_schema
,
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
53 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
56 class UserTopic(BaseTopic
):
59 schema_new
= user_new_schema
60 schema_edit
= user_edit_schema
63 def __init__(self
, db
, fs
, msg
, auth
):
64 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
67 def _get_project_filter(session
):
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"
74 if session
["admin"]: # allows all
77 return {"username": session
["username"]}
79 def check_conflict_on_new(self
, session
, indata
):
80 # check username not exists
83 {"username": indata
.get("username")},
87 raise EngineException(
88 "username '{}' exists".format(indata
["username"]), HTTPStatus
.CONFLICT
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(
96 {BaseTopic
.id_field("projects", p
): p
},
100 raise EngineException(
101 "project '{}' does not exist".format(p
), HTTPStatus
.CONFLICT
104 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
112 if _id
== session
["username"]:
113 raise EngineException(
114 "You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
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"]
123 content
["_admin"]["salt"] = salt
124 if content
.get("password"):
125 content
["password"] = sha256(
126 content
["password"].encode("utf-8") + salt
.encode("utf-8")
128 if content
.get("project_role_mappings"):
130 mapping
["project"] for mapping
in content
["project_role_mappings"]
133 if content
.get("projects"):
134 content
["projects"] += projects
136 content
["projects"] = projects
139 def format_on_edit(final_content
, edit_content
):
140 BaseTopic
.format_on_edit(final_content
, edit_content
)
141 if edit_content
.get("password"):
143 final_content
["_admin"]["salt"] = salt
144 final_content
["password"] = sha256(
145 edit_content
["password"].encode("utf-8") + salt
.encode("utf-8")
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
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
,
161 return BaseTopic
.edit(
162 self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, content
=content
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
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
,
177 return BaseTopic
.new(
178 self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
182 class ProjectTopic(BaseTopic
):
184 topic_msg
= "projects"
185 schema_new
= project_new_schema
186 schema_edit
= project_edit_schema
189 def __init__(self
, db
, fs
, msg
, auth
):
190 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
193 def _get_project_filter(session
):
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"
200 if session
["admin"]: # allows all
203 return {"_id.cont": session
["project_id"]}
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
211 {"name": indata
.get("name")},
215 raise EngineException(
216 "name '{}' exists".format(indata
["name"]), HTTPStatus
.CONFLICT
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"]
225 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
233 if _id
in session
["project_id"]:
234 raise EngineException(
235 "You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
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
,
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
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
,
258 return BaseTopic
.edit(
259 self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, content
=content
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
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
,
274 return BaseTopic
.new(
275 self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
279 class CommonVimWimSdn(BaseTopic
):
280 """Common class for VIM, WIM SDN just to unify methods that are equal to all of them"""
282 config_to_encrypt
= (
284 ) # what keys at config must be encrypted because contains passwords
285 password_to_encrypt
= "" # key that contains a password
288 def _create_operation(op_type
, params
=None):
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
297 "lcmOperationType": op_type
,
298 "operationState": "PROCESSING",
300 "statusEnteredTime": now
,
301 "detailed-status": "",
302 "operationParams": params
,
305 def check_conflict_on_new(self
, session
, indata
):
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
312 self
.check_unique_name(session
, indata
["name"], _id
=None)
314 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
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
323 if not session
["force"] and edit_content
.get("name"):
324 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
328 def format_on_edit(self
, final_content
, edit_content
):
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
335 super().format_on_edit(final_content
, edit_content
)
338 schema_version
= final_content
.get("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"],
346 config_to_encrypt_keys
= self
.config_to_encrypt
.get(
348 ) or self
.config_to_encrypt
.get("default")
349 if edit_content
.get("config") and config_to_encrypt_keys
:
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"],
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
365 def format_on_new(self
, content
, project_id
=None, make_public
=False):
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
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"])
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
,
389 def _encrypt_config_fields(self
, content
, schema_version
):
390 config_to_encrypt_keys
= self
.config_to_encrypt
.get(
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
,
402 def _insert_create_operation(self
, content
):
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":
410 "ram": {"total": None, "used": None},
411 "vcpus": {"total": None, "used": None},
412 "instances": {"total": None, "used": None},
415 "volumes": {"total": None, "used": None},
416 "snapshots": {"total": None, "used": None},
417 "storage": {"total": None, "used": None},
420 "networks": {"total": None, "used": None},
421 "subnets": {"total": None, "used": None},
422 "floating_ips": {"total": None, "used": None},
424 content
["resources"] = {
430 def delete(self
, session
, _id
, dry_run
=False, not_send_msg
=None):
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
440 filter_q
= self
._get
_project
_filter
(session
)
441 filter_q
["_id"] = _id
442 db_content
= self
.db
.get_one(self
.topic
, filter_q
)
444 self
.check_conflict_on_del(session
, _id
, db_content
)
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(
454 for p
in db_content
["_admin"]["projects_read"]
455 if p
not in session
["project_id"] and p
!= "ANY"
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
464 "_admin.projects_read": session
["project_id"],
465 "_admin.projects_write": session
["project_id"],
468 self
.topic
, filter_q
, update_dict
=None, pull_list
=update_dict_pull
475 for p
in db_content
["_admin"]["projects_write"]
476 if p
== "ANY" or p
in session
["project_id"]
481 raise EngineException(
482 "You have not write permission to delete it",
483 http_code
=HTTPStatus
.UNAUTHORIZED
,
488 self
.db
.del_one(self
.topic
, {"_id": _id
})
491 "deleted", {"_id": _id
, "op_id": op_id
}, not_send_msg
=not_send_msg
494 update_dict
= {"_admin.to_delete": True}
498 update_dict
=update_dict
,
499 push
={"_admin.operations": self
._create
_operation
("delete")},
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"])
507 "delete", {"_id": _id
, "op_id": op_id
}, not_send_msg
=not_send_msg
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
518 password_to_encrypt
= "vim_password"
519 config_to_encrypt
= {
520 "1.1": ("admin_password", "nsx_password", "vcenter_password"),
529 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
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
,
545 super().check_conflict_on_del(session
, _id
, db_content
)
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
554 password_to_encrypt
= "password"
555 config_to_encrypt
= {}
558 class SdnTopic(CommonVimWimSdn
):
561 quota_name
= "sdn_controllers"
562 schema_new
= sdn_new_schema
563 schema_edit
= sdn_edit_schema
565 password_to_encrypt
= "password"
566 config_to_encrypt
= {}
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)"
574 input["url"] = "http://{}:{}/".format(input["ip"], input["port"])
577 elif create
and not input.get("url"):
578 raise ValidationError("You must provide 'url'")
581 def _validate_input_new(self
, input, force
=False):
582 input = super()._validate
_input
_new
(input, force
)
583 return self
._obtain
_url
(input, True)
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)
590 class K8sClusterTopic(CommonVimWimSdn
):
591 topic
= "k8sclusters"
592 topic_msg
= "k8scluster"
593 schema_new
= k8scluster_new_schema
594 schema_edit
= k8scluster_edit_schema
596 password_to_encrypt
= None
597 config_to_encrypt
= {}
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"],
604 ["password", "secret"],
605 schema_version
=content
["schema_version"],
608 # Add Helm/Juju Repo lists
609 repos
= {"helm-chart": [], "juju-bundle": []}
610 for proj
in content
["_admin"]["projects_read"]:
612 for repo
in self
.db
.get_list(
613 "k8srepos", {"_admin.projects_read": proj
}
615 if repo
["_id"] not in repos
[repo
["type"]]:
616 repos
[repo
["type"]].append(repo
["_id"])
618 content
["_admin"][k
.replace("-", "_") + "_repos"] = repos
[k
]
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"],
626 ["password", "secret"],
627 schema_version
=final_content
["schema_version"],
628 salt
=final_content
["_id"],
631 final_content
["credentials"], edit_content
["credentials"]
633 oid
= super().format_on_edit(final_content
, edit_content
)
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
640 final_content
= super().check_conflict_on_edit(
641 session
, final_content
, edit_content
, _id
643 # Update Helm/Juju Repo lists
644 repos
= {"helm-chart": [], "juju-bundle": []}
645 for proj
in session
.get("set_project", []):
647 for repo
in self
.db
.get_list(
648 "k8srepos", {"_admin.projects_read": proj
}
650 if repo
["_id"] not in repos
[repo
["type"]]:
651 repos
[repo
["type"]].append(repo
["_id"])
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
]
659 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
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
,
678 super().check_conflict_on_del(session
, _id
, db_content
)
681 class VcaTopic(CommonVimWimSdn
):
684 schema_new
= vca_new_schema
685 schema_edit
= vca_edit_schema
687 password_to_encrypt
= None
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"]
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(
705 schema_version
=schema_version
,
706 salt
=final_content
["_id"],
710 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
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
,
729 super().check_conflict_on_del(session
, _id
, db_content
)
732 class PaasTopic(CommonVimWimSdn
):
735 schema_new
= paas_new_schema
736 schema_edit
= paas_edit_schema
738 password_to_encrypt
= "secret"
739 config_to_encrypt
= {}
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"
747 def _check_if_used_by_ns(self
):
750 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
760 self
._check
_if
_used
_by
_ns
()
762 super().check_conflict_on_del(session
, _id
, db_content
)
765 class K8sRepoTopic(CommonVimWimSdn
):
767 topic_msg
= "k8srepo"
768 schema_new
= k8srepo_new_schema
769 schema_edit
= k8srepo_edit_schema
771 password_to_encrypt
= None
772 config_to_encrypt
= {}
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"]:
783 "_admin.projects_read": proj
,
784 "_admin." + repo_list
+ ".ne": content
["_id"],
787 push
={"_admin." + repo_list
: content
["_id"]},
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
)
795 # Remove from Helm/Juju Repo lists
796 repo_list
= type.replace("-", "_") + "_repos"
799 {"_admin." + repo_list
: _id
},
801 pull
={"_admin." + repo_list
: _id
},
806 class OsmRepoTopic(BaseTopic
):
808 topic_msg
= "osmrepos"
809 schema_new
= osmrepo_new_schema
810 schema_edit
= osmrepo_edit_schema
812 # TODO: Implement user/password
815 class UserTopicAuth(UserTopic
):
818 schema_new
= user_new_schema
819 schema_edit
= user_edit_schema
821 def __init__(self
, db
, fs
, msg
, auth
):
822 UserTopic
.__init
__(self
, db
, fs
, msg
, auth
)
825 def check_conflict_on_new(self
, session
, indata
):
827 Check that the data to be inserted is valid
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
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
,
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
846 if "projects" in indata
.keys():
847 # convert to new format project_role_mappings
848 role
= self
.auth
.get_role_list({"name": "project_admin"})
850 role
= self
.auth
.get_role_list()
852 raise AuthconnNotFoundException(
853 "Can't find default role for user '{}'".format(username
)
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)
866 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
868 Check that the data to be edited/uploaded is valid
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
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
,
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
),
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 (
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
,
906 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
914 if db_content
["username"] == session
["username"]:
915 raise EngineException(
916 "You cannot delete your own login user ", http_code
=HTTPStatus
.CONFLICT
918 # TODO: Check that user is not logged in ? How? (Would require listing current tokens)
921 def format_on_show(content
):
923 Modifies the content of the role information to separate the role
924 metadata from the role definition.
926 project_role_mappings
= []
928 if "projects" in content
:
929 for project
in content
["projects"]:
930 for role
in project
["roles"]:
931 project_role_mappings
.append(
933 "project": project
["_id"],
934 "project_name": project
["name"],
936 "role_name": role
["name"],
939 del content
["projects"]
940 content
["project_role_mappings"] = project_role_mappings
944 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
946 Creates a new entry into the authentication backend.
948 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
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)
958 content
= BaseTopic
._remove
_envelop
(indata
)
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"])
966 content
["_admin"] = {"created": now
, "modified": now
}
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
}
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"]
980 rollback
.append({"topic": self
.topic
, "_id": _id
})
981 # del content["password"]
982 self
._send
_msg
("created", content
, not_send_msg
=None)
984 except ValidationError
as e
:
985 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
987 def show(self
, session
, _id
, filter_q
=None, api_req
=False):
989 Get complete information on an topic
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.
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)
1003 elif len(users
) > 1:
1004 raise EngineException(
1005 "Too many users found for '{}'".format(_id
), HTTPStatus
.CONFLICT
1008 raise EngineException(
1009 "User '{}' not found".format(_id
), HTTPStatus
.NOT_FOUND
1012 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
1014 Updates an user entry.
1016 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1018 :param indata: data to be inserted
1019 :param kwargs: used to override the indata descriptor
1021 :return: _id: identity of the inserted data.
1023 indata
= self
._remove
_envelop
(indata
)
1025 # Override descriptor with query string kwargs
1027 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
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)
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")
1045 if indata
.get("project_role_mappings") and (
1046 indata
.get("remove_project_role_mappings")
1047 or indata
.get("add_project_role_mappings")
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
,
1055 if indata
.get("projects") or indata
.get("add_projects"):
1056 role
= self
.auth
.get_role_list({"name": "project_admin"})
1058 role
= self
.auth
.get_role_list()
1060 raise AuthconnNotFoundException(
1061 "Can't find a default role for user '{}'".format(
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(
1077 elif k
.startswith("$+"):
1078 indata
["add_project_role_mappings"].append(
1079 {"project": v
, "role": rid
}
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
}
1087 # user = self.show(session, _id) # Already in 'content'
1088 original_mapping
= content
["project_role_mappings"]
1090 mappings_to_add
= []
1091 mappings_to_remove
= []
1094 for to_remove
in indata
.get("remove_project_role_mappings", ()):
1095 for mapping
in original_mapping
:
1096 if to_remove
["project"] in (
1098 mapping
["project_name"],
1100 if not to_remove
.get("role") or to_remove
["role"] in (
1102 mapping
["role_name"],
1104 mappings_to_remove
.append(mapping
)
1107 for to_add
in indata
.get("add_project_role_mappings", ()):
1108 for mapping
in original_mapping
:
1109 if to_add
["project"] in (
1111 mapping
["project_name"],
1112 ) and to_add
["role"] in (mapping
["role"], mapping
["role_name"]):
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
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
})
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 (
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
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 (
1141 mapping
["project_name"],
1142 ) and to_set
["role"] in (mapping
["role"], mapping
["role_name"]):
1146 if mapping
not in mappings_to_remove
: # do not remove
1147 mappings_to_remove
.append(mapping
)
1149 self
.auth
.update_user(
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
,
1159 data_to_send
= {"_id": _id
, "changes": indata
}
1160 self
._send
_msg
("edited", data_to_send
, not_send_msg
=None)
1163 except ValidationError
as e
:
1164 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
1166 def list(self
, session
, filter_q
=None, api_req
=False):
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.
1174 user_list
= self
.auth
.get_user_list(filter_q
)
1175 if not session
["allow_show_user_project_role"]:
1176 # Bug 853 - Default filtering
1178 usr
for usr
in user_list
if usr
["username"] == session
["username"]
1182 def delete(self
, session
, _id
, dry_run
=False, not_send_msg
=None):
1184 Delete item by its internal _id
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, ...
1193 # Allow _id to be a name or uuid
1194 user
= self
.auth
.get_user(_id
)
1196 self
.check_conflict_on_del(session
, uid
, user
)
1198 v
= self
.auth
.delete_user(uid
)
1199 self
._send
_msg
("deleted", user
, not_send_msg
=not_send_msg
)
1204 class ProjectTopicAuth(ProjectTopic
):
1205 # topic = "projects"
1206 topic_msg
= "project"
1207 schema_new
= project_new_schema
1208 schema_edit
= project_edit_schema
1210 def __init__(self
, db
, fs
, msg
, auth
):
1211 ProjectTopic
.__init
__(self
, db
, fs
, msg
, auth
)
1214 def check_conflict_on_new(self
, session
, indata
):
1216 Check that the data to be inserted is valid
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
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
,
1229 project_list
= self
.auth
.get_project_list(filter_q
={"name": project_name
})
1232 raise EngineException(
1233 "project '{}' exists".format(project_name
), HTTPStatus
.CONFLICT
1236 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
1238 Check that the data to be edited/uploaded is valid
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
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
,
1255 if final_content
["name"] == "admin":
1256 raise EngineException(
1257 "You cannot rename project 'admin'", http_code
=HTTPStatus
.CONFLICT
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
}
1264 raise EngineException(
1265 "project '{}' is already used".format(project_name
),
1266 HTTPStatus
.CONFLICT
,
1268 return final_content
1270 def check_conflict_on_del(self
, session
, _id
, db_content
):
1272 Check if deletion can be done because of dependencies if it is not force. To override
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
1280 def check_rw_projects(topic
, title
, id_field
):
1281 for desc
in self
.db
.get_list(topic
):
1284 in desc
["_admin"]["projects_read"]
1285 + desc
["_admin"]["projects_write"]
1287 raise EngineException(
1288 "Project '{}' ({}) is being used by {} '{}'".format(
1289 db_content
["name"], _id
, title
, desc
[id_field
]
1291 HTTPStatus
.CONFLICT
,
1294 if _id
in session
["project_id"]:
1295 raise EngineException(
1296 "You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
1299 if db_content
["name"] == "admin":
1300 raise EngineException(
1301 "You cannot delete project 'admin'", http_code
=HTTPStatus
.CONFLICT
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"]
1313 HTTPStatus
.CONFLICT
,
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")
1323 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
1325 Creates a new entry into the authentication backend.
1327 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
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)
1337 content
= BaseTopic
._remove
_envelop
(indata
)
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
)
1344 content
, project_id
=session
["project_id"], make_public
=session
["public"]
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)
1350 except ValidationError
as e
:
1351 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
1353 def show(self
, session
, _id
, filter_q
=None, api_req
=False):
1355 Get complete information on an topic
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.
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:
1369 elif len(projects
) > 1:
1370 raise EngineException("Too many projects found", HTTPStatus
.CONFLICT
)
1372 raise EngineException("Project not found", HTTPStatus
.NOT_FOUND
)
1374 def list(self
, session
, filter_q
=None, api_req
=False):
1376 Get a list of the topic that matches a filter
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.
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
]
1390 def delete(self
, session
, _id
, dry_run
=False, not_send_msg
=None):
1392 Delete item by its internal _id
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, ...
1400 # Allow _id to be a name or uuid
1401 proj
= self
.auth
.get_project(_id
)
1403 self
.check_conflict_on_del(session
, pid
, proj
)
1405 v
= self
.auth
.delete_project(pid
)
1406 self
._send
_msg
("deleted", proj
, not_send_msg
=None)
1410 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
1412 Updates a project entry.
1414 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1416 :param indata: data to be inserted
1417 :param kwargs: used to override the indata descriptor
1419 :return: _id: identity of the inserted data.
1421 indata
= self
._remove
_envelop
(indata
)
1423 # Override descriptor with query string kwargs
1425 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
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
)
1441 class RoleTopicAuth(BaseTopic
):
1443 topic_msg
= None # "roles"
1444 schema_new
= roles_new_schema
1445 schema_edit
= roles_edit_schema
1446 multiproject
= False
1448 def __init__(self
, db
, fs
, msg
, auth
):
1449 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
1451 self
.operations
= auth
.role_permissions
1452 # self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles"
1455 def validate_role_definition(operations
, role_definitions
):
1457 Validates the role definition against the operations defined in
1458 the resources to operations files.
1460 :param operations: operations list
1461 :param role_definitions: role definition to test
1462 :return: None if ok, raises ValidationError exception on error
1464 if not role_definitions
.get("permissions"):
1466 ignore_fields
= ["admin", "default"]
1467 for role_def
in role_definitions
["permissions"].keys():
1468 if role_def
in ignore_fields
:
1470 if role_def
[-1] == ":":
1471 raise ValidationError("Operation cannot end with ':'")
1476 for op
in operations
1477 if op
== role_def
or op
.startswith(role_def
+ ":")
1483 raise ValidationError("Invalid permission '{}'".format(role_def
))
1485 def _validate_input_new(self
, input, force
=False):
1487 Validates input user content for a new entry.
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.
1494 validate_input(input, self
.schema_new
)
1495 self
.validate_role_definition(self
.operations
, input)
1499 def _validate_input_edit(self
, input, content
, force
=False):
1501 Validates input user content for updating an entry.
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.
1507 if self
.schema_edit
:
1508 validate_input(input, self
.schema_edit
)
1509 self
.validate_role_definition(self
.operations
, input)
1513 def check_conflict_on_new(self
, session
, indata
):
1515 Check that the data to be inserted is valid
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
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
,
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
1536 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
1538 Check that the data to be edited/uploaded is valid
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
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
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
,
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
,
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
1577 return final_content
1579 def check_conflict_on_del(self
, session
, _id
, db_content
):
1581 Check if deletion can be done because of dependencies if it is not force. To override
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
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
,
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"]
1604 HTTPStatus
.CONFLICT
,
1608 def format_on_new(content
, project_id
=None, make_public
=False): # TO BE REMOVED ?
1610 Modifies content descriptor to include _admin
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
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
1624 if "permissions" not in content
:
1625 content
["permissions"] = {}
1627 if "default" not in content
["permissions"]:
1628 content
["permissions"]["default"] = False
1629 if "admin" not in content
["permissions"]:
1630 content
["permissions"]["admin"] = False
1633 def format_on_edit(final_content
, edit_content
):
1635 Modifies final_content descriptor to include the modified date.
1637 :param final_content: final descriptor generated
1638 :param edit_content: alterations to be include
1639 :return: None, but final_content is modified
1641 if "_admin" in final_content
:
1642 final_content
["_admin"]["modified"] = time()
1644 if "permissions" not in final_content
:
1645 final_content
["permissions"] = {}
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
1653 def show(self
, session
, _id
, filter_q
=None, api_req
=False):
1655 Get complete information on an topic
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.
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)
1667 raise AuthconnNotFoundException(
1668 "Not found any role with filter {}".format(filter_q
)
1670 elif len(roles
) > 1:
1671 raise AuthconnConflictException(
1672 "Found more than one role with filter {}".format(filter_q
)
1676 def list(self
, session
, filter_q
=None, api_req
=False):
1678 Get a list of the topic that matches a filter
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.
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
]
1692 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
1694 Creates a new entry into database.
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)
1704 content
= self
._remove
_envelop
(indata
)
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
)
1711 content
, project_id
=session
["project_id"], make_public
=session
["public"]
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)
1720 except ValidationError
as e
:
1721 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
1723 def delete(self
, session
, _id
, dry_run
=False, not_send_msg
=None):
1725 Delete item by its internal _id
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, ...
1733 filter_q
= {BaseTopic
.id_field(self
.topic
, _id
): _id
}
1734 roles
= self
.auth
.get_role_list(filter_q
)
1736 raise AuthconnNotFoundException(
1737 "Not found any role with filter {}".format(filter_q
)
1739 elif len(roles
) > 1:
1740 raise AuthconnConflictException(
1741 "Found more than one role with filter {}".format(filter_q
)
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
1748 v
= self
.auth
.delete_role(rid
)
1749 # v = self.db.del_one(self.topic, filter_q)
1753 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
1755 Updates a role entry.
1757 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1759 :param indata: data to be inserted
1760 :param kwargs: used to override the indata descriptor
1762 :return: _id: identity of the inserted data.
1765 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
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
)