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
29 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
32 class UserTopic(BaseTopic
):
35 schema_new
= user_new_schema
36 schema_edit
= user_edit_schema
39 def __init__(self
, db
, fs
, msg
):
40 BaseTopic
.__init
__(self
, db
, fs
, msg
)
43 def _get_project_filter(session
):
45 Generates a filter dictionary for querying database users.
46 Current policy is admin can show all, non admin, only its own user.
47 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
50 if session
["admin"]: # allows all
53 return {"username": session
["username"]}
55 def check_conflict_on_new(self
, session
, indata
):
56 # check username not exists
57 if self
.db
.get_one(self
.topic
, {"username": indata
.get("username")}, fail_on_empty
=False, fail_on_more
=False):
58 raise EngineException("username '{}' exists".format(indata
["username"]), HTTPStatus
.CONFLICT
)
60 if not session
["force"]:
61 for p
in indata
.get("projects"):
62 # To allow project addressing by Name as well as ID
63 if not self
.db
.get_one("projects", {BaseTopic
.id_field("projects", p
): p
}, fail_on_empty
=False,
65 raise EngineException("project '{}' does not exist".format(p
), HTTPStatus
.CONFLICT
)
67 def check_conflict_on_del(self
, session
, _id
, db_content
):
69 Check if deletion can be done because of dependencies if it is not force. To override
70 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
71 :param _id: internal _id
72 :param db_content: The database content of this item _id
73 :return: None if ok or raises EngineException with the conflict
75 if _id
== session
["username"]:
76 raise EngineException("You cannot delete your own user", http_code
=HTTPStatus
.CONFLICT
)
79 def format_on_new(content
, project_id
=None, make_public
=False):
80 BaseTopic
.format_on_new(content
, make_public
=False)
81 # Removed so that the UUID is kept, to allow User Name modification
82 # content["_id"] = content["username"]
84 content
["_admin"]["salt"] = salt
85 if content
.get("password"):
86 content
["password"] = sha256(content
["password"].encode('utf-8') + salt
.encode('utf-8')).hexdigest()
87 if content
.get("project_role_mappings"):
88 projects
= [mapping
[0] for mapping
in content
["project_role_mappings"]]
90 if content
.get("projects"):
91 content
["projects"] += projects
93 content
["projects"] = projects
96 def format_on_edit(final_content
, edit_content
):
97 BaseTopic
.format_on_edit(final_content
, edit_content
)
98 if edit_content
.get("password"):
100 final_content
["_admin"]["salt"] = salt
101 final_content
["password"] = sha256(edit_content
["password"].encode('utf-8') +
102 salt
.encode('utf-8')).hexdigest()
104 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
105 if not session
["admin"]:
106 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
107 # Names that look like UUIDs are not allowed
108 name
= (indata
if indata
else kwargs
).get("username")
109 if is_valid_uuid(name
):
110 raise EngineException("Usernames that look like UUIDs are not allowed",
111 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
112 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, content
=content
)
114 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
115 if not session
["admin"]:
116 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
117 # Names that look like UUIDs are not allowed
118 name
= indata
["username"] if indata
else kwargs
["username"]
119 if is_valid_uuid(name
):
120 raise EngineException("Usernames that look like UUIDs are not allowed",
121 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
122 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
)
125 class ProjectTopic(BaseTopic
):
127 topic_msg
= "projects"
128 schema_new
= project_new_schema
129 schema_edit
= project_edit_schema
132 def __init__(self
, db
, fs
, msg
):
133 BaseTopic
.__init
__(self
, db
, fs
, msg
)
136 def _get_project_filter(session
):
138 Generates a filter dictionary for querying database users.
139 Current policy is admin can show all, non admin, only its own user.
140 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
143 if session
["admin"]: # allows all
146 return {"_id.cont": session
["project_id"]}
148 def check_conflict_on_new(self
, session
, indata
):
149 if not indata
.get("name"):
150 raise EngineException("missing 'name'")
151 # check name not exists
152 if self
.db
.get_one(self
.topic
, {"name": indata
.get("name")}, fail_on_empty
=False, fail_on_more
=False):
153 raise EngineException("name '{}' exists".format(indata
["name"]), HTTPStatus
.CONFLICT
)
156 def format_on_new(content
, project_id
=None, make_public
=False):
157 BaseTopic
.format_on_new(content
, None)
158 # Removed so that the UUID is kept, to allow Project Name modification
159 # content["_id"] = content["name"]
161 def check_conflict_on_del(self
, session
, _id
, db_content
):
163 Check if deletion can be done because of dependencies if it is not force. To override
164 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
165 :param _id: internal _id
166 :param db_content: The database content of this item _id
167 :return: None if ok or raises EngineException with the conflict
169 if _id
in session
["project_id"]:
170 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
173 _filter
= {"projects": _id
}
174 if self
.db
.get_list("users", _filter
):
175 raise EngineException("There is some USER that contains this project", http_code
=HTTPStatus
.CONFLICT
)
177 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
178 if not session
["admin"]:
179 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
180 # Names that look like UUIDs are not allowed
181 name
= (indata
if indata
else kwargs
).get("name")
182 if is_valid_uuid(name
):
183 raise EngineException("Project names that look like UUIDs are not allowed",
184 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
185 return BaseTopic
.edit(self
, session
, _id
, indata
=indata
, kwargs
=kwargs
, content
=content
)
187 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
188 if not session
["admin"]:
189 raise EngineException("needed admin privileges", http_code
=HTTPStatus
.UNAUTHORIZED
)
190 # Names that look like UUIDs are not allowed
191 name
= indata
["name"] if indata
else kwargs
["name"]
192 if is_valid_uuid(name
):
193 raise EngineException("Project names that look like UUIDs are not allowed",
194 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
195 return BaseTopic
.new(self
, rollback
, session
, indata
=indata
, kwargs
=kwargs
, headers
=headers
)
198 class VimAccountTopic(BaseTopic
):
199 topic
= "vim_accounts"
200 topic_msg
= "vim_account"
201 schema_new
= vim_account_new_schema
202 schema_edit
= vim_account_edit_schema
203 vim_config_encrypted
= ("admin_password", "nsx_password", "vcenter_password")
206 def __init__(self
, db
, fs
, msg
):
207 BaseTopic
.__init
__(self
, db
, fs
, msg
)
209 def check_conflict_on_new(self
, session
, indata
):
210 self
.check_unique_name(session
, indata
["name"], _id
=None)
212 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
213 if not session
["force"] and edit_content
.get("name"):
214 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
217 schema_version
= final_content
.get("schema_version")
219 if edit_content
.get("vim_password"):
220 final_content
["vim_password"] = self
.db
.encrypt(edit_content
["vim_password"],
221 schema_version
=schema_version
, salt
=_id
)
222 if edit_content
.get("config"):
223 for p
in self
.vim_config_encrypted
:
224 if edit_content
["config"].get(p
):
225 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
226 schema_version
=schema_version
, salt
=_id
)
228 def format_on_new(self
, content
, project_id
=None, make_public
=False):
229 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
230 content
["schema_version"] = schema_version
= "1.1"
233 if content
.get("vim_password"):
234 content
["vim_password"] = self
.db
.encrypt(content
["vim_password"], schema_version
=schema_version
,
236 if content
.get("config"):
237 for p
in self
.vim_config_encrypted
:
238 if content
["config"].get(p
):
239 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
242 content
["_admin"]["operationalState"] = "PROCESSING"
244 def delete(self
, session
, _id
, dry_run
=False):
246 Delete item by its internal _id
247 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
248 :param _id: server internal id
249 :param dry_run: make checking but do not delete
250 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
252 # TODO add admin to filter, validate rights
253 if dry_run
or session
["force"]: # delete completely
254 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
255 else: # if not, sent to kafka
256 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
257 self
.db
.set_one("vim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
258 self
._send
_msg
("delete", {"_id": _id
})
259 return v
# TODO indicate an offline operation to return 202 ACCEPTED
262 class WimAccountTopic(BaseTopic
):
263 topic
= "wim_accounts"
264 topic_msg
= "wim_account"
265 schema_new
= wim_account_new_schema
266 schema_edit
= wim_account_edit_schema
268 wim_config_encrypted
= ()
270 def __init__(self
, db
, fs
, msg
):
271 BaseTopic
.__init
__(self
, db
, fs
, msg
)
273 def check_conflict_on_new(self
, session
, indata
):
274 self
.check_unique_name(session
, indata
["name"], _id
=None)
276 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
277 if not session
["force"] and edit_content
.get("name"):
278 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
281 schema_version
= final_content
.get("schema_version")
283 if edit_content
.get("wim_password"):
284 final_content
["wim_password"] = self
.db
.encrypt(edit_content
["wim_password"],
285 schema_version
=schema_version
, salt
=_id
)
286 if edit_content
.get("config"):
287 for p
in self
.wim_config_encrypted
:
288 if edit_content
["config"].get(p
):
289 final_content
["config"][p
] = self
.db
.encrypt(edit_content
["config"][p
],
290 schema_version
=schema_version
, salt
=_id
)
292 def format_on_new(self
, content
, project_id
=None, make_public
=False):
293 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
294 content
["schema_version"] = schema_version
= "1.1"
297 if content
.get("wim_password"):
298 content
["wim_password"] = self
.db
.encrypt(content
["wim_password"], schema_version
=schema_version
,
300 if content
.get("config"):
301 for p
in self
.wim_config_encrypted
:
302 if content
["config"].get(p
):
303 content
["config"][p
] = self
.db
.encrypt(content
["config"][p
], schema_version
=schema_version
,
306 content
["_admin"]["operationalState"] = "PROCESSING"
308 def delete(self
, session
, _id
, dry_run
=False):
310 Delete item by its internal _id
311 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
312 :param _id: server internal id
313 :param dry_run: make checking but do not delete
314 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
316 # TODO add admin to filter, validate rights
317 if dry_run
or session
["force"]: # delete completely
318 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
319 else: # if not, sent to kafka
320 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
321 self
.db
.set_one("wim_accounts", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
322 self
._send
_msg
("delete", {"_id": _id
})
323 return v
# TODO indicate an offline operation to return 202 ACCEPTED
326 class SdnTopic(BaseTopic
):
329 schema_new
= sdn_new_schema
330 schema_edit
= sdn_edit_schema
333 def __init__(self
, db
, fs
, msg
):
334 BaseTopic
.__init
__(self
, db
, fs
, msg
)
336 def check_conflict_on_new(self
, session
, indata
):
337 self
.check_unique_name(session
, indata
["name"], _id
=None)
339 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
340 if not session
["force"] and edit_content
.get("name"):
341 self
.check_unique_name(session
, edit_content
["name"], _id
=_id
)
344 schema_version
= final_content
.get("schema_version")
345 if schema_version
and edit_content
.get("password"):
346 final_content
["password"] = self
.db
.encrypt(edit_content
["password"], schema_version
=schema_version
,
349 def format_on_new(self
, content
, project_id
=None, make_public
=False):
350 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
351 content
["schema_version"] = schema_version
= "1.1"
353 if content
.get("password"):
354 content
["password"] = self
.db
.encrypt(content
["password"], schema_version
=schema_version
,
357 content
["_admin"]["operationalState"] = "PROCESSING"
359 def delete(self
, session
, _id
, dry_run
=False):
361 Delete item by its internal _id
362 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
363 :param _id: server internal id
364 :param dry_run: make checking but do not delete
365 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
367 if dry_run
or session
["force"]: # delete completely
368 return BaseTopic
.delete(self
, session
, _id
, dry_run
)
369 else: # if not sent to kafka
370 v
= BaseTopic
.delete(self
, session
, _id
, dry_run
=True)
371 self
.db
.set_one("sdns", {"_id": _id
}, {"_admin.to_delete": True}) # TODO change status
372 self
._send
_msg
("delete", {"_id": _id
})
373 return v
# TODO indicate an offline operation to return 202 ACCEPTED
376 class UserTopicAuth(UserTopic
):
378 # topic_msg = "users"
379 schema_new
= user_new_schema
380 schema_edit
= user_edit_schema
382 def __init__(self
, db
, fs
, msg
, auth
):
383 UserTopic
.__init
__(self
, db
, fs
, msg
)
386 def check_conflict_on_new(self
, session
, indata
):
388 Check that the data to be inserted is valid
390 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
391 :param indata: data to be inserted
392 :return: None or raises EngineException
394 username
= indata
.get("username")
395 if is_valid_uuid(username
):
396 raise EngineException("username '{}' cannot be a uuid format".format(username
),
397 HTTPStatus
.UNPROCESSABLE_ENTITY
)
399 # Check that username is not used, regardless keystone already checks this
400 if self
.auth
.get_user_list(filter_q
={"name": username
}):
401 raise EngineException("username '{}' is already used".format(username
), HTTPStatus
.CONFLICT
)
403 if "projects" in indata
.keys():
404 raise EngineException("Format invalid: the keyword \"projects\" is not allowed for keystone authentication",
405 HTTPStatus
.BAD_REQUEST
)
407 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
409 Check that the data to be edited/uploaded is valid
411 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
412 :param final_content: data once modified
413 :param edit_content: incremental data that contains the modifications to apply
414 :param _id: internal _id
415 :return: None or raises EngineException
418 if "username" in edit_content
:
419 username
= edit_content
.get("username")
420 if is_valid_uuid(username
):
421 raise EngineException("username '{}' cannot be an uuid format".format(username
),
422 HTTPStatus
.UNPROCESSABLE_ENTITY
)
424 # Check that username is not used, regardless keystone already checks this
425 if self
.auth
.get_user_list(filter_q
={"name": username
}):
426 raise EngineException("username '{}' is already used".format(username
), HTTPStatus
.CONFLICT
)
428 if final_content
["username"] == "admin":
429 for mapping
in edit_content
.get("remove_project_role_mappings", ()):
430 if mapping
["project"] == "admin" and mapping
.get("role") in (None, "system_admin"):
431 # TODO make this also available for project id and role id
432 raise EngineException("You cannot remove system_admin role from admin user",
433 http_code
=HTTPStatus
.FORBIDDEN
)
435 def check_conflict_on_del(self
, session
, _id
, db_content
):
437 Check if deletion can be done because of dependencies if it is not force. To override
438 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
439 :param _id: internal _id
440 :param db_content: The database content of this item _id
441 :return: None if ok or raises EngineException with the conflict
443 if db_content
["username"] == session
["username"]:
444 raise EngineException("You cannot delete your own login user ", http_code
=HTTPStatus
.CONFLICT
)
447 # def format_on_new(content, project_id=None, make_public=False):
449 # Modifies content descriptor to include _id.
451 # NOTE: No password salt required because the authentication backend
452 # should handle these security concerns.
454 # :param content: descriptor to be modified
455 # :param make_public: if included it is generated as public for reading.
456 # :return: None, but content is modified
458 # BaseTopic.format_on_new(content, make_public=False)
459 # content["_id"] = content["username"]
460 # content["password"] = content["password"]
463 # def format_on_edit(final_content, edit_content):
465 # Modifies final_content descriptor to include the modified date.
467 # NOTE: No password salt required because the authentication backend
468 # should handle these security concerns.
470 # :param final_content: final descriptor generated
471 # :param edit_content: alterations to be include
472 # :return: None, but final_content is modified
474 # BaseTopic.format_on_edit(final_content, edit_content)
475 # if "password" in edit_content:
476 # final_content["password"] = edit_content["password"]
478 # final_content["project_role_mappings"] = edit_content["project_role_mappings"]
481 def format_on_show(content
):
483 Modifies the content of the role information to separate the role
484 metadata from the role definition.
486 project_role_mappings
= []
488 for project
in content
["projects"]:
489 for role
in project
["roles"]:
490 project_role_mappings
.append({"project": project
["_id"],
491 "project_name": project
["name"],
493 "role_name": role
["name"]})
495 del content
["projects"]
496 content
["project_role_mappings"] = project_role_mappings
500 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
502 Creates a new entry into the authentication backend.
504 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
506 :param rollback: list to append created items at database in case a rollback may to be done
507 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
508 :param indata: data to be inserted
509 :param kwargs: used to override the indata descriptor
510 :param headers: http request headers
511 :return: _id: identity of the inserted data.
514 content
= BaseTopic
._remove
_envelop
(indata
)
516 # Override descriptor with query string kwargs
517 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
518 content
= self
._validate
_input
_new
(content
, session
["force"])
519 self
.check_conflict_on_new(session
, content
)
520 # self.format_on_new(content, session["project_id"], make_public=session["public"])
521 _id
= self
.auth
.create_user(content
["username"], content
["password"])["_id"]
523 if "project_role_mappings" in content
.keys():
524 for mapping
in content
["project_role_mappings"]:
525 self
.auth
.assign_role_to_user(_id
, mapping
["project"], mapping
["role"])
527 rollback
.append({"topic": self
.topic
, "_id": _id
})
528 # del content["password"]
529 # self._send_msg("create", content)
531 except ValidationError
as e
:
532 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
534 def show(self
, session
, _id
):
536 Get complete information on an topic
538 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
539 :param _id: server internal id
540 :return: dictionary, raise exception if not found.
542 # Allow _id to be a name or uuid
543 filter_q
= {self
.id_field(self
.topic
, _id
): _id
}
544 users
= self
.auth
.get_user_list(filter_q
)
547 return self
.format_on_show(users
[0])
549 raise EngineException("Too many users found", HTTPStatus
.CONFLICT
)
551 raise EngineException("User not found", HTTPStatus
.NOT_FOUND
)
553 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
555 Updates an user entry.
557 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
559 :param indata: data to be inserted
560 :param kwargs: used to override the indata descriptor
562 :return: _id: identity of the inserted data.
564 indata
= self
._remove
_envelop
(indata
)
566 # Override descriptor with query string kwargs
568 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
570 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
573 content
= self
.show(session
, _id
)
574 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
575 # self.format_on_edit(content, indata)
577 if "password" in indata
or "username" in indata
:
578 self
.auth
.update_user(_id
, new_name
=indata
.get("username"), new_password
=indata
.get("password"))
579 if not indata
.get("remove_project_role_mappings") and not indata
.get("add_project_role_mappings") and \
580 not indata
.get("project_role_mappings"):
582 if indata
.get("project_role_mappings") and \
583 (indata
.get("remove_project_role_mappings") or indata
.get("add_project_role_mappings")):
584 raise EngineException("Option 'project_role_mappings' is incompatible with 'add_project_role_mappings"
585 "' or 'remove_project_role_mappings'", http_code
=HTTPStatus
.BAD_REQUEST
)
587 user
= self
.show(session
, _id
)
588 original_mapping
= user
["project_role_mappings"]
591 mappings_to_remove
= []
594 for to_remove
in indata
.get("remove_project_role_mappings", ()):
595 for mapping
in original_mapping
:
596 if to_remove
["project"] in (mapping
["project"], mapping
["project_name"]):
597 if not to_remove
.get("role") or to_remove
["role"] in (mapping
["role"], mapping
["role_name"]):
598 mappings_to_remove
.append(mapping
)
601 for to_add
in indata
.get("add_project_role_mappings", ()):
602 for mapping
in original_mapping
:
603 if to_add
["project"] in (mapping
["project"], mapping
["project_name"]) and \
604 to_add
["role"] in (mapping
["role"], mapping
["role_name"]):
606 if mapping
in mappings_to_remove
: # do not remove
607 mappings_to_remove
.remove(mapping
)
608 break # do not add, it is already at user
610 mappings_to_add
.append(to_add
)
613 if indata
.get("project_role_mappings"):
614 for to_set
in indata
["project_role_mappings"]:
615 for mapping
in original_mapping
:
616 if to_set
["project"] in (mapping
["project"], mapping
["project_name"]) and \
617 to_set
["role"] in (mapping
["role"], mapping
["role_name"]):
619 if mapping
in mappings_to_remove
: # do not remove
620 mappings_to_remove
.remove(mapping
)
621 break # do not add, it is already at user
623 mappings_to_add
.append(to_set
)
624 for mapping
in original_mapping
:
625 for to_set
in indata
["project_role_mappings"]:
626 if to_set
["project"] in (mapping
["project"], mapping
["project_name"]) and \
627 to_set
["role"] in (mapping
["role"], mapping
["role_name"]):
631 if mapping
not in mappings_to_remove
: # do not remove
632 mappings_to_remove
.append(mapping
)
634 for mapping
in mappings_to_remove
:
635 self
.auth
.remove_role_from_user(
641 for mapping
in mappings_to_add
:
642 self
.auth
.assign_role_to_user(
649 except ValidationError
as e
:
650 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
652 def list(self
, session
, filter_q
=None):
654 Get a list of the topic that matches a filter
655 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
656 :param filter_q: filter of data to be applied
657 :return: The list, it can be empty if no one match the filter.
659 users
= [self
.format_on_show(user
) for user
in self
.auth
.get_user_list(filter_q
)]
663 def delete(self
, session
, _id
, dry_run
=False):
665 Delete item by its internal _id
667 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
668 :param _id: server internal id
669 :param force: indicates if deletion must be forced in case of conflict
670 :param dry_run: make checking but do not delete
671 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
673 # Allow _id to be a name or uuid
674 filter_q
= {self
.id_field(self
.topic
, _id
): _id
}
675 user_list
= self
.auth
.get_user_list(filter_q
)
677 raise EngineException("User '{}' not found".format(_id
), http_code
=HTTPStatus
.NOT_FOUND
)
678 _id
= user_list
[0]["_id"]
679 self
.check_conflict_on_del(session
, _id
, user_list
[0])
681 v
= self
.auth
.delete_user(_id
)
686 class ProjectTopicAuth(ProjectTopic
):
688 # topic_msg = "projects"
689 schema_new
= project_new_schema
690 schema_edit
= project_edit_schema
692 def __init__(self
, db
, fs
, msg
, auth
):
693 ProjectTopic
.__init
__(self
, db
, fs
, msg
)
696 def check_conflict_on_new(self
, session
, indata
):
698 Check that the data to be inserted is valid
700 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
701 :param indata: data to be inserted
702 :return: None or raises EngineException
704 project_name
= indata
.get("name")
705 if is_valid_uuid(project_name
):
706 raise EngineException("project name '{}' cannot be an uuid format".format(project_name
),
707 HTTPStatus
.UNPROCESSABLE_ENTITY
)
709 project_list
= self
.auth
.get_project_list(filter_q
={"name": project_name
})
712 raise EngineException("project '{}' exists".format(project_name
), HTTPStatus
.CONFLICT
)
714 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
716 Check that the data to be edited/uploaded is valid
718 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
719 :param final_content: data once modified
720 :param edit_content: incremental data that contains the modifications to apply
721 :param _id: internal _id
722 :return: None or raises EngineException
725 project_name
= edit_content
.get("name")
727 if is_valid_uuid(project_name
):
728 raise EngineException("project name '{}' cannot be an uuid format".format(project_name
),
729 HTTPStatus
.UNPROCESSABLE_ENTITY
)
731 # Check that project name is not used, regardless keystone already checks this
732 if self
.auth
.get_project_list(filter_q
={"name": project_name
}):
733 raise EngineException("project '{}' is already used".format(project_name
), HTTPStatus
.CONFLICT
)
735 def check_conflict_on_del(self
, session
, _id
, db_content
):
737 Check if deletion can be done because of dependencies if it is not force. To override
739 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
740 :param _id: internal _id
741 :param db_content: The database content of this item _id
742 :return: None if ok or raises EngineException with the conflict
744 # projects = self.auth.get_project_list()
745 # current_project = [project for project in projects
746 # if project["name"] in session["project_id"]][0]
747 # TODO check that any user is using this project, raise CONFLICT exception
748 if _id
== session
["project_id"]:
749 raise EngineException("You cannot delete your own project", http_code
=HTTPStatus
.CONFLICT
)
751 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
753 Creates a new entry into the authentication backend.
755 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
757 :param rollback: list to append created items at database in case a rollback may to be done
758 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
759 :param indata: data to be inserted
760 :param kwargs: used to override the indata descriptor
761 :param headers: http request headers
762 :return: _id: identity of the inserted data.
765 content
= BaseTopic
._remove
_envelop
(indata
)
767 # Override descriptor with query string kwargs
768 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
769 content
= self
._validate
_input
_new
(content
, session
["force"])
770 self
.check_conflict_on_new(session
, content
)
771 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=session
["public"])
772 _id
= self
.auth
.create_project(content
["name"])
773 rollback
.append({"topic": self
.topic
, "_id": _id
})
774 # self._send_msg("create", content)
776 except ValidationError
as e
:
777 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
779 def show(self
, session
, _id
):
781 Get complete information on an topic
783 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
784 :param _id: server internal id
785 :return: dictionary, raise exception if not found.
787 # Allow _id to be a name or uuid
788 filter_q
= {self
.id_field(self
.topic
, _id
): _id
}
789 projects
= self
.auth
.get_project_list(filter_q
=filter_q
)
791 if len(projects
) == 1:
793 elif len(projects
) > 1:
794 raise EngineException("Too many projects found", HTTPStatus
.CONFLICT
)
796 raise EngineException("Project not found", HTTPStatus
.NOT_FOUND
)
798 def list(self
, session
, filter_q
=None):
800 Get a list of the topic that matches a filter
802 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
803 :param filter_q: filter of data to be applied
804 :return: The list, it can be empty if no one match the filter.
806 return self
.auth
.get_project_list(filter_q
)
808 def delete(self
, session
, _id
, dry_run
=False):
810 Delete item by its internal _id
812 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
813 :param _id: server internal id
814 :param dry_run: make checking but do not delete
815 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
817 # Allow _id to be a name or uuid
818 filter_q
= {self
.id_field(self
.topic
, _id
): _id
}
819 project_list
= self
.auth
.get_project_list(filter_q
)
821 raise EngineException("Project '{}' not found".format(_id
), http_code
=HTTPStatus
.NOT_FOUND
)
822 _id
= project_list
[0]["_id"]
823 self
.check_conflict_on_del(session
, _id
, project_list
[0])
825 v
= self
.auth
.delete_project(_id
)
829 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
831 Updates a project entry.
833 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
835 :param indata: data to be inserted
836 :param kwargs: used to override the indata descriptor
838 :return: _id: identity of the inserted data.
840 indata
= self
._remove
_envelop
(indata
)
842 # Override descriptor with query string kwargs
844 BaseTopic
._update
_input
_with
_kwargs
(indata
, kwargs
)
846 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
849 content
= self
.show(session
, _id
)
850 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
851 # self.format_on_edit(content, indata)
854 self
.auth
.update_project(content
["_id"], indata
["name"])
855 except ValidationError
as e
:
856 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
859 class RoleTopicAuth(BaseTopic
):
860 topic
= "roles_operations"
862 schema_new
= roles_new_schema
863 schema_edit
= roles_edit_schema
866 def __init__(self
, db
, fs
, msg
, auth
, ops
):
867 BaseTopic
.__init
__(self
, db
, fs
, msg
)
869 self
.operations
= ops
872 def validate_role_definition(operations
, role_definitions
):
874 Validates the role definition against the operations defined in
875 the resources to operations files.
877 :param operations: operations list
878 :param role_definitions: role definition to test
879 :return: None if ok, raises ValidationError exception on error
881 ignore_fields
= ["_id", "_admin", "name"]
882 for role_def
in role_definitions
.keys():
883 if role_def
in ignore_fields
:
885 if role_def
== "root":
886 if isinstance(role_definitions
[role_def
], bool):
889 raise ValidationError("Operation authorization \".\" should be True/False.")
890 if role_def
[-1] == ":":
891 raise ValidationError("Operation cannot end with \".\"")
893 role_def_matches
= [op
for op
in operations
if op
.startswith(role_def
)]
895 if len(role_def_matches
) == 0:
896 raise ValidationError("No matching operation found.")
898 if not isinstance(role_definitions
[role_def
], bool):
899 raise ValidationError("Operation authorization {} should be True/False.".format(role_def
))
901 def _validate_input_new(self
, input, force
=False):
903 Validates input user content for a new entry.
905 :param input: user input content for the new topic
906 :param force: may be used for being more tolerant
907 :return: The same input content, or a changed version of it.
910 validate_input(input, self
.schema_new
)
911 self
.validate_role_definition(self
.operations
, input)
915 def _validate_input_edit(self
, input, force
=False):
917 Validates input user content for updating an entry.
919 :param input: user input content for the new topic
920 :param force: may be used for being more tolerant
921 :return: The same input content, or a changed version of it.
924 validate_input(input, self
.schema_edit
)
925 self
.validate_role_definition(self
.operations
, input)
929 def check_conflict_on_new(self
, session
, indata
):
931 Check that the data to be inserted is valid
933 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
934 :param indata: data to be inserted
935 :return: None or raises EngineException
937 role
= indata
.get("name")
938 role_list
= list(map(lambda x
: x
["name"], self
.auth
.get_role_list()))
940 if role
in role_list
:
941 raise EngineException("role '{}' exists".format(role
), HTTPStatus
.CONFLICT
)
943 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
945 Check that the data to be edited/uploaded is valid
947 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
948 :param final_content: data once modified
949 :param edit_content: incremental data that contains the modifications to apply
950 :param _id: internal _id
951 :return: None or raises EngineException
953 roles
= self
.auth
.get_role_list()
954 system_admin_role
= [role
for role
in roles
955 if role
["name"] == "system_admin"][0]
957 if _id
== system_admin_role
["_id"]:
958 raise EngineException("You cannot edit system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
960 def check_conflict_on_del(self
, session
, _id
, db_content
):
962 Check if deletion can be done because of dependencies if it is not force. To override
964 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
965 :param _id: internal _id
966 :param db_content: The database content of this item _id
967 :return: None if ok or raises EngineException with the conflict
969 roles
= self
.auth
.get_role_list()
970 system_admin_role
= [role
for role
in roles
971 if role
["name"] == "system_admin"][0]
973 if _id
== system_admin_role
["_id"]:
974 raise EngineException("You cannot delete system_admin role", http_code
=HTTPStatus
.FORBIDDEN
)
977 def format_on_new(content
, project_id
=None, make_public
=False):
979 Modifies content descriptor to include _admin
981 :param content: descriptor to be modified
982 :param project_id: if included, it add project read/write permissions
983 :param make_public: if included it is generated as public for reading.
984 :return: None, but content is modified
987 if "_admin" not in content
:
988 content
["_admin"] = {}
989 if not content
["_admin"].get("created"):
990 content
["_admin"]["created"] = now
991 content
["_admin"]["modified"] = now
993 if ":" in content
.keys():
994 content
["root"] = content
[":"]
997 if "root" not in content
.keys():
998 content
["root"] = False
1001 def format_on_edit(final_content
, edit_content
):
1003 Modifies final_content descriptor to include the modified date.
1005 :param final_content: final descriptor generated
1006 :param edit_content: alterations to be include
1007 :return: None, but final_content is modified
1009 final_content
["_admin"]["modified"] = time()
1011 ignore_fields
= ["_id", "name", "_admin"]
1012 delete_keys
= [key
for key
in final_content
.keys() if key
not in ignore_fields
]
1014 for key
in delete_keys
:
1015 del final_content
[key
]
1017 # Saving the role definition
1018 for role_def
, value
in edit_content
.items():
1019 final_content
[role_def
] = value
1021 if ":" in final_content
.keys():
1022 final_content
["root"] = final_content
[":"]
1023 del final_content
[":"]
1025 if "root" not in final_content
.keys():
1026 final_content
["root"] = False
1029 def format_on_show(content
):
1031 Modifies the content of the role information to separate the role
1032 metadata from the role definition. Eases the reading process of the
1035 :param definition: role definition to be processed
1037 content
["_id"] = str(content
["_id"])
1039 def show(self
, session
, _id
):
1041 Get complete information on an topic
1043 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1044 :param _id: server internal id
1045 :return: dictionary, raise exception if not found.
1047 filter_db
= {"_id": _id
}
1049 role
= self
.db
.get_one(self
.topic
, filter_db
)
1050 new_role
= dict(role
)
1051 self
.format_on_show(new_role
)
1055 def list(self
, session
, filter_q
=None):
1057 Get a list of the topic that matches a filter
1059 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1060 :param filter_q: filter of data to be applied
1061 :return: The list, it can be empty if no one match the filter.
1067 filter_q
["root"] = filter_q
[":"]
1069 for key
in filter_q
.keys():
1072 filter_q
[key
] = filter_q
[key
] in ["True", "true"]
1074 roles
= self
.db
.get_list(self
.topic
, filter_q
)
1078 new_role
= dict(role
)
1079 self
.format_on_show(new_role
)
1080 new_roles
.append(new_role
)
1084 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
1086 Creates a new entry into database.
1088 :param rollback: list to append created items at database in case a rollback may to be done
1089 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1090 :param indata: data to be inserted
1091 :param kwargs: used to override the indata descriptor
1092 :param headers: http request headers
1093 :return: _id: identity of the inserted data.
1096 content
= BaseTopic
._remove
_envelop
(indata
)
1098 # Override descriptor with query string kwargs
1099 BaseTopic
._update
_input
_with
_kwargs
(content
, kwargs
)
1100 content
= self
._validate
_input
_new
(content
, session
["force"])
1101 self
.check_conflict_on_new(session
, content
)
1102 self
.format_on_new(content
, project_id
=session
["project_id"], make_public
=session
["public"])
1103 role_name
= content
["name"]
1104 role
= self
.auth
.create_role(role_name
)
1105 content
["_id"] = role
["_id"]
1106 _id
= self
.db
.create(self
.topic
, content
)
1107 rollback
.append({"topic": self
.topic
, "_id": _id
})
1108 # self._send_msg("create", content)
1110 except ValidationError
as e
:
1111 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
1113 def delete(self
, session
, _id
, dry_run
=False):
1115 Delete item by its internal _id
1117 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1118 :param _id: server internal id
1119 :param dry_run: make checking but do not delete
1120 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1122 self
.check_conflict_on_del(session
, _id
, None)
1123 filter_q
= {"_id": _id
}
1125 self
.auth
.delete_role(_id
)
1126 v
= self
.db
.del_one(self
.topic
, filter_q
)
1130 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
1132 Updates a role entry.
1134 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1136 :param indata: data to be inserted
1137 :param kwargs: used to override the indata descriptor
1139 :return: _id: identity of the inserted data.
1141 indata
= self
._remove
_envelop
(indata
)
1143 # Override descriptor with query string kwargs
1145 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
1147 indata
= self
._validate
_input
_edit
(indata
, force
=session
["force"])
1150 content
= self
.show(session
, _id
)
1151 self
.check_conflict_on_edit(session
, content
, indata
, _id
=_id
)
1152 self
.format_on_edit(content
, indata
)
1153 self
.db
.replace(self
.topic
, _id
, content
)
1155 except ValidationError
as e
:
1156 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)