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