85fe9ff20141666c79ede243ff6b93f6e247e06b
1 # -*- coding: utf-8 -*-
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 from uuid
import uuid4
18 from hashlib
import sha256
19 from http
import HTTPStatus
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
30 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
33 class UserTopic(BaseTopic
):
36 schema_new
= user_new_schema
37 schema_edit
= user_edit_schema
40 def __init__(self
, db
, fs
, msg
):
41 BaseTopic
.__init
__(self
, db
, fs
, msg
)
44 def _get_project_filter(session
):
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"
51 if session
["admin"]: # allows all
54 return {"username": session
["username"]}
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
)
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,
66 raise EngineException("project '{}' does not exist".format(p
), HTTPStatus
.CONFLICT
)
68 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
76 if _id
== session
["username"]:
77 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
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"]
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"]]
91 if content
.get("projects"):
92 content
["projects"] += projects
94 content
["projects"] = projects
97 def format_on_edit(final_content
, edit_content
):
98 BaseTopic
.format_on_edit(final_content
, edit_content
)
99 if edit_content
.get("password"):
101 final_content
["_admin"]["salt"] = salt
102 final_content
["password"] = sha256(edit_content
["password"].encode('utf-8') +
103 salt
.encode('utf-8')).hexdigest()
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
)
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
)
126 class ProjectTopic(BaseTopic
):
128 topic_msg
= "projects"
129 schema_new
= project_new_schema
130 schema_edit
= project_edit_schema
133 def __init__(self
, db
, fs
, msg
):
134 BaseTopic
.__init
__(self
, db
, fs
, msg
)
137 def _get_project_filter(session
):
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"
144 if session
["admin"]: # allows all
147 return {"_id.cont": session
["project_id"]}
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
)
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"]
162 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
170 if _id
in session
["project_id"]:
171 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
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
)
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
)
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
)
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")
207 def __init__(self
, db
, fs
, msg
):
208 BaseTopic
.__init
__(self
, db
, fs
, msg
)
210 def check_conflict_on_new(self
, session
, indata
):
211 self
.check_unique_name(session
, indata
["name"], _id
=None)
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
)
218 schema_version
= final_content
.get("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
)
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"
234 if content
.get("vim_password"):
235 content
["vim_password"] = self
.db
.encrypt(content
["vim_password"], schema_version
=schema_version
,
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
,
243 content
["_admin"]["operationalState"] = "PROCESSING"
245 def delete(self
, session
, _id
, dry_run
=False):
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, ...
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
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
269 wim_config_encrypted
= ()
271 def __init__(self
, db
, fs
, msg
):
272 BaseTopic
.__init
__(self
, db
, fs
, msg
)
274 def check_conflict_on_new(self
, session
, indata
):
275 self
.check_unique_name(session
, indata
["name"], _id
=None)
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
)
282 schema_version
= final_content
.get("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
)
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"
298 if content
.get("wim_password"):
299 content
["wim_password"] = self
.db
.encrypt(content
["wim_password"], schema_version
=schema_version
,
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
,
307 content
["_admin"]["operationalState"] = "PROCESSING"
309 def delete(self
, session
, _id
, dry_run
=False):
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, ...
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
327 class SdnTopic(BaseTopic
):
330 schema_new
= sdn_new_schema
331 schema_edit
= sdn_edit_schema
334 def __init__(self
, db
, fs
, msg
):
335 BaseTopic
.__init
__(self
, db
, fs
, msg
)
337 def check_conflict_on_new(self
, session
, indata
):
338 self
.check_unique_name(session
, indata
["name"], _id
=None)
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
)
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
,
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"
354 if content
.get("password"):
355 content
["password"] = self
.db
.encrypt(content
["password"], schema_version
=schema_version
,
358 content
["_admin"]["operationalState"] = "PROCESSING"
360 def delete(self
, session
, _id
, dry_run
=False):
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, ...
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
377 class UserTopicAuth(UserTopic
):
379 # topic_msg = "users"
380 schema_new
= user_new_schema
381 schema_edit
= user_edit_schema
383 def __init__(self
, db
, fs
, msg
, auth
):
384 UserTopic
.__init
__(self
, db
, fs
, msg
)
387 def check_conflict_on_new(self
, session
, indata
):
389 Check that the data to be inserted is valid
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
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
)
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
)
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)
413 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
415 Check that the data to be edited/uploaded is valid
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
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
)
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
)
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
)
441 def check_conflict_on_del(self
, session
, _id
, db_content
):
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
449 if db_content
["username"] == session
["username"]:
450 raise EngineException("You cannot delete your own login user ", http_code
=HTTPStatus
.CONFLICT
)
453 # def format_on_new(content, project_id=None, make_public=False):
455 # Modifies content descriptor to include _id.
457 # NOTE: No password salt required because the authentication backend
458 # should handle these security concerns.
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
464 # BaseTopic.format_on_new(content, make_public=False)
465 # content["_id"] = content["username"]
466 # content["password"] = content["password"]
469 # def format_on_edit(final_content, edit_content):
471 # Modifies final_content descriptor to include the modified date.
473 # NOTE: No password salt required because the authentication backend
474 # should handle these security concerns.
476 # :param final_content: final descriptor generated
477 # :param edit_content: alterations to be include
478 # :return: None, but final_content is modified
480 # BaseTopic.format_on_edit(final_content, edit_content)
481 # if "password" in edit_content:
482 # final_content["password"] = edit_content["password"]
484 # final_content["project_role_mappings"] = edit_content["project_role_mappings"]
487 def format_on_show(content
):
489 Modifies the content of the role information to separate the role
490 metadata from the role definition.
492 project_role_mappings
= []
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"],
499 "role_name": role
["name"]})
501 del content
["projects"]
502 content
["project_role_mappings"] = project_role_mappings
506 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
508 Creates a new entry into the authentication backend.
510 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
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.
520 content
= BaseTopic
._remove
_envelop
(indata
)
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"]
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"])
533 rollback
.append({"topic": self
.topic
, "_id": _id
})
534 # del content["password"]
535 # self._send_msg("create", content)
537 except ValidationError
as e
:
538 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
540 def show(self
, session
, _id
):
542 Get complete information on an topic
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.
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
)
553 return self
.format_on_show(users
[0])
555 raise EngineException("Too many users found", HTTPStatus
.CONFLICT
)
557 raise EngineException("User not found", HTTPStatus
.NOT_FOUND
)
559 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
561 Updates an user entry.
563 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
565 :param indata: data to be inserted
566 :param kwargs: used to override the indata descriptor
568 :return: _id: identity of the inserted data.
570 indata
= self
._remove
_envelop
(indata
)
572 # Override descriptor with query string kwargs
574 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
576 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
579 content
= self
.show(session
, _id
)
580 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
581 # self.format_on_edit(content, indata)
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"):
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
)
593 user
= self
.show(session
, _id
)
594 original_mapping
= user
["project_role_mappings"]
597 mappings_to_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
)
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"]):
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
616 mappings_to_add
.append(to_add
)
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"]):
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
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"]):
637 if mapping
not in mappings_to_remove
: # do not remove
638 mappings_to_remove
.append(mapping
)
640 for mapping
in mappings_to_remove
:
641 self
.auth
.remove_role_from_user(
647 for mapping
in mappings_to_add
:
648 self
.auth
.assign_role_to_user(
655 except ValidationError
as e
:
656 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
658 def list(self
, session
, filter_q
=None):
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.
665 users
= [self
.format_on_show(user
) for user
in self
.auth
.get_user_list(filter_q
)]
669 def delete(self
, session
, _id
, dry_run
=False):
671 Delete item by its internal _id
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, ...
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
)
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])
687 v
= self
.auth
.delete_user(_id
)
692 class ProjectTopicAuth(ProjectTopic
):
694 # topic_msg = "projects"
695 schema_new
= project_new_schema
696 schema_edit
= project_edit_schema
698 def __init__(self
, db
, fs
, msg
, auth
):
699 ProjectTopic
.__init
__(self
, db
, fs
, msg
)
702 def check_conflict_on_new(self
, session
, indata
):
704 Check that the data to be inserted is valid
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
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
)
715 project_list
= self
.auth
.get_project_list(filter_q
={"name": project_name
})
718 raise EngineException("project '{}' exists".format(project_name
), HTTPStatus
.CONFLICT
)
720 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
722 Check that the data to be edited/uploaded is valid
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
731 project_name
= edit_content
.get("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
)
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
)
741 def check_conflict_on_del(self
, session
, _id
, db_content
):
743 Check if deletion can be done because of dependencies if it is not force. To override
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
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
)
757 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
759 Creates a new entry into the authentication backend.
761 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
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.
771 content
= BaseTopic
._remove
_envelop
(indata
)
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)
782 except ValidationError
as e
:
783 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
785 def show(self
, session
, _id
):
787 Get complete information on an topic
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.
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
)
797 if len(projects
) == 1:
799 elif len(projects
) > 1:
800 raise EngineException("Too many projects found", HTTPStatus
.CONFLICT
)
802 raise EngineException("Project not found", HTTPStatus
.NOT_FOUND
)
804 def list(self
, session
, filter_q
=None):
806 Get a list of the topic that matches a filter
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.
812 return self
.auth
.get_project_list(filter_q
)
814 def delete(self
, session
, _id
, dry_run
=False):
816 Delete item by its internal _id
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, ...
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
)
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])
831 v
= self
.auth
.delete_project(_id
)
835 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
837 Updates a project entry.
839 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
841 :param indata: data to be inserted
842 :param kwargs: used to override the indata descriptor
844 :return: _id: identity of the inserted data.
846 indata
= self
._remove
_envelop
(indata
)
848 # Override descriptor with query string kwargs
850 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
852 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
855 content
= self
.show(session
, _id
)
856 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
857 # self.format_on_edit(content, indata)
860 self
.auth
.update_project(content
["_id"], indata
["name"])
861 except ValidationError
as e
:
862 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
865 class RoleTopicAuth(BaseTopic
):
867 topic_msg
= None # "roles"
868 schema_new
= roles_new_schema
869 schema_edit
= roles_edit_schema
872 def __init__(self
, db
, fs
, msg
, auth
, ops
):
873 BaseTopic
.__init
__(self
, db
, fs
, msg
)
875 self
.operations
= ops
876 self
.topic
= "roles_operations" if isinstance(auth
, AuthconnKeystone
) else "roles"
879 def validate_role_definition(operations
, role_definitions
):
881 Validates the role definition against the operations defined in
882 the resources to operations files.
884 :param operations: operations list
885 :param role_definitions: role definition to test
886 :return: None if ok, raises ValidationError exception on error
888 if not role_definitions
.get("permissions"):
890 ignore_fields
= ["admin", "default"]
891 for role_def
in role_definitions
["permissions"].keys():
892 if role_def
in ignore_fields
:
894 if role_def
[-1] == ":":
895 raise ValidationError("Operation cannot end with ':'")
897 role_def_matches
= [op
for op
in operations
if op
.startswith(role_def
)]
899 if len(role_def_matches
) == 0:
900 raise ValidationError("Invalid permission '{}'".format(role_def
))
902 def _validate_input_new(self
, input, force
=False):
904 Validates input user content for a new entry.
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.
911 validate_input(input, self
.schema_new
)
912 self
.validate_role_definition(self
.operations
, input)
916 def _validate_input_edit(self
, input, force
=False):
918 Validates input user content for updating an entry.
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.
925 validate_input(input, self
.schema_edit
)
926 self
.validate_role_definition(self
.operations
, input)
930 def check_conflict_on_new(self
, session
, indata
):
932 Check that the data to be inserted is valid
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
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
)
942 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
944 Check that the data to be edited/uploaded is valid
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
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
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
)
963 def check_conflict_on_del(self
, session
, _id
, db_content
):
965 Check if deletion can be done because of dependencies if it is not force. To override
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
972 roles
= self
.auth
.get_role_list()
973 system_admin_roles
= [role
for role
in roles
if role
["name"] == "system_admin"]
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
)
979 def format_on_new(content
, project_id
=None, make_public
=False):
981 Modifies content descriptor to include _admin
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
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
995 if "permissions" not in content
:
996 content
["permissions"] = {}
998 if "default" not in content
["permissions"]:
999 content
["permissions"]["default"] = False
1000 if "admin" not in content
["permissions"]:
1001 content
["permissions"]["admin"] = False
1004 def format_on_edit(final_content
, edit_content
):
1006 Modifies final_content descriptor to include the modified date.
1008 :param final_content: final descriptor generated
1009 :param edit_content: alterations to be include
1010 :return: None, but final_content is modified
1012 final_content
["_admin"]["modified"] = time()
1014 if "permissions" not in final_content
:
1015 final_content
["permissions"] = {}
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
1023 # def format_on_show(content):
1025 # Modifies the content of the role information to separate the role
1026 # metadata from the role definition. Eases the reading process of the
1029 # :param definition: role definition to be processed
1031 # content["_id"] = str(content["_id"])
1033 # def show(self, session, _id):
1035 # Get complete information on an topic
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.
1041 # filter_db = {"_id": _id}
1042 # filter_db = { BaseTopic.id_field(self.topic, _id): _id } # To allow role addressing by name
1044 # role = self.db.get_one(self.topic, filter_db)
1045 # new_role = dict(role)
1046 # self.format_on_show(new_role)
1050 # def list(self, session, filter_q=None):
1052 # Get a list of the topic that matches a filter
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.
1061 # if ":" in filter_q:
1062 # filter_q["root"] = filter_q[":"]
1064 # for key in filter_q.keys():
1067 # filter_q[key] = filter_q[key] in ["True", "true"]
1069 # roles = self.db.get_list(self.topic, filter_q)
1072 # for role in roles:
1073 # new_role = dict(role)
1074 # self.format_on_show(new_role)
1075 # new_roles.append(new_role)
1079 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
1081 Creates a new entry into database.
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.
1091 content
= self
._remove
_envelop
(indata
)
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)
1105 except ValidationError
as e
:
1106 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
1108 def delete(self
, session
, _id
, dry_run
=False):
1110 Delete item by its internal _id
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, ...
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
1121 self
.auth
.delete_role(_id
)
1122 v
= self
.db
.del_one(self
.topic
, filter_q
)
1126 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
1128 Updates a role entry.
1130 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1132 :param indata: data to be inserted
1133 :param kwargs: used to override the indata descriptor
1135 :return: _id: identity of the inserted data.
1137 _id
= super().edit(session
, _id
, indata
, kwargs
, content
)
1138 if indata
.get("name"):
1139 self
.auth
.update_role(_id
, name
=indata
.get("name"))