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