fix bug 791. Adding input validation for ns-create. It was only for ns-instantiate
[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 # convert to new format project_role_mappings
406 if not indata.get("project_role_mappings"):
407 indata["project_role_mappings"] = []
408 for project in indata["projects"]:
409 indata["project_role_mappings"].append({"project": project, "role": "project_user"})
410 # raise EngineException("Format invalid: the keyword 'projects' is not allowed for keystone authentication",
411 # HTTPStatus.BAD_REQUEST)
412
413 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
414 """
415 Check that the data to be edited/uploaded is valid
416
417 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
418 :param final_content: data once modified
419 :param edit_content: incremental data that contains the modifications to apply
420 :param _id: internal _id
421 :return: None or raises EngineException
422 """
423
424 if "username" in edit_content:
425 username = edit_content.get("username")
426 if is_valid_uuid(username):
427 raise EngineException("username '{}' cannot have an uuid format".format(username),
428 HTTPStatus.UNPROCESSABLE_ENTITY)
429
430 # Check that username is not used, regardless keystone already checks this
431 if self.auth.get_user_list(filter_q={"name": username}):
432 raise EngineException("username '{}' is already used".format(username), HTTPStatus.CONFLICT)
433
434 if final_content["username"] == "admin":
435 for mapping in edit_content.get("remove_project_role_mappings", ()):
436 if mapping["project"] == "admin" and mapping.get("role") in (None, "system_admin"):
437 # TODO make this also available for project id and role id
438 raise EngineException("You cannot remove system_admin role from admin user",
439 http_code=HTTPStatus.FORBIDDEN)
440
441 def check_conflict_on_del(self, session, _id, db_content):
442 """
443 Check if deletion can be done because of dependencies if it is not force. To override
444 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
445 :param _id: internal _id
446 :param db_content: The database content of this item _id
447 :return: None if ok or raises EngineException with the conflict
448 """
449 if db_content["username"] == session["username"]:
450 raise EngineException("You cannot delete your own login user ", http_code=HTTPStatus.CONFLICT)
451
452 # @staticmethod
453 # def format_on_new(content, project_id=None, make_public=False):
454 # """
455 # Modifies content descriptor to include _id.
456 #
457 # NOTE: No password salt required because the authentication backend
458 # should handle these security concerns.
459 #
460 # :param content: descriptor to be modified
461 # :param make_public: if included it is generated as public for reading.
462 # :return: None, but content is modified
463 # """
464 # BaseTopic.format_on_new(content, make_public=False)
465 # content["_id"] = content["username"]
466 # content["password"] = content["password"]
467
468 # @staticmethod
469 # def format_on_edit(final_content, edit_content):
470 # """
471 # Modifies final_content descriptor to include the modified date.
472 #
473 # NOTE: No password salt required because the authentication backend
474 # should handle these security concerns.
475 #
476 # :param final_content: final descriptor generated
477 # :param edit_content: alterations to be include
478 # :return: None, but final_content is modified
479 # """
480 # BaseTopic.format_on_edit(final_content, edit_content)
481 # if "password" in edit_content:
482 # final_content["password"] = edit_content["password"]
483 # else:
484 # final_content["project_role_mappings"] = edit_content["project_role_mappings"]
485
486 @staticmethod
487 def format_on_show(content):
488 """
489 Modifies the content of the role information to separate the role
490 metadata from the role definition.
491 """
492 project_role_mappings = []
493
494 for project in content["projects"]:
495 for role in project["roles"]:
496 project_role_mappings.append({"project": project["_id"],
497 "project_name": project["name"],
498 "role": role["_id"],
499 "role_name": role["name"]})
500
501 del content["projects"]
502 content["project_role_mappings"] = project_role_mappings
503
504 return content
505
506 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
507 """
508 Creates a new entry into the authentication backend.
509
510 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
511
512 :param rollback: list to append created items at database in case a rollback may to be done
513 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
514 :param indata: data to be inserted
515 :param kwargs: used to override the indata descriptor
516 :param headers: http request headers
517 :return: _id: identity of the inserted data.
518 """
519 try:
520 content = BaseTopic._remove_envelop(indata)
521
522 # Override descriptor with query string kwargs
523 BaseTopic._update_input_with_kwargs(content, kwargs)
524 content = self._validate_input_new(content, session["force"])
525 self.check_conflict_on_new(session, content)
526 # self.format_on_new(content, session["project_id"], make_public=session["public"])
527 _id = self.auth.create_user(content["username"], content["password"])["_id"]
528
529 if "project_role_mappings" in content.keys():
530 for mapping in content["project_role_mappings"]:
531 self.auth.assign_role_to_user(_id, mapping["project"], mapping["role"])
532
533 rollback.append({"topic": self.topic, "_id": _id})
534 # del content["password"]
535 # self._send_msg("create", content)
536 return _id
537 except ValidationError as e:
538 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
539
540 def show(self, session, _id):
541 """
542 Get complete information on an topic
543
544 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
545 :param _id: server internal id
546 :return: dictionary, raise exception if not found.
547 """
548 # Allow _id to be a name or uuid
549 filter_q = {self.id_field(self.topic, _id): _id}
550 users = self.auth.get_user_list(filter_q)
551
552 if len(users) == 1:
553 return self.format_on_show(users[0])
554 elif len(users) > 1:
555 raise EngineException("Too many users found", HTTPStatus.CONFLICT)
556 else:
557 raise EngineException("User not found", HTTPStatus.NOT_FOUND)
558
559 def edit(self, session, _id, indata=None, kwargs=None, content=None):
560 """
561 Updates an user entry.
562
563 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
564 :param _id:
565 :param indata: data to be inserted
566 :param kwargs: used to override the indata descriptor
567 :param content:
568 :return: _id: identity of the inserted data.
569 """
570 indata = self._remove_envelop(indata)
571
572 # Override descriptor with query string kwargs
573 if kwargs:
574 BaseTopic._update_input_with_kwargs(indata, kwargs)
575 try:
576 indata = self._validate_input_edit(indata, force=session["force"])
577
578 if not content:
579 content = self.show(session, _id)
580 self.check_conflict_on_edit(session, content, indata, _id=_id)
581 # self.format_on_edit(content, indata)
582
583 if "password" in indata or "username" in indata:
584 self.auth.update_user(_id, new_name=indata.get("username"), new_password=indata.get("password"))
585 if not indata.get("remove_project_role_mappings") and not indata.get("add_project_role_mappings") and \
586 not indata.get("project_role_mappings"):
587 return _id
588 if indata.get("project_role_mappings") and \
589 (indata.get("remove_project_role_mappings") or indata.get("add_project_role_mappings")):
590 raise EngineException("Option 'project_role_mappings' is incompatible with 'add_project_role_mappings"
591 "' or 'remove_project_role_mappings'", http_code=HTTPStatus.BAD_REQUEST)
592
593 user = self.show(session, _id)
594 original_mapping = user["project_role_mappings"]
595
596 mappings_to_add = []
597 mappings_to_remove = []
598
599 # remove
600 for to_remove in indata.get("remove_project_role_mappings", ()):
601 for mapping in original_mapping:
602 if to_remove["project"] in (mapping["project"], mapping["project_name"]):
603 if not to_remove.get("role") or to_remove["role"] in (mapping["role"], mapping["role_name"]):
604 mappings_to_remove.append(mapping)
605
606 # add
607 for to_add in indata.get("add_project_role_mappings", ()):
608 for mapping in original_mapping:
609 if to_add["project"] in (mapping["project"], mapping["project_name"]) and \
610 to_add["role"] in (mapping["role"], mapping["role_name"]):
611
612 if mapping in mappings_to_remove: # do not remove
613 mappings_to_remove.remove(mapping)
614 break # do not add, it is already at user
615 else:
616 mappings_to_add.append(to_add)
617
618 # set
619 if indata.get("project_role_mappings"):
620 for to_set in indata["project_role_mappings"]:
621 for mapping in original_mapping:
622 if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
623 to_set["role"] in (mapping["role"], mapping["role_name"]):
624
625 if mapping in mappings_to_remove: # do not remove
626 mappings_to_remove.remove(mapping)
627 break # do not add, it is already at user
628 else:
629 mappings_to_add.append(to_set)
630 for mapping in original_mapping:
631 for to_set in indata["project_role_mappings"]:
632 if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
633 to_set["role"] in (mapping["role"], mapping["role_name"]):
634 break
635 else:
636 # delete
637 if mapping not in mappings_to_remove: # do not remove
638 mappings_to_remove.append(mapping)
639
640 for mapping in mappings_to_remove:
641 self.auth.remove_role_from_user(
642 _id,
643 mapping["project"],
644 mapping["role"]
645 )
646
647 for mapping in mappings_to_add:
648 self.auth.assign_role_to_user(
649 _id,
650 mapping["project"],
651 mapping["role"]
652 )
653
654 return "_id"
655 except ValidationError as e:
656 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
657
658 def list(self, session, filter_q=None):
659 """
660 Get a list of the topic that matches a filter
661 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
662 :param filter_q: filter of data to be applied
663 :return: The list, it can be empty if no one match the filter.
664 """
665 users = [self.format_on_show(user) for user in self.auth.get_user_list(filter_q)]
666
667 return users
668
669 def delete(self, session, _id, dry_run=False):
670 """
671 Delete item by its internal _id
672
673 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
674 :param _id: server internal id
675 :param force: indicates if deletion must be forced in case of conflict
676 :param dry_run: make checking but do not delete
677 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
678 """
679 # Allow _id to be a name or uuid
680 filter_q = {self.id_field(self.topic, _id): _id}
681 user_list = self.auth.get_user_list(filter_q)
682 if not user_list:
683 raise EngineException("User '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
684 _id = user_list[0]["_id"]
685 self.check_conflict_on_del(session, _id, user_list[0])
686 if not dry_run:
687 v = self.auth.delete_user(_id)
688 return v
689 return None
690
691
692 class ProjectTopicAuth(ProjectTopic):
693 # topic = "projects"
694 # topic_msg = "projects"
695 schema_new = project_new_schema
696 schema_edit = project_edit_schema
697
698 def __init__(self, db, fs, msg, auth):
699 ProjectTopic.__init__(self, db, fs, msg)
700 self.auth = auth
701
702 def check_conflict_on_new(self, session, indata):
703 """
704 Check that the data to be inserted is valid
705
706 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
707 :param indata: data to be inserted
708 :return: None or raises EngineException
709 """
710 project_name = indata.get("name")
711 if is_valid_uuid(project_name):
712 raise EngineException("project name '{}' cannot have an uuid format".format(project_name),
713 HTTPStatus.UNPROCESSABLE_ENTITY)
714
715 project_list = self.auth.get_project_list(filter_q={"name": project_name})
716
717 if project_list:
718 raise EngineException("project '{}' exists".format(project_name), HTTPStatus.CONFLICT)
719
720 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
721 """
722 Check that the data to be edited/uploaded is valid
723
724 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
725 :param final_content: data once modified
726 :param edit_content: incremental data that contains the modifications to apply
727 :param _id: internal _id
728 :return: None or raises EngineException
729 """
730
731 project_name = edit_content.get("name")
732 if project_name:
733 if is_valid_uuid(project_name):
734 raise EngineException("project name '{}' cannot be an uuid format".format(project_name),
735 HTTPStatus.UNPROCESSABLE_ENTITY)
736
737 # Check that project name is not used, regardless keystone already checks this
738 if self.auth.get_project_list(filter_q={"name": project_name}):
739 raise EngineException("project '{}' is already used".format(project_name), HTTPStatus.CONFLICT)
740
741 def check_conflict_on_del(self, session, _id, db_content):
742 """
743 Check if deletion can be done because of dependencies if it is not force. To override
744
745 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
746 :param _id: internal _id
747 :param db_content: The database content of this item _id
748 :return: None if ok or raises EngineException with the conflict
749 """
750 # projects = self.auth.get_project_list()
751 # current_project = [project for project in projects
752 # if project["name"] in session["project_id"]][0]
753 # TODO check that any user is using this project, raise CONFLICT exception
754 if _id == session["project_id"]:
755 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
756
757 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
758 """
759 Creates a new entry into the authentication backend.
760
761 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
762
763 :param rollback: list to append created items at database in case a rollback may to be done
764 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
765 :param indata: data to be inserted
766 :param kwargs: used to override the indata descriptor
767 :param headers: http request headers
768 :return: _id: identity of the inserted data.
769 """
770 try:
771 content = BaseTopic._remove_envelop(indata)
772
773 # Override descriptor with query string kwargs
774 BaseTopic._update_input_with_kwargs(content, kwargs)
775 content = self._validate_input_new(content, session["force"])
776 self.check_conflict_on_new(session, content)
777 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
778 _id = self.auth.create_project(content["name"])
779 rollback.append({"topic": self.topic, "_id": _id})
780 # self._send_msg("create", content)
781 return _id
782 except ValidationError as e:
783 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
784
785 def show(self, session, _id):
786 """
787 Get complete information on an topic
788
789 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
790 :param _id: server internal id
791 :return: dictionary, raise exception if not found.
792 """
793 # Allow _id to be a name or uuid
794 filter_q = {self.id_field(self.topic, _id): _id}
795 projects = self.auth.get_project_list(filter_q=filter_q)
796
797 if len(projects) == 1:
798 return projects[0]
799 elif len(projects) > 1:
800 raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
801 else:
802 raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
803
804 def list(self, session, filter_q=None):
805 """
806 Get a list of the topic that matches a filter
807
808 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
809 :param filter_q: filter of data to be applied
810 :return: The list, it can be empty if no one match the filter.
811 """
812 return self.auth.get_project_list(filter_q)
813
814 def delete(self, session, _id, dry_run=False):
815 """
816 Delete item by its internal _id
817
818 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
819 :param _id: server internal id
820 :param dry_run: make checking but do not delete
821 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
822 """
823 # Allow _id to be a name or uuid
824 filter_q = {self.id_field(self.topic, _id): _id}
825 project_list = self.auth.get_project_list(filter_q)
826 if not project_list:
827 raise EngineException("Project '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
828 _id = project_list[0]["_id"]
829 self.check_conflict_on_del(session, _id, project_list[0])
830 if not dry_run:
831 v = self.auth.delete_project(_id)
832 return v
833 return None
834
835 def edit(self, session, _id, indata=None, kwargs=None, content=None):
836 """
837 Updates a project entry.
838
839 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
840 :param _id:
841 :param indata: data to be inserted
842 :param kwargs: used to override the indata descriptor
843 :param content:
844 :return: _id: identity of the inserted data.
845 """
846 indata = self._remove_envelop(indata)
847
848 # Override descriptor with query string kwargs
849 if kwargs:
850 BaseTopic._update_input_with_kwargs(indata, kwargs)
851 try:
852 indata = self._validate_input_edit(indata, force=session["force"])
853
854 if not content:
855 content = self.show(session, _id)
856 self.check_conflict_on_edit(session, content, indata, _id=_id)
857 # self.format_on_edit(content, indata)
858
859 if "name" in indata:
860 self.auth.update_project(content["_id"], indata["name"])
861 except ValidationError as e:
862 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
863
864
865 class RoleTopicAuth(BaseTopic):
866 topic = "roles"
867 topic_msg = None # "roles"
868 schema_new = roles_new_schema
869 schema_edit = roles_edit_schema
870 multiproject = False
871
872 def __init__(self, db, fs, msg, auth, ops):
873 BaseTopic.__init__(self, db, fs, msg)
874 self.auth = auth
875 self.operations = ops
876 self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles"
877
878 @staticmethod
879 def validate_role_definition(operations, role_definitions):
880 """
881 Validates the role definition against the operations defined in
882 the resources to operations files.
883
884 :param operations: operations list
885 :param role_definitions: role definition to test
886 :return: None if ok, raises ValidationError exception on error
887 """
888 if not role_definitions.get("permissions"):
889 return
890 ignore_fields = ["admin", "default"]
891 for role_def in role_definitions["permissions"].keys():
892 if role_def in ignore_fields:
893 continue
894 if role_def[-1] == ":":
895 raise ValidationError("Operation cannot end with ':'")
896
897 role_def_matches = [op for op in operations if op.startswith(role_def)]
898
899 if len(role_def_matches) == 0:
900 raise ValidationError("Invalid permission '{}'".format(role_def))
901
902 def _validate_input_new(self, input, force=False):
903 """
904 Validates input user content for a new entry.
905
906 :param input: user input content for the new topic
907 :param force: may be used for being more tolerant
908 :return: The same input content, or a changed version of it.
909 """
910 if self.schema_new:
911 validate_input(input, self.schema_new)
912 self.validate_role_definition(self.operations, input)
913
914 return input
915
916 def _validate_input_edit(self, input, force=False):
917 """
918 Validates input user content for updating an entry.
919
920 :param input: user input content for the new topic
921 :param force: may be used for being more tolerant
922 :return: The same input content, or a changed version of it.
923 """
924 if self.schema_edit:
925 validate_input(input, self.schema_edit)
926 self.validate_role_definition(self.operations, input)
927
928 return input
929
930 def check_conflict_on_new(self, session, indata):
931 """
932 Check that the data to be inserted is valid
933
934 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
935 :param indata: data to be inserted
936 :return: None or raises EngineException
937 """
938 # check name not exists
939 if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
940 raise EngineException("role name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT)
941
942 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
943 """
944 Check that the data to be edited/uploaded is valid
945
946 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
947 :param final_content: data once modified
948 :param edit_content: incremental data that contains the modifications to apply
949 :param _id: internal _id
950 :return: None or raises EngineException
951 """
952 if "default" not in final_content["permissions"]:
953 final_content["permissions"]["default"] = False
954 if "admin" not in final_content["permissions"]:
955 final_content["permissions"]["admin"] = False
956
957 # check name not exists
958 if "name" in edit_content:
959 role_name = edit_content["name"]
960 if self.db.get_one(self.topic, {"name": role_name, "_id.ne": _id}, fail_on_empty=False, fail_on_more=False):
961 raise EngineException("role name '{}' exists".format(role_name), HTTPStatus.CONFLICT)
962
963 def check_conflict_on_del(self, session, _id, db_content):
964 """
965 Check if deletion can be done because of dependencies if it is not force. To override
966
967 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
968 :param _id: internal _id
969 :param db_content: The database content of this item _id
970 :return: None if ok or raises EngineException with the conflict
971 """
972 roles = self.auth.get_role_list()
973 system_admin_roles = [role for role in roles if role["name"] == "system_admin"]
974
975 if system_admin_roles and _id == system_admin_roles[0]["_id"]:
976 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
977
978 @staticmethod
979 def format_on_new(content, project_id=None, make_public=False):
980 """
981 Modifies content descriptor to include _admin
982
983 :param content: descriptor to be modified
984 :param project_id: if included, it add project read/write permissions
985 :param make_public: if included it is generated as public for reading.
986 :return: None, but content is modified
987 """
988 now = time()
989 if "_admin" not in content:
990 content["_admin"] = {}
991 if not content["_admin"].get("created"):
992 content["_admin"]["created"] = now
993 content["_admin"]["modified"] = now
994
995 if "permissions" not in content:
996 content["permissions"] = {}
997
998 if "default" not in content["permissions"]:
999 content["permissions"]["default"] = False
1000 if "admin" not in content["permissions"]:
1001 content["permissions"]["admin"] = False
1002
1003 @staticmethod
1004 def format_on_edit(final_content, edit_content):
1005 """
1006 Modifies final_content descriptor to include the modified date.
1007
1008 :param final_content: final descriptor generated
1009 :param edit_content: alterations to be include
1010 :return: None, but final_content is modified
1011 """
1012 final_content["_admin"]["modified"] = time()
1013
1014 if "permissions" not in final_content:
1015 final_content["permissions"] = {}
1016
1017 if "default" not in final_content["permissions"]:
1018 final_content["permissions"]["default"] = False
1019 if "admin" not in final_content["permissions"]:
1020 final_content["permissions"]["admin"] = False
1021
1022 # @staticmethod
1023 # def format_on_show(content):
1024 # """
1025 # Modifies the content of the role information to separate the role
1026 # metadata from the role definition. Eases the reading process of the
1027 # role definition.
1028 #
1029 # :param definition: role definition to be processed
1030 # """
1031 # content["_id"] = str(content["_id"])
1032 #
1033 # def show(self, session, _id):
1034 # """
1035 # Get complete information on an topic
1036 #
1037 # :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1038 # :param _id: server internal id
1039 # :return: dictionary, raise exception if not found.
1040 # """
1041 # filter_db = {"_id": _id}
1042 # filter_db = { BaseTopic.id_field(self.topic, _id): _id } # To allow role addressing by name
1043 #
1044 # role = self.db.get_one(self.topic, filter_db)
1045 # new_role = dict(role)
1046 # self.format_on_show(new_role)
1047 #
1048 # return new_role
1049
1050 # def list(self, session, filter_q=None):
1051 # """
1052 # Get a list of the topic that matches a filter
1053 #
1054 # :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1055 # :param filter_q: filter of data to be applied
1056 # :return: The list, it can be empty if no one match the filter.
1057 # """
1058 # if not filter_q:
1059 # filter_q = {}
1060 #
1061 # if ":" in filter_q:
1062 # filter_q["root"] = filter_q[":"]
1063 #
1064 # for key in filter_q.keys():
1065 # if key == "name":
1066 # continue
1067 # filter_q[key] = filter_q[key] in ["True", "true"]
1068 #
1069 # roles = self.db.get_list(self.topic, filter_q)
1070 # new_roles = []
1071 #
1072 # for role in roles:
1073 # new_role = dict(role)
1074 # self.format_on_show(new_role)
1075 # new_roles.append(new_role)
1076 #
1077 # return new_roles
1078
1079 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1080 """
1081 Creates a new entry into database.
1082
1083 :param rollback: list to append created items at database in case a rollback may to be done
1084 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1085 :param indata: data to be inserted
1086 :param kwargs: used to override the indata descriptor
1087 :param headers: http request headers
1088 :return: _id: identity of the inserted data.
1089 """
1090 try:
1091 content = self._remove_envelop(indata)
1092
1093 # Override descriptor with query string kwargs
1094 self._update_input_with_kwargs(content, kwargs)
1095 content = self._validate_input_new(content, session["force"])
1096 self.check_conflict_on_new(session, content)
1097 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
1098 role_name = content["name"]
1099 role_id = self.auth.create_role(role_name)
1100 content["_id"] = role_id
1101 _id = self.db.create(self.topic, content)
1102 rollback.append({"topic": self.topic, "_id": _id})
1103 # self._send_msg("create", content)
1104 return _id
1105 except ValidationError as e:
1106 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1107
1108 def delete(self, session, _id, dry_run=False):
1109 """
1110 Delete item by its internal _id
1111
1112 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1113 :param _id: server internal id
1114 :param dry_run: make checking but do not delete
1115 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1116 """
1117 self.check_conflict_on_del(session, _id, None)
1118 # filter_q = {"_id": _id}
1119 filter_q = {BaseTopic.id_field(self.topic, _id): _id} # To allow role addressing by name
1120 if not dry_run:
1121 self.auth.delete_role(_id)
1122 v = self.db.del_one(self.topic, filter_q)
1123 return v
1124 return None
1125
1126 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1127 """
1128 Updates a role entry.
1129
1130 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1131 :param _id:
1132 :param indata: data to be inserted
1133 :param kwargs: used to override the indata descriptor
1134 :param content:
1135 :return: _id: identity of the inserted data.
1136 """
1137 _id = super().edit(session, _id, indata, kwargs, content)
1138 if indata.get("name"):
1139 self.auth.update_role(_id, name=indata.get("name"))