Fix Bug 748: Changing the scope using an available token (i.e. without reissuing...
[osm/NBI.git] / osm_nbi / admin_topics.py
1 # -*- coding: utf-8 -*-
2
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12 # implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 # import logging
17 from uuid import uuid4
18 from hashlib import sha256
19 from http import HTTPStatus
20 from time import time
21 from validation import user_new_schema, user_edit_schema, project_new_schema, project_edit_schema
22 from validation import vim_account_new_schema, vim_account_edit_schema, sdn_new_schema, sdn_edit_schema
23 from validation import wim_account_new_schema, wim_account_edit_schema, roles_new_schema, roles_edit_schema
24 from validation import validate_input
25 from validation import ValidationError
26 from validation import is_valid_uuid # To check that User/Project Names don't look like UUIDs
27 from base_topic import BaseTopic, EngineException
28 from authconn_keystone import AuthconnKeystone
29
30 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
31
32
33 class UserTopic(BaseTopic):
34 topic = "users"
35 topic_msg = "users"
36 schema_new = user_new_schema
37 schema_edit = user_edit_schema
38 multiproject = False
39
40 def __init__(self, db, fs, msg):
41 BaseTopic.__init__(self, db, fs, msg)
42
43 @staticmethod
44 def _get_project_filter(session):
45 """
46 Generates a filter dictionary for querying database users.
47 Current policy is admin can show all, non admin, only its own user.
48 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
49 :return:
50 """
51 if session["admin"]: # allows all
52 return {}
53 else:
54 return {"username": session["username"]}
55
56 def check_conflict_on_new(self, session, indata):
57 # check username not exists
58 if self.db.get_one(self.topic, {"username": indata.get("username")}, fail_on_empty=False, fail_on_more=False):
59 raise EngineException("username '{}' exists".format(indata["username"]), HTTPStatus.CONFLICT)
60 # check projects
61 if not session["force"]:
62 for p in indata.get("projects") or []:
63 # To allow project addressing by Name as well as ID
64 if not self.db.get_one("projects", {BaseTopic.id_field("projects", p): p}, fail_on_empty=False,
65 fail_on_more=False):
66 raise EngineException("project '{}' does not exist".format(p), HTTPStatus.CONFLICT)
67
68 def check_conflict_on_del(self, session, _id, db_content):
69 """
70 Check if deletion can be done because of dependencies if it is not force. To override
71 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
72 :param _id: internal _id
73 :param db_content: The database content of this item _id
74 :return: None if ok or raises EngineException with the conflict
75 """
76 if _id == session["username"]:
77 raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
78
79 @staticmethod
80 def format_on_new(content, project_id=None, make_public=False):
81 BaseTopic.format_on_new(content, make_public=False)
82 # Removed so that the UUID is kept, to allow User Name modification
83 # content["_id"] = content["username"]
84 salt = uuid4().hex
85 content["_admin"]["salt"] = salt
86 if content.get("password"):
87 content["password"] = sha256(content["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
88 if content.get("project_role_mappings"):
89 projects = [mapping[0] for mapping in content["project_role_mappings"]]
90
91 if content.get("projects"):
92 content["projects"] += projects
93 else:
94 content["projects"] = projects
95
96 @staticmethod
97 def format_on_edit(final_content, edit_content):
98 BaseTopic.format_on_edit(final_content, edit_content)
99 if edit_content.get("password"):
100 salt = uuid4().hex
101 final_content["_admin"]["salt"] = salt
102 final_content["password"] = sha256(edit_content["password"].encode('utf-8') +
103 salt.encode('utf-8')).hexdigest()
104
105 def edit(self, session, _id, indata=None, kwargs=None, content=None):
106 if not session["admin"]:
107 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
108 # Names that look like UUIDs are not allowed
109 name = (indata if indata else kwargs).get("username")
110 if is_valid_uuid(name):
111 raise EngineException("Usernames that look like UUIDs are not allowed",
112 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
113 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
114
115 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
116 if not session["admin"]:
117 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
118 # Names that look like UUIDs are not allowed
119 name = indata["username"] if indata else kwargs["username"]
120 if is_valid_uuid(name):
121 raise EngineException("Usernames that look like UUIDs are not allowed",
122 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
123 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
124
125
126 class ProjectTopic(BaseTopic):
127 topic = "projects"
128 topic_msg = "projects"
129 schema_new = project_new_schema
130 schema_edit = project_edit_schema
131 multiproject = False
132
133 def __init__(self, db, fs, msg):
134 BaseTopic.__init__(self, db, fs, msg)
135
136 @staticmethod
137 def _get_project_filter(session):
138 """
139 Generates a filter dictionary for querying database users.
140 Current policy is admin can show all, non admin, only its own user.
141 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
142 :return:
143 """
144 if session["admin"]: # allows all
145 return {}
146 else:
147 return {"_id.cont": session["project_id"]}
148
149 def check_conflict_on_new(self, session, indata):
150 if not indata.get("name"):
151 raise EngineException("missing 'name'")
152 # check name not exists
153 if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
154 raise EngineException("name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT)
155
156 @staticmethod
157 def format_on_new(content, project_id=None, make_public=False):
158 BaseTopic.format_on_new(content, None)
159 # Removed so that the UUID is kept, to allow Project Name modification
160 # content["_id"] = content["name"]
161
162 def check_conflict_on_del(self, session, _id, db_content):
163 """
164 Check if deletion can be done because of dependencies if it is not force. To override
165 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
166 :param _id: internal _id
167 :param db_content: The database content of this item _id
168 :return: None if ok or raises EngineException with the conflict
169 """
170 if _id in session["project_id"]:
171 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
172 if session["force"]:
173 return
174 _filter = {"projects": _id}
175 if self.db.get_list("users", _filter):
176 raise EngineException("There is some USER that contains this project", http_code=HTTPStatus.CONFLICT)
177
178 def edit(self, session, _id, indata=None, kwargs=None, content=None):
179 if not session["admin"]:
180 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
181 # Names that look like UUIDs are not allowed
182 name = (indata if indata else kwargs).get("name")
183 if is_valid_uuid(name):
184 raise EngineException("Project names that look like UUIDs are not allowed",
185 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
186 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
187
188 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
189 if not session["admin"]:
190 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
191 # Names that look like UUIDs are not allowed
192 name = indata["name"] if indata else kwargs["name"]
193 if is_valid_uuid(name):
194 raise EngineException("Project names that look like UUIDs are not allowed",
195 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
196 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
197
198
199 class VimAccountTopic(BaseTopic):
200 topic = "vim_accounts"
201 topic_msg = "vim_account"
202 schema_new = vim_account_new_schema
203 schema_edit = vim_account_edit_schema
204 vim_config_encrypted = ("admin_password", "nsx_password", "vcenter_password")
205 multiproject = True
206
207 def __init__(self, db, fs, msg):
208 BaseTopic.__init__(self, db, fs, msg)
209
210 def check_conflict_on_new(self, session, indata):
211 self.check_unique_name(session, indata["name"], _id=None)
212
213 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
214 if not session["force"] and edit_content.get("name"):
215 self.check_unique_name(session, edit_content["name"], _id=_id)
216
217 # encrypt passwords
218 schema_version = final_content.get("schema_version")
219 if schema_version:
220 if edit_content.get("vim_password"):
221 final_content["vim_password"] = self.db.encrypt(edit_content["vim_password"],
222 schema_version=schema_version, salt=_id)
223 if edit_content.get("config"):
224 for p in self.vim_config_encrypted:
225 if edit_content["config"].get(p):
226 final_content["config"][p] = self.db.encrypt(edit_content["config"][p],
227 schema_version=schema_version, salt=_id)
228
229 def format_on_new(self, content, project_id=None, make_public=False):
230 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
231 content["schema_version"] = schema_version = "1.1"
232
233 # encrypt passwords
234 if content.get("vim_password"):
235 content["vim_password"] = self.db.encrypt(content["vim_password"], schema_version=schema_version,
236 salt=content["_id"])
237 if content.get("config"):
238 for p in self.vim_config_encrypted:
239 if content["config"].get(p):
240 content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version,
241 salt=content["_id"])
242
243 content["_admin"]["operationalState"] = "PROCESSING"
244
245 def delete(self, session, _id, dry_run=False):
246 """
247 Delete item by its internal _id
248 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
249 :param _id: server internal id
250 :param dry_run: make checking but do not delete
251 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
252 """
253 # TODO add admin to filter, validate rights
254 if dry_run or session["force"]: # delete completely
255 return BaseTopic.delete(self, session, _id, dry_run)
256 else: # if not, sent to kafka
257 v = BaseTopic.delete(self, session, _id, dry_run=True)
258 self.db.set_one("vim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
259 self._send_msg("delete", {"_id": _id})
260 return v # TODO indicate an offline operation to return 202 ACCEPTED
261
262
263 class WimAccountTopic(BaseTopic):
264 topic = "wim_accounts"
265 topic_msg = "wim_account"
266 schema_new = wim_account_new_schema
267 schema_edit = wim_account_edit_schema
268 multiproject = True
269 wim_config_encrypted = ()
270
271 def __init__(self, db, fs, msg):
272 BaseTopic.__init__(self, db, fs, msg)
273
274 def check_conflict_on_new(self, session, indata):
275 self.check_unique_name(session, indata["name"], _id=None)
276
277 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
278 if not session["force"] and edit_content.get("name"):
279 self.check_unique_name(session, edit_content["name"], _id=_id)
280
281 # encrypt passwords
282 schema_version = final_content.get("schema_version")
283 if schema_version:
284 if edit_content.get("wim_password"):
285 final_content["wim_password"] = self.db.encrypt(edit_content["wim_password"],
286 schema_version=schema_version, salt=_id)
287 if edit_content.get("config"):
288 for p in self.wim_config_encrypted:
289 if edit_content["config"].get(p):
290 final_content["config"][p] = self.db.encrypt(edit_content["config"][p],
291 schema_version=schema_version, salt=_id)
292
293 def format_on_new(self, content, project_id=None, make_public=False):
294 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
295 content["schema_version"] = schema_version = "1.1"
296
297 # encrypt passwords
298 if content.get("wim_password"):
299 content["wim_password"] = self.db.encrypt(content["wim_password"], schema_version=schema_version,
300 salt=content["_id"])
301 if content.get("config"):
302 for p in self.wim_config_encrypted:
303 if content["config"].get(p):
304 content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version,
305 salt=content["_id"])
306
307 content["_admin"]["operationalState"] = "PROCESSING"
308
309 def delete(self, session, _id, dry_run=False):
310 """
311 Delete item by its internal _id
312 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
313 :param _id: server internal id
314 :param dry_run: make checking but do not delete
315 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
316 """
317 # TODO add admin to filter, validate rights
318 if dry_run or session["force"]: # delete completely
319 return BaseTopic.delete(self, session, _id, dry_run)
320 else: # if not, sent to kafka
321 v = BaseTopic.delete(self, session, _id, dry_run=True)
322 self.db.set_one("wim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
323 self._send_msg("delete", {"_id": _id})
324 return v # TODO indicate an offline operation to return 202 ACCEPTED
325
326
327 class SdnTopic(BaseTopic):
328 topic = "sdns"
329 topic_msg = "sdn"
330 schema_new = sdn_new_schema
331 schema_edit = sdn_edit_schema
332 multiproject = True
333
334 def __init__(self, db, fs, msg):
335 BaseTopic.__init__(self, db, fs, msg)
336
337 def check_conflict_on_new(self, session, indata):
338 self.check_unique_name(session, indata["name"], _id=None)
339
340 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
341 if not session["force"] and edit_content.get("name"):
342 self.check_unique_name(session, edit_content["name"], _id=_id)
343
344 # encrypt passwords
345 schema_version = final_content.get("schema_version")
346 if schema_version and edit_content.get("password"):
347 final_content["password"] = self.db.encrypt(edit_content["password"], schema_version=schema_version,
348 salt=_id)
349
350 def format_on_new(self, content, project_id=None, make_public=False):
351 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
352 content["schema_version"] = schema_version = "1.1"
353 # encrypt passwords
354 if content.get("password"):
355 content["password"] = self.db.encrypt(content["password"], schema_version=schema_version,
356 salt=content["_id"])
357
358 content["_admin"]["operationalState"] = "PROCESSING"
359
360 def delete(self, session, _id, dry_run=False):
361 """
362 Delete item by its internal _id
363 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
364 :param _id: server internal id
365 :param dry_run: make checking but do not delete
366 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
367 """
368 if dry_run or session["force"]: # delete completely
369 return BaseTopic.delete(self, session, _id, dry_run)
370 else: # if not sent to kafka
371 v = BaseTopic.delete(self, session, _id, dry_run=True)
372 self.db.set_one("sdns", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
373 self._send_msg("delete", {"_id": _id})
374 return v # TODO indicate an offline operation to return 202 ACCEPTED
375
376
377 class UserTopicAuth(UserTopic):
378 # topic = "users"
379 # topic_msg = "users"
380 schema_new = user_new_schema
381 schema_edit = user_edit_schema
382
383 def __init__(self, db, fs, msg, auth):
384 UserTopic.__init__(self, db, fs, msg)
385 self.auth = auth
386
387 def check_conflict_on_new(self, session, indata):
388 """
389 Check that the data to be inserted is valid
390
391 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
392 :param indata: data to be inserted
393 :return: None or raises EngineException
394 """
395 username = indata.get("username")
396 if is_valid_uuid(username):
397 raise EngineException("username '{}' cannot have a uuid format".format(username),
398 HTTPStatus.UNPROCESSABLE_ENTITY)
399
400 # Check that username is not used, regardless keystone already checks this
401 if self.auth.get_user_list(filter_q={"name": username}):
402 raise EngineException("username '{}' is already used".format(username), HTTPStatus.CONFLICT)
403
404 if "projects" in indata.keys():
405 raise EngineException("Format invalid: the keyword \"projects\" is not allowed for keystone authentication",
406 HTTPStatus.BAD_REQUEST)
407
408 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
409 """
410 Check that the data to be edited/uploaded is valid
411
412 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
413 :param final_content: data once modified
414 :param edit_content: incremental data that contains the modifications to apply
415 :param _id: internal _id
416 :return: None or raises EngineException
417 """
418
419 if "username" in edit_content:
420 username = edit_content.get("username")
421 if is_valid_uuid(username):
422 raise EngineException("username '{}' cannot have an uuid format".format(username),
423 HTTPStatus.UNPROCESSABLE_ENTITY)
424
425 # Check that username is not used, regardless keystone already checks this
426 if self.auth.get_user_list(filter_q={"name": username}):
427 raise EngineException("username '{}' is already used".format(username), HTTPStatus.CONFLICT)
428
429 if final_content["username"] == "admin":
430 for mapping in edit_content.get("remove_project_role_mappings", ()):
431 if mapping["project"] == "admin" and mapping.get("role") in (None, "system_admin"):
432 # TODO make this also available for project id and role id
433 raise EngineException("You cannot remove system_admin role from admin user",
434 http_code=HTTPStatus.FORBIDDEN)
435
436 def check_conflict_on_del(self, session, _id, db_content):
437 """
438 Check if deletion can be done because of dependencies if it is not force. To override
439 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
440 :param _id: internal _id
441 :param db_content: The database content of this item _id
442 :return: None if ok or raises EngineException with the conflict
443 """
444 if db_content["username"] == session["username"]:
445 raise EngineException("You cannot delete your own login user ", http_code=HTTPStatus.CONFLICT)
446
447 # @staticmethod
448 # def format_on_new(content, project_id=None, make_public=False):
449 # """
450 # Modifies content descriptor to include _id.
451 #
452 # NOTE: No password salt required because the authentication backend
453 # should handle these security concerns.
454 #
455 # :param content: descriptor to be modified
456 # :param make_public: if included it is generated as public for reading.
457 # :return: None, but content is modified
458 # """
459 # BaseTopic.format_on_new(content, make_public=False)
460 # content["_id"] = content["username"]
461 # content["password"] = content["password"]
462
463 # @staticmethod
464 # def format_on_edit(final_content, edit_content):
465 # """
466 # Modifies final_content descriptor to include the modified date.
467 #
468 # NOTE: No password salt required because the authentication backend
469 # should handle these security concerns.
470 #
471 # :param final_content: final descriptor generated
472 # :param edit_content: alterations to be include
473 # :return: None, but final_content is modified
474 # """
475 # BaseTopic.format_on_edit(final_content, edit_content)
476 # if "password" in edit_content:
477 # final_content["password"] = edit_content["password"]
478 # else:
479 # final_content["project_role_mappings"] = edit_content["project_role_mappings"]
480
481 @staticmethod
482 def format_on_show(content):
483 """
484 Modifies the content of the role information to separate the role
485 metadata from the role definition.
486 """
487 project_role_mappings = []
488
489 for project in content["projects"]:
490 for role in project["roles"]:
491 project_role_mappings.append({"project": project["_id"],
492 "project_name": project["name"],
493 "role": role["_id"],
494 "role_name": role["name"]})
495
496 del content["projects"]
497 content["project_role_mappings"] = project_role_mappings
498
499 return content
500
501 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
502 """
503 Creates a new entry into the authentication backend.
504
505 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
506
507 :param rollback: list to append created items at database in case a rollback may to be done
508 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
509 :param indata: data to be inserted
510 :param kwargs: used to override the indata descriptor
511 :param headers: http request headers
512 :return: _id: identity of the inserted data.
513 """
514 try:
515 content = BaseTopic._remove_envelop(indata)
516
517 # Override descriptor with query string kwargs
518 BaseTopic._update_input_with_kwargs(content, kwargs)
519 content = self._validate_input_new(content, session["force"])
520 self.check_conflict_on_new(session, content)
521 # self.format_on_new(content, session["project_id"], make_public=session["public"])
522 _id = self.auth.create_user(content["username"], content["password"])["_id"]
523
524 if "project_role_mappings" in content.keys():
525 for mapping in content["project_role_mappings"]:
526 self.auth.assign_role_to_user(_id, mapping["project"], mapping["role"])
527
528 rollback.append({"topic": self.topic, "_id": _id})
529 # del content["password"]
530 # self._send_msg("create", content)
531 return _id
532 except ValidationError as e:
533 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
534
535 def show(self, session, _id):
536 """
537 Get complete information on an topic
538
539 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
540 :param _id: server internal id
541 :return: dictionary, raise exception if not found.
542 """
543 # Allow _id to be a name or uuid
544 filter_q = {self.id_field(self.topic, _id): _id}
545 users = self.auth.get_user_list(filter_q)
546
547 if len(users) == 1:
548 return self.format_on_show(users[0])
549 elif len(users) > 1:
550 raise EngineException("Too many users found", HTTPStatus.CONFLICT)
551 else:
552 raise EngineException("User not found", HTTPStatus.NOT_FOUND)
553
554 def edit(self, session, _id, indata=None, kwargs=None, content=None):
555 """
556 Updates an user entry.
557
558 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
559 :param _id:
560 :param indata: data to be inserted
561 :param kwargs: used to override the indata descriptor
562 :param content:
563 :return: _id: identity of the inserted data.
564 """
565 indata = self._remove_envelop(indata)
566
567 # Override descriptor with query string kwargs
568 if kwargs:
569 BaseTopic._update_input_with_kwargs(indata, kwargs)
570 try:
571 indata = self._validate_input_edit(indata, force=session["force"])
572
573 if not content:
574 content = self.show(session, _id)
575 self.check_conflict_on_edit(session, content, indata, _id=_id)
576 # self.format_on_edit(content, indata)
577
578 if "password" in indata or "username" in indata:
579 self.auth.update_user(_id, new_name=indata.get("username"), new_password=indata.get("password"))
580 if not indata.get("remove_project_role_mappings") and not indata.get("add_project_role_mappings") and \
581 not indata.get("project_role_mappings"):
582 return _id
583 if indata.get("project_role_mappings") and \
584 (indata.get("remove_project_role_mappings") or indata.get("add_project_role_mappings")):
585 raise EngineException("Option 'project_role_mappings' is incompatible with 'add_project_role_mappings"
586 "' or 'remove_project_role_mappings'", http_code=HTTPStatus.BAD_REQUEST)
587
588 user = self.show(session, _id)
589 original_mapping = user["project_role_mappings"]
590
591 mappings_to_add = []
592 mappings_to_remove = []
593
594 # remove
595 for to_remove in indata.get("remove_project_role_mappings", ()):
596 for mapping in original_mapping:
597 if to_remove["project"] in (mapping["project"], mapping["project_name"]):
598 if not to_remove.get("role") or to_remove["role"] in (mapping["role"], mapping["role_name"]):
599 mappings_to_remove.append(mapping)
600
601 # add
602 for to_add in indata.get("add_project_role_mappings", ()):
603 for mapping in original_mapping:
604 if to_add["project"] in (mapping["project"], mapping["project_name"]) and \
605 to_add["role"] in (mapping["role"], mapping["role_name"]):
606
607 if mapping in mappings_to_remove: # do not remove
608 mappings_to_remove.remove(mapping)
609 break # do not add, it is already at user
610 else:
611 mappings_to_add.append(to_add)
612
613 # set
614 if indata.get("project_role_mappings"):
615 for to_set in indata["project_role_mappings"]:
616 for mapping in original_mapping:
617 if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
618 to_set["role"] in (mapping["role"], mapping["role_name"]):
619
620 if mapping in mappings_to_remove: # do not remove
621 mappings_to_remove.remove(mapping)
622 break # do not add, it is already at user
623 else:
624 mappings_to_add.append(to_set)
625 for mapping in original_mapping:
626 for to_set in indata["project_role_mappings"]:
627 if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
628 to_set["role"] in (mapping["role"], mapping["role_name"]):
629 break
630 else:
631 # delete
632 if mapping not in mappings_to_remove: # do not remove
633 mappings_to_remove.append(mapping)
634
635 for mapping in mappings_to_remove:
636 self.auth.remove_role_from_user(
637 _id,
638 mapping["project"],
639 mapping["role"]
640 )
641
642 for mapping in mappings_to_add:
643 self.auth.assign_role_to_user(
644 _id,
645 mapping["project"],
646 mapping["role"]
647 )
648
649 return "_id"
650 except ValidationError as e:
651 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
652
653 def list(self, session, filter_q=None):
654 """
655 Get a list of the topic that matches a filter
656 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
657 :param filter_q: filter of data to be applied
658 :return: The list, it can be empty if no one match the filter.
659 """
660 users = [self.format_on_show(user) for user in self.auth.get_user_list(filter_q)]
661
662 return users
663
664 def delete(self, session, _id, dry_run=False):
665 """
666 Delete item by its internal _id
667
668 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
669 :param _id: server internal id
670 :param force: indicates if deletion must be forced in case of conflict
671 :param dry_run: make checking but do not delete
672 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
673 """
674 # Allow _id to be a name or uuid
675 filter_q = {self.id_field(self.topic, _id): _id}
676 user_list = self.auth.get_user_list(filter_q)
677 if not user_list:
678 raise EngineException("User '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
679 _id = user_list[0]["_id"]
680 self.check_conflict_on_del(session, _id, user_list[0])
681 if not dry_run:
682 v = self.auth.delete_user(_id)
683 return v
684 return None
685
686
687 class ProjectTopicAuth(ProjectTopic):
688 # topic = "projects"
689 # topic_msg = "projects"
690 schema_new = project_new_schema
691 schema_edit = project_edit_schema
692
693 def __init__(self, db, fs, msg, auth):
694 ProjectTopic.__init__(self, db, fs, msg)
695 self.auth = auth
696
697 def check_conflict_on_new(self, session, indata):
698 """
699 Check that the data to be inserted is valid
700
701 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
702 :param indata: data to be inserted
703 :return: None or raises EngineException
704 """
705 project_name = indata.get("name")
706 if is_valid_uuid(project_name):
707 raise EngineException("project name '{}' cannot have an uuid format".format(project_name),
708 HTTPStatus.UNPROCESSABLE_ENTITY)
709
710 project_list = self.auth.get_project_list(filter_q={"name": project_name})
711
712 if project_list:
713 raise EngineException("project '{}' exists".format(project_name), HTTPStatus.CONFLICT)
714
715 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
716 """
717 Check that the data to be edited/uploaded is valid
718
719 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
720 :param final_content: data once modified
721 :param edit_content: incremental data that contains the modifications to apply
722 :param _id: internal _id
723 :return: None or raises EngineException
724 """
725
726 project_name = edit_content.get("name")
727 if project_name:
728 if is_valid_uuid(project_name):
729 raise EngineException("project name '{}' cannot be an uuid format".format(project_name),
730 HTTPStatus.UNPROCESSABLE_ENTITY)
731
732 # Check that project name is not used, regardless keystone already checks this
733 if self.auth.get_project_list(filter_q={"name": project_name}):
734 raise EngineException("project '{}' is already used".format(project_name), HTTPStatus.CONFLICT)
735
736 def check_conflict_on_del(self, session, _id, db_content):
737 """
738 Check if deletion can be done because of dependencies if it is not force. To override
739
740 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
741 :param _id: internal _id
742 :param db_content: The database content of this item _id
743 :return: None if ok or raises EngineException with the conflict
744 """
745 # projects = self.auth.get_project_list()
746 # current_project = [project for project in projects
747 # if project["name"] in session["project_id"]][0]
748 # TODO check that any user is using this project, raise CONFLICT exception
749 if _id == session["project_id"]:
750 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
751
752 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
753 """
754 Creates a new entry into the authentication backend.
755
756 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
757
758 :param rollback: list to append created items at database in case a rollback may to be done
759 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
760 :param indata: data to be inserted
761 :param kwargs: used to override the indata descriptor
762 :param headers: http request headers
763 :return: _id: identity of the inserted data.
764 """
765 try:
766 content = BaseTopic._remove_envelop(indata)
767
768 # Override descriptor with query string kwargs
769 BaseTopic._update_input_with_kwargs(content, kwargs)
770 content = self._validate_input_new(content, session["force"])
771 self.check_conflict_on_new(session, content)
772 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
773 _id = self.auth.create_project(content["name"])
774 rollback.append({"topic": self.topic, "_id": _id})
775 # self._send_msg("create", content)
776 return _id
777 except ValidationError as e:
778 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
779
780 def show(self, session, _id):
781 """
782 Get complete information on an topic
783
784 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
785 :param _id: server internal id
786 :return: dictionary, raise exception if not found.
787 """
788 # Allow _id to be a name or uuid
789 filter_q = {self.id_field(self.topic, _id): _id}
790 projects = self.auth.get_project_list(filter_q=filter_q)
791
792 if len(projects) == 1:
793 return projects[0]
794 elif len(projects) > 1:
795 raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
796 else:
797 raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
798
799 def list(self, session, filter_q=None):
800 """
801 Get a list of the topic that matches a filter
802
803 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
804 :param filter_q: filter of data to be applied
805 :return: The list, it can be empty if no one match the filter.
806 """
807 return self.auth.get_project_list(filter_q)
808
809 def delete(self, session, _id, dry_run=False):
810 """
811 Delete item by its internal _id
812
813 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
814 :param _id: server internal id
815 :param dry_run: make checking but do not delete
816 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
817 """
818 # Allow _id to be a name or uuid
819 filter_q = {self.id_field(self.topic, _id): _id}
820 project_list = self.auth.get_project_list(filter_q)
821 if not project_list:
822 raise EngineException("Project '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
823 _id = project_list[0]["_id"]
824 self.check_conflict_on_del(session, _id, project_list[0])
825 if not dry_run:
826 v = self.auth.delete_project(_id)
827 return v
828 return None
829
830 def edit(self, session, _id, indata=None, kwargs=None, content=None):
831 """
832 Updates a project entry.
833
834 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
835 :param _id:
836 :param indata: data to be inserted
837 :param kwargs: used to override the indata descriptor
838 :param content:
839 :return: _id: identity of the inserted data.
840 """
841 indata = self._remove_envelop(indata)
842
843 # Override descriptor with query string kwargs
844 if kwargs:
845 BaseTopic._update_input_with_kwargs(indata, kwargs)
846 try:
847 indata = self._validate_input_edit(indata, force=session["force"])
848
849 if not content:
850 content = self.show(session, _id)
851 self.check_conflict_on_edit(session, content, indata, _id=_id)
852 # self.format_on_edit(content, indata)
853
854 if "name" in indata:
855 self.auth.update_project(content["_id"], indata["name"])
856 except ValidationError as e:
857 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
858
859
860 class RoleTopicAuth(BaseTopic):
861 topic = "roles"
862 topic_msg = None # "roles"
863 schema_new = roles_new_schema
864 schema_edit = roles_edit_schema
865 multiproject = False
866
867 def __init__(self, db, fs, msg, auth, ops):
868 BaseTopic.__init__(self, db, fs, msg)
869 self.auth = auth
870 self.operations = ops
871 self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles"
872
873 @staticmethod
874 def validate_role_definition(operations, role_definitions):
875 """
876 Validates the role definition against the operations defined in
877 the resources to operations files.
878
879 :param operations: operations list
880 :param role_definitions: role definition to test
881 :return: None if ok, raises ValidationError exception on error
882 """
883 if not role_definitions.get("permissions"):
884 return
885 ignore_fields = ["admin", "default"]
886 for role_def in role_definitions["permissions"].keys():
887 if role_def in ignore_fields:
888 continue
889 if role_def[-1] == ":":
890 raise ValidationError("Operation cannot end with ':'")
891
892 role_def_matches = [op for op in operations if op.startswith(role_def)]
893
894 if len(role_def_matches) == 0:
895 raise ValidationError("Invalid permission '{}'".format(role_def))
896
897 def _validate_input_new(self, input, force=False):
898 """
899 Validates input user content for a new entry.
900
901 :param input: user input content for the new topic
902 :param force: may be used for being more tolerant
903 :return: The same input content, or a changed version of it.
904 """
905 if self.schema_new:
906 validate_input(input, self.schema_new)
907 self.validate_role_definition(self.operations, input)
908
909 return input
910
911 def _validate_input_edit(self, input, force=False):
912 """
913 Validates input user content for updating an entry.
914
915 :param input: user input content for the new topic
916 :param force: may be used for being more tolerant
917 :return: The same input content, or a changed version of it.
918 """
919 if self.schema_edit:
920 validate_input(input, self.schema_edit)
921 self.validate_role_definition(self.operations, input)
922
923 return input
924
925 def check_conflict_on_new(self, session, indata):
926 """
927 Check that the data to be inserted is valid
928
929 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
930 :param indata: data to be inserted
931 :return: None or raises EngineException
932 """
933 # check name not exists
934 if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
935 raise EngineException("role name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT)
936
937 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
938 """
939 Check that the data to be edited/uploaded is valid
940
941 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
942 :param final_content: data once modified
943 :param edit_content: incremental data that contains the modifications to apply
944 :param _id: internal _id
945 :return: None or raises EngineException
946 """
947 if "default" not in final_content["permissions"]:
948 final_content["permissions"]["default"] = False
949 if "admin" not in final_content["permissions"]:
950 final_content["permissions"]["admin"] = False
951
952 # check name not exists
953 if "name" in edit_content:
954 role_name = edit_content["name"]
955 if self.db.get_one(self.topic, {"name": role_name, "_id.ne": _id}, fail_on_empty=False, fail_on_more=False):
956 raise EngineException("role name '{}' exists".format(role_name), HTTPStatus.CONFLICT)
957
958 def check_conflict_on_del(self, session, _id, db_content):
959 """
960 Check if deletion can be done because of dependencies if it is not force. To override
961
962 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
963 :param _id: internal _id
964 :param db_content: The database content of this item _id
965 :return: None if ok or raises EngineException with the conflict
966 """
967 roles = self.auth.get_role_list()
968 system_admin_roles = [role for role in roles if role["name"] == "system_admin"]
969
970 if system_admin_roles and _id == system_admin_roles[0]["_id"]:
971 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
972
973 @staticmethod
974 def format_on_new(content, project_id=None, make_public=False):
975 """
976 Modifies content descriptor to include _admin
977
978 :param content: descriptor to be modified
979 :param project_id: if included, it add project read/write permissions
980 :param make_public: if included it is generated as public for reading.
981 :return: None, but content is modified
982 """
983 now = time()
984 if "_admin" not in content:
985 content["_admin"] = {}
986 if not content["_admin"].get("created"):
987 content["_admin"]["created"] = now
988 content["_admin"]["modified"] = now
989
990 if "permissions" not in content:
991 content["permissions"] = {}
992
993 if "default" not in content["permissions"]:
994 content["permissions"]["default"] = False
995 if "admin" not in content["permissions"]:
996 content["permissions"]["admin"] = False
997
998 @staticmethod
999 def format_on_edit(final_content, edit_content):
1000 """
1001 Modifies final_content descriptor to include the modified date.
1002
1003 :param final_content: final descriptor generated
1004 :param edit_content: alterations to be include
1005 :return: None, but final_content is modified
1006 """
1007 final_content["_admin"]["modified"] = time()
1008
1009 if "permissions" not in final_content:
1010 final_content["permissions"] = {}
1011
1012 if "default" not in final_content["permissions"]:
1013 final_content["permissions"]["default"] = False
1014 if "admin" not in final_content["permissions"]:
1015 final_content["permissions"]["admin"] = False
1016
1017 # @staticmethod
1018 # def format_on_show(content):
1019 # """
1020 # Modifies the content of the role information to separate the role
1021 # metadata from the role definition. Eases the reading process of the
1022 # role definition.
1023 #
1024 # :param definition: role definition to be processed
1025 # """
1026 # content["_id"] = str(content["_id"])
1027 #
1028 # def show(self, session, _id):
1029 # """
1030 # Get complete information on an topic
1031 #
1032 # :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1033 # :param _id: server internal id
1034 # :return: dictionary, raise exception if not found.
1035 # """
1036 # filter_db = {"_id": _id}
1037 # filter_db = { BaseTopic.id_field(self.topic, _id): _id } # To allow role addressing by name
1038 #
1039 # role = self.db.get_one(self.topic, filter_db)
1040 # new_role = dict(role)
1041 # self.format_on_show(new_role)
1042 #
1043 # return new_role
1044
1045 # def list(self, session, filter_q=None):
1046 # """
1047 # Get a list of the topic that matches a filter
1048 #
1049 # :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1050 # :param filter_q: filter of data to be applied
1051 # :return: The list, it can be empty if no one match the filter.
1052 # """
1053 # if not filter_q:
1054 # filter_q = {}
1055 #
1056 # if ":" in filter_q:
1057 # filter_q["root"] = filter_q[":"]
1058 #
1059 # for key in filter_q.keys():
1060 # if key == "name":
1061 # continue
1062 # filter_q[key] = filter_q[key] in ["True", "true"]
1063 #
1064 # roles = self.db.get_list(self.topic, filter_q)
1065 # new_roles = []
1066 #
1067 # for role in roles:
1068 # new_role = dict(role)
1069 # self.format_on_show(new_role)
1070 # new_roles.append(new_role)
1071 #
1072 # return new_roles
1073
1074 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1075 """
1076 Creates a new entry into database.
1077
1078 :param rollback: list to append created items at database in case a rollback may to be done
1079 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1080 :param indata: data to be inserted
1081 :param kwargs: used to override the indata descriptor
1082 :param headers: http request headers
1083 :return: _id: identity of the inserted data.
1084 """
1085 try:
1086 content = self._remove_envelop(indata)
1087
1088 # Override descriptor with query string kwargs
1089 self._update_input_with_kwargs(content, kwargs)
1090 content = self._validate_input_new(content, session["force"])
1091 self.check_conflict_on_new(session, content)
1092 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
1093 role_name = content["name"]
1094 role_id = self.auth.create_role(role_name)
1095 content["_id"] = role_id
1096 _id = self.db.create(self.topic, content)
1097 rollback.append({"topic": self.topic, "_id": _id})
1098 # self._send_msg("create", content)
1099 return _id
1100 except ValidationError as e:
1101 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1102
1103 def delete(self, session, _id, dry_run=False):
1104 """
1105 Delete item by its internal _id
1106
1107 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1108 :param _id: server internal id
1109 :param dry_run: make checking but do not delete
1110 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1111 """
1112 self.check_conflict_on_del(session, _id, None)
1113 # filter_q = {"_id": _id}
1114 filter_q = {BaseTopic.id_field(self.topic, _id): _id} # To allow role addressing by name
1115 if not dry_run:
1116 self.auth.delete_role(_id)
1117 v = self.db.del_one(self.topic, filter_q)
1118 return v
1119 return None
1120
1121 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1122 """
1123 Updates a role entry.
1124
1125 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1126 :param _id:
1127 :param indata: data to be inserted
1128 :param kwargs: used to override the indata descriptor
1129 :param content:
1130 :return: _id: identity of the inserted data.
1131 """
1132 _id = super().edit(session, _id, indata, kwargs, content)
1133 if indata.get("name"):
1134 self.auth.update_role(_id, name=indata.get("name"))