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