blob: 9facd8bdec3b3b7878c10f484dd6413ce5bb1ce0 [file] [log] [blame]
tiernob24258a2018-10-04 18:39:49 +02001# -*- coding: utf-8 -*-
2
tiernod125caf2018-11-22 16:05:54 +00003# 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
tiernob24258a2018-10-04 18:39:49 +020016# import logging
17from uuid import uuid4
18from hashlib import sha256
19from http import HTTPStatus
Eduardo Sousa5c01e192019-05-08 02:35:47 +010020from time import time
tiernob24258a2018-10-04 18:39:49 +020021from validation import user_new_schema, user_edit_schema, project_new_schema, project_edit_schema
22from validation import vim_account_new_schema, vim_account_edit_schema, sdn_new_schema, sdn_edit_schema
Eduardo Sousa5c01e192019-05-08 02:35:47 +010023from validation import wim_account_new_schema, wim_account_edit_schema, roles_new_schema, roles_edit_schema
24from validation import validate_input
25from validation import ValidationError
delacruzramoc061f562019-04-05 11:00:02 +020026from validation import is_valid_uuid # To check that User/Project Names don't look like UUIDs
tiernob24258a2018-10-04 18:39:49 +020027from base_topic import BaseTopic, EngineException
28
29__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
30
31
32class UserTopic(BaseTopic):
33 topic = "users"
34 topic_msg = "users"
35 schema_new = user_new_schema
36 schema_edit = user_edit_schema
tierno65ca36d2019-02-12 19:27:52 +010037 multiproject = False
tiernob24258a2018-10-04 18:39:49 +020038
39 def __init__(self, db, fs, msg):
40 BaseTopic.__init__(self, db, fs, msg)
41
42 @staticmethod
tierno65ca36d2019-02-12 19:27:52 +010043 def _get_project_filter(session):
tiernob24258a2018-10-04 18:39:49 +020044 """
45 Generates a filter dictionary for querying database users.
46 Current policy is admin can show all, non admin, only its own user.
tierno65ca36d2019-02-12 19:27:52 +010047 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +020048 :return:
49 """
50 if session["admin"]: # allows all
51 return {}
52 else:
53 return {"username": session["username"]}
54
tierno65ca36d2019-02-12 19:27:52 +010055 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +020056 # 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)
59 # check projects
tierno65ca36d2019-02-12 19:27:52 +010060 if not session["force"]:
61 for p in indata.get("projects"):
delacruzramoc061f562019-04-05 11:00:02 +020062 # 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,
64 fail_on_more=False):
65 raise EngineException("project '{}' does not exist".format(p), HTTPStatus.CONFLICT)
tiernob24258a2018-10-04 18:39:49 +020066
tiernob4844ab2019-05-23 08:42:12 +000067 def check_conflict_on_del(self, session, _id, db_content):
68 """
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
74 """
tiernob24258a2018-10-04 18:39:49 +020075 if _id == session["username"]:
76 raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
77
78 @staticmethod
79 def format_on_new(content, project_id=None, make_public=False):
80 BaseTopic.format_on_new(content, make_public=False)
delacruzramoc061f562019-04-05 11:00:02 +020081 # Removed so that the UUID is kept, to allow User Name modification
82 # content["_id"] = content["username"]
tiernob24258a2018-10-04 18:39:49 +020083 salt = uuid4().hex
84 content["_admin"]["salt"] = salt
85 if content.get("password"):
86 content["password"] = sha256(content["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
Eduardo Sousa339ed782019-05-28 14:25:00 +010087 if content.get("project_role_mappings"):
88 projects = [mapping[0] for mapping in content["project_role_mappings"]]
89
90 if content.get("projects"):
91 content["projects"] += projects
92 else:
93 content["projects"] = projects
tiernob24258a2018-10-04 18:39:49 +020094
95 @staticmethod
96 def format_on_edit(final_content, edit_content):
97 BaseTopic.format_on_edit(final_content, edit_content)
98 if edit_content.get("password"):
99 salt = uuid4().hex
100 final_content["_admin"]["salt"] = salt
101 final_content["password"] = sha256(edit_content["password"].encode('utf-8') +
102 salt.encode('utf-8')).hexdigest()
103
tierno65ca36d2019-02-12 19:27:52 +0100104 def edit(self, session, _id, indata=None, kwargs=None, content=None):
tiernob24258a2018-10-04 18:39:49 +0200105 if not session["admin"]:
106 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200107 # 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)
tierno65ca36d2019-02-12 19:27:52 +0100112 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
tiernob24258a2018-10-04 18:39:49 +0200113
tierno65ca36d2019-02-12 19:27:52 +0100114 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200115 if not session["admin"]:
116 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200117 # 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)
tierno65ca36d2019-02-12 19:27:52 +0100122 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
tiernob24258a2018-10-04 18:39:49 +0200123
124
125class ProjectTopic(BaseTopic):
126 topic = "projects"
127 topic_msg = "projects"
128 schema_new = project_new_schema
129 schema_edit = project_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100130 multiproject = False
tiernob24258a2018-10-04 18:39:49 +0200131
132 def __init__(self, db, fs, msg):
133 BaseTopic.__init__(self, db, fs, msg)
134
tierno65ca36d2019-02-12 19:27:52 +0100135 @staticmethod
136 def _get_project_filter(session):
137 """
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"
141 :return:
142 """
143 if session["admin"]: # allows all
144 return {}
145 else:
146 return {"_id.cont": session["project_id"]}
147
148 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200149 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)
154
155 @staticmethod
156 def format_on_new(content, project_id=None, make_public=False):
157 BaseTopic.format_on_new(content, None)
delacruzramoc061f562019-04-05 11:00:02 +0200158 # Removed so that the UUID is kept, to allow Project Name modification
159 # content["_id"] = content["name"]
tiernob24258a2018-10-04 18:39:49 +0200160
tiernob4844ab2019-05-23 08:42:12 +0000161 def check_conflict_on_del(self, session, _id, db_content):
162 """
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
168 """
tierno65ca36d2019-02-12 19:27:52 +0100169 if _id in session["project_id"]:
tiernob24258a2018-10-04 18:39:49 +0200170 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
tierno65ca36d2019-02-12 19:27:52 +0100171 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200172 return
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)
176
tierno65ca36d2019-02-12 19:27:52 +0100177 def edit(self, session, _id, indata=None, kwargs=None, content=None):
tiernob24258a2018-10-04 18:39:49 +0200178 if not session["admin"]:
179 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200180 # 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)
tierno65ca36d2019-02-12 19:27:52 +0100185 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
tiernob24258a2018-10-04 18:39:49 +0200186
tierno65ca36d2019-02-12 19:27:52 +0100187 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200188 if not session["admin"]:
189 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200190 # 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)
tierno65ca36d2019-02-12 19:27:52 +0100195 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
tiernob24258a2018-10-04 18:39:49 +0200196
197
198class 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
tierno92c1c7d2018-11-12 15:22:37 +0100203 vim_config_encrypted = ("admin_password", "nsx_password", "vcenter_password")
tierno65ca36d2019-02-12 19:27:52 +0100204 multiproject = True
tiernob24258a2018-10-04 18:39:49 +0200205
206 def __init__(self, db, fs, msg):
207 BaseTopic.__init__(self, db, fs, msg)
208
tierno65ca36d2019-02-12 19:27:52 +0100209 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200210 self.check_unique_name(session, indata["name"], _id=None)
211
tierno65ca36d2019-02-12 19:27:52 +0100212 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
213 if not session["force"] and edit_content.get("name"):
tiernob24258a2018-10-04 18:39:49 +0200214 self.check_unique_name(session, edit_content["name"], _id=_id)
215
tierno92c1c7d2018-11-12 15:22:37 +0100216 # encrypt passwords
217 schema_version = final_content.get("schema_version")
218 if 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)
227
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"
231
232 # encrypt passwords
233 if content.get("vim_password"):
234 content["vim_password"] = self.db.encrypt(content["vim_password"], schema_version=schema_version,
235 salt=content["_id"])
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,
240 salt=content["_id"])
241
tiernob24258a2018-10-04 18:39:49 +0200242 content["_admin"]["operationalState"] = "PROCESSING"
243
tierno65ca36d2019-02-12 19:27:52 +0100244 def delete(self, session, _id, dry_run=False):
tiernob24258a2018-10-04 18:39:49 +0200245 """
246 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100247 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200248 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200249 :param dry_run: make checking but do not delete
250 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
251 """
252 # TODO add admin to filter, validate rights
tierno65ca36d2019-02-12 19:27:52 +0100253 if dry_run or session["force"]: # delete completely
254 return BaseTopic.delete(self, session, _id, dry_run)
tiernob24258a2018-10-04 18:39:49 +0200255 else: # if not, sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100256 v = BaseTopic.delete(self, session, _id, dry_run=True)
tiernob24258a2018-10-04 18:39:49 +0200257 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
260
261
tierno55ba2e62018-12-11 17:22:22 +0000262class 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
tierno65ca36d2019-02-12 19:27:52 +0100267 multiproject = True
tierno55ba2e62018-12-11 17:22:22 +0000268 wim_config_encrypted = ()
269
270 def __init__(self, db, fs, msg):
271 BaseTopic.__init__(self, db, fs, msg)
272
tierno65ca36d2019-02-12 19:27:52 +0100273 def check_conflict_on_new(self, session, indata):
tierno55ba2e62018-12-11 17:22:22 +0000274 self.check_unique_name(session, indata["name"], _id=None)
275
tierno65ca36d2019-02-12 19:27:52 +0100276 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
277 if not session["force"] and edit_content.get("name"):
tierno55ba2e62018-12-11 17:22:22 +0000278 self.check_unique_name(session, edit_content["name"], _id=_id)
279
280 # encrypt passwords
281 schema_version = final_content.get("schema_version")
282 if 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)
291
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"
295
296 # encrypt passwords
297 if content.get("wim_password"):
298 content["wim_password"] = self.db.encrypt(content["wim_password"], schema_version=schema_version,
299 salt=content["_id"])
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,
304 salt=content["_id"])
305
306 content["_admin"]["operationalState"] = "PROCESSING"
307
tierno65ca36d2019-02-12 19:27:52 +0100308 def delete(self, session, _id, dry_run=False):
tierno55ba2e62018-12-11 17:22:22 +0000309 """
310 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100311 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno55ba2e62018-12-11 17:22:22 +0000312 :param _id: server internal id
tierno55ba2e62018-12-11 17:22:22 +0000313 :param dry_run: make checking but do not delete
314 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
315 """
316 # TODO add admin to filter, validate rights
tierno65ca36d2019-02-12 19:27:52 +0100317 if dry_run or session["force"]: # delete completely
318 return BaseTopic.delete(self, session, _id, dry_run)
tierno55ba2e62018-12-11 17:22:22 +0000319 else: # if not, sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100320 v = BaseTopic.delete(self, session, _id, dry_run=True)
tierno55ba2e62018-12-11 17:22:22 +0000321 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
324
325
tiernob24258a2018-10-04 18:39:49 +0200326class SdnTopic(BaseTopic):
327 topic = "sdns"
328 topic_msg = "sdn"
329 schema_new = sdn_new_schema
330 schema_edit = sdn_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100331 multiproject = True
tiernob24258a2018-10-04 18:39:49 +0200332
333 def __init__(self, db, fs, msg):
334 BaseTopic.__init__(self, db, fs, msg)
335
tierno65ca36d2019-02-12 19:27:52 +0100336 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200337 self.check_unique_name(session, indata["name"], _id=None)
338
tierno65ca36d2019-02-12 19:27:52 +0100339 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
340 if not session["force"] and edit_content.get("name"):
tiernob24258a2018-10-04 18:39:49 +0200341 self.check_unique_name(session, edit_content["name"], _id=_id)
342
tierno92c1c7d2018-11-12 15:22:37 +0100343 # encrypt passwords
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,
347 salt=_id)
348
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"
352 # encrypt passwords
353 if content.get("password"):
354 content["password"] = self.db.encrypt(content["password"], schema_version=schema_version,
355 salt=content["_id"])
356
tiernob24258a2018-10-04 18:39:49 +0200357 content["_admin"]["operationalState"] = "PROCESSING"
358
tierno65ca36d2019-02-12 19:27:52 +0100359 def delete(self, session, _id, dry_run=False):
tiernob24258a2018-10-04 18:39:49 +0200360 """
361 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100362 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200363 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200364 :param dry_run: make checking but do not delete
365 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
366 """
tierno65ca36d2019-02-12 19:27:52 +0100367 if dry_run or session["force"]: # delete completely
368 return BaseTopic.delete(self, session, _id, dry_run)
tiernob24258a2018-10-04 18:39:49 +0200369 else: # if not sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100370 v = BaseTopic.delete(self, session, _id, dry_run=True)
tiernob24258a2018-10-04 18:39:49 +0200371 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
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100374
375
376class UserTopicAuth(UserTopic):
tierno65ca36d2019-02-12 19:27:52 +0100377 # topic = "users"
378 # topic_msg = "users"
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100379 schema_new = user_new_schema
380 schema_edit = user_edit_schema
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100381
382 def __init__(self, db, fs, msg, auth):
383 UserTopic.__init__(self, db, fs, msg)
384 self.auth = auth
385
tierno65ca36d2019-02-12 19:27:52 +0100386 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100387 """
388 Check that the data to be inserted is valid
389
tierno65ca36d2019-02-12 19:27:52 +0100390 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100391 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100392 :return: None or raises EngineException
393 """
394 username = indata.get("username")
395 user_list = list(map(lambda x: x["username"], self.auth.get_user_list()))
396
Eduardo Sousa339ed782019-05-28 14:25:00 +0100397 if "projects" in indata.keys():
398 raise EngineException("Format invalid: the keyword \"projects\" is not allowed for Keystone",
399 HTTPStatus.BAD_REQUEST)
400
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100401 if username in user_list:
402 raise EngineException("username '{}' exists".format(username), HTTPStatus.CONFLICT)
403
tierno65ca36d2019-02-12 19:27:52 +0100404 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100405 """
406 Check that the data to be edited/uploaded is valid
407
tierno65ca36d2019-02-12 19:27:52 +0100408 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100409 :param final_content: data once modified
410 :param edit_content: incremental data that contains the modifications to apply
411 :param _id: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100412 :return: None or raises EngineException
413 """
414 users = self.auth.get_user_list()
Eduardo Sousa8c3df702019-06-04 09:20:01 +0100415 admin_user = [user for user in users if user["username"] == "admin"][0]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100416
Eduardo Sousa8c3df702019-06-04 09:20:01 +0100417 if _id == admin_user["_id"] and "project_role_mappings" in edit_content.keys():
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100418 elem = {
419 "project": "admin",
420 "role": "system_admin"
421 }
422 if elem not in edit_content:
Eduardo Sousa8c3df702019-06-04 09:20:01 +0100423 raise EngineException("You cannot remove system_admin role from admin user",
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100424 http_code=HTTPStatus.FORBIDDEN)
425
tiernob4844ab2019-05-23 08:42:12 +0000426 def check_conflict_on_del(self, session, _id, db_content):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100427 """
428 Check if deletion can be done because of dependencies if it is not force. To override
tierno65ca36d2019-02-12 19:27:52 +0100429 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100430 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000431 :param db_content: The database content of this item _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100432 :return: None if ok or raises EngineException with the conflict
433 """
434 if _id == session["username"]:
435 raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
436
437 @staticmethod
438 def format_on_new(content, project_id=None, make_public=False):
439 """
440 Modifies content descriptor to include _id.
441
442 NOTE: No password salt required because the authentication backend
443 should handle these security concerns.
444
445 :param content: descriptor to be modified
446 :param make_public: if included it is generated as public for reading.
447 :return: None, but content is modified
448 """
449 BaseTopic.format_on_new(content, make_public=False)
450 content["_id"] = content["username"]
451 content["password"] = content["password"]
452
453 @staticmethod
454 def format_on_edit(final_content, edit_content):
455 """
456 Modifies final_content descriptor to include the modified date.
457
458 NOTE: No password salt required because the authentication backend
459 should handle these security concerns.
460
461 :param final_content: final descriptor generated
462 :param edit_content: alterations to be include
463 :return: None, but final_content is modified
464 """
465 BaseTopic.format_on_edit(final_content, edit_content)
466 if "password" in edit_content:
467 final_content["password"] = edit_content["password"]
468 else:
469 final_content["project_role_mappings"] = edit_content["project_role_mappings"]
470
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100471 @staticmethod
472 def format_on_show(content):
473 """
Eduardo Sousa44603902019-06-04 08:10:32 +0100474 Modifies the content of the role information to separate the role
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100475 metadata from the role definition.
476 """
477 project_role_mappings = []
478
479 for project in content["projects"]:
480 for role in project["roles"]:
Eduardo Sousa44603902019-06-04 08:10:32 +0100481 project_role_mappings.append({"project": project["_id"], "role": role["_id"]})
482
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100483 del content["projects"]
484 content["project_role_mappings"] = project_role_mappings
485
Eduardo Sousa0b1d61b2019-05-30 19:55:52 +0100486 return content
487
tierno65ca36d2019-02-12 19:27:52 +0100488 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100489 """
490 Creates a new entry into the authentication backend.
491
492 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
493
494 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100495 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100496 :param indata: data to be inserted
497 :param kwargs: used to override the indata descriptor
498 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100499 :return: _id: identity of the inserted data.
500 """
501 try:
502 content = BaseTopic._remove_envelop(indata)
503
504 # Override descriptor with query string kwargs
505 BaseTopic._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100506 content = self._validate_input_new(content, session["force"])
507 self.check_conflict_on_new(session, content)
508 self.format_on_new(content, session["project_id"], make_public=session["public"])
Eduardo Sousa44603902019-06-04 08:10:32 +0100509 _id = self.auth.create_user(content["username"], content["password"])["_id"]
510
Eduardo Sousaa519a962019-06-06 15:00:50 +0100511 if "project_role_mappings" in content.keys():
512 for mapping in content["project_role_mappings"]:
513 self.auth.assign_role_to_user(_id, mapping["project"], mapping["role"])
Eduardo Sousa44603902019-06-04 08:10:32 +0100514
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100515 rollback.append({"topic": self.topic, "_id": _id})
516 del content["password"]
517 # self._send_msg("create", content)
518 return _id
519 except ValidationError as e:
520 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
521
522 def show(self, session, _id):
523 """
524 Get complete information on an topic
525
tierno65ca36d2019-02-12 19:27:52 +0100526 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100527 :param _id: server internal id
528 :return: dictionary, raise exception if not found.
529 """
530 users = [user for user in self.auth.get_user_list() if user["_id"] == _id]
531
532 if len(users) == 1:
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100533 return self.format_on_show(users[0])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100534 elif len(users) > 1:
535 raise EngineException("Too many users found", HTTPStatus.CONFLICT)
536 else:
537 raise EngineException("User not found", HTTPStatus.NOT_FOUND)
538
tierno65ca36d2019-02-12 19:27:52 +0100539 def edit(self, session, _id, indata=None, kwargs=None, content=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100540 """
541 Updates an user entry.
542
tierno65ca36d2019-02-12 19:27:52 +0100543 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100544 :param _id:
545 :param indata: data to be inserted
546 :param kwargs: used to override the indata descriptor
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100547 :param content:
548 :return: _id: identity of the inserted data.
549 """
550 indata = self._remove_envelop(indata)
551
552 # Override descriptor with query string kwargs
553 if kwargs:
554 BaseTopic._update_input_with_kwargs(indata, kwargs)
555 try:
tierno65ca36d2019-02-12 19:27:52 +0100556 indata = self._validate_input_edit(indata, force=session["force"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100557
558 if not content:
559 content = self.show(session, _id)
tierno65ca36d2019-02-12 19:27:52 +0100560 self.check_conflict_on_edit(session, content, indata, _id=_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100561 self.format_on_edit(content, indata)
562
563 if "password" in content:
Eduardo Sousa8c3df702019-06-04 09:20:01 +0100564 self.auth.change_password(content["username"], content["password"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100565 else:
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100566 user = self.show(session, _id)
567 original_mapping = user["project_role_mappings"]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100568 edit_mapping = content["project_role_mappings"]
Eduardo Sousa44603902019-06-04 08:10:32 +0100569
570 mappings_to_remove = [mapping for mapping in original_mapping
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100571 if mapping not in edit_mapping]
Eduardo Sousa44603902019-06-04 08:10:32 +0100572
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100573 mappings_to_add = [mapping for mapping in edit_mapping
574 if mapping not in original_mapping]
Eduardo Sousa44603902019-06-04 08:10:32 +0100575
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100576 for mapping in mappings_to_remove:
577 self.auth.remove_role_from_user(
Eduardo Sousa8c3df702019-06-04 09:20:01 +0100578 _id,
Eduardo Sousa88d58a42019-05-30 13:18:36 +0100579 mapping["project"],
580 mapping["role"]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100581 )
Eduardo Sousa44603902019-06-04 08:10:32 +0100582
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100583 for mapping in mappings_to_add:
584 self.auth.assign_role_to_user(
Eduardo Sousa8c3df702019-06-04 09:20:01 +0100585 _id,
Eduardo Sousa88d58a42019-05-30 13:18:36 +0100586 mapping["project"],
587 mapping["role"]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100588 )
589
590 return content["_id"]
591 except ValidationError as e:
592 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
593
594 def list(self, session, filter_q=None):
595 """
596 Get a list of the topic that matches a filter
tierno65ca36d2019-02-12 19:27:52 +0100597 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100598 :param filter_q: filter of data to be applied
599 :return: The list, it can be empty if no one match the filter.
600 """
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100601 if not filter_q:
602 filter_q = {}
603
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100604 users = [self.format_on_show(user) for user in self.auth.get_user_list(filter_q)]
605
606 return users
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100607
tierno65ca36d2019-02-12 19:27:52 +0100608 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100609 """
610 Delete item by its internal _id
611
tierno65ca36d2019-02-12 19:27:52 +0100612 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100613 :param _id: server internal id
614 :param force: indicates if deletion must be forced in case of conflict
615 :param dry_run: make checking but do not delete
616 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
617 """
tiernob4844ab2019-05-23 08:42:12 +0000618 self.check_conflict_on_del(session, _id, None)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100619 if not dry_run:
620 v = self.auth.delete_user(_id)
621 return v
622 return None
623
624
625class ProjectTopicAuth(ProjectTopic):
tierno65ca36d2019-02-12 19:27:52 +0100626 # topic = "projects"
627 # topic_msg = "projects"
Eduardo Sousa44603902019-06-04 08:10:32 +0100628 schema_new = project_new_schema
629 schema_edit = project_edit_schema
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100630
631 def __init__(self, db, fs, msg, auth):
632 ProjectTopic.__init__(self, db, fs, msg)
633 self.auth = auth
634
tierno65ca36d2019-02-12 19:27:52 +0100635 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100636 """
637 Check that the data to be inserted is valid
638
tierno65ca36d2019-02-12 19:27:52 +0100639 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100640 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100641 :return: None or raises EngineException
642 """
643 project = indata.get("name")
644 project_list = list(map(lambda x: x["name"], self.auth.get_project_list()))
645
646 if project in project_list:
647 raise EngineException("project '{}' exists".format(project), HTTPStatus.CONFLICT)
648
tiernob4844ab2019-05-23 08:42:12 +0000649 def check_conflict_on_del(self, session, _id, db_content):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100650 """
651 Check if deletion can be done because of dependencies if it is not force. To override
652
tierno65ca36d2019-02-12 19:27:52 +0100653 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100654 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000655 :param db_content: The database content of this item _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100656 :return: None if ok or raises EngineException with the conflict
657 """
tierno38dcfeb2019-06-10 16:44:00 +0000658 # projects = self.auth.get_project_list()
659 # current_project = [project for project in projects
660 # if project["name"] in session["project_id"]][0]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100661
tierno38dcfeb2019-06-10 16:44:00 +0000662 if _id == session["project_id"]:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100663 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
664
tierno65ca36d2019-02-12 19:27:52 +0100665 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100666 """
667 Creates a new entry into the authentication backend.
668
669 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
670
671 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100672 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100673 :param indata: data to be inserted
674 :param kwargs: used to override the indata descriptor
675 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100676 :return: _id: identity of the inserted data.
677 """
678 try:
679 content = BaseTopic._remove_envelop(indata)
680
681 # Override descriptor with query string kwargs
682 BaseTopic._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100683 content = self._validate_input_new(content, session["force"])
684 self.check_conflict_on_new(session, content)
685 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100686 _id = self.auth.create_project(content["name"])
687 rollback.append({"topic": self.topic, "_id": _id})
688 # self._send_msg("create", content)
689 return _id
690 except ValidationError as e:
691 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
692
693 def show(self, session, _id):
694 """
695 Get complete information on an topic
696
tierno65ca36d2019-02-12 19:27:52 +0100697 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100698 :param _id: server internal id
699 :return: dictionary, raise exception if not found.
700 """
701 projects = [project for project in self.auth.get_project_list() if project["_id"] == _id]
702
703 if len(projects) == 1:
704 return projects[0]
705 elif len(projects) > 1:
706 raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
707 else:
708 raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
709
710 def list(self, session, filter_q=None):
711 """
712 Get a list of the topic that matches a filter
713
tierno65ca36d2019-02-12 19:27:52 +0100714 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100715 :param filter_q: filter of data to be applied
716 :return: The list, it can be empty if no one match the filter.
717 """
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100718 if not filter_q:
719 filter_q = {}
720
721 return self.auth.get_project_list(filter_q)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100722
tierno65ca36d2019-02-12 19:27:52 +0100723 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100724 """
725 Delete item by its internal _id
726
tierno65ca36d2019-02-12 19:27:52 +0100727 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100728 :param _id: server internal id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100729 :param dry_run: make checking but do not delete
730 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
731 """
tiernob4844ab2019-05-23 08:42:12 +0000732 self.check_conflict_on_del(session, _id, None)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100733 if not dry_run:
734 v = self.auth.delete_project(_id)
735 return v
736 return None
737
tierno4015b472019-06-10 13:57:29 +0000738 def edit(self, session, _id, indata=None, kwargs=None, content=None):
739 """
740 Updates a project entry.
741
742 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
743 :param _id:
744 :param indata: data to be inserted
745 :param kwargs: used to override the indata descriptor
746 :param content:
747 :return: _id: identity of the inserted data.
748 """
749 indata = self._remove_envelop(indata)
750
751 # Override descriptor with query string kwargs
752 if kwargs:
753 BaseTopic._update_input_with_kwargs(indata, kwargs)
754 try:
755 indata = self._validate_input_edit(indata, force=session["force"])
756
757 if not content:
758 content = self.show(session, _id)
759 self.check_conflict_on_edit(session, content, indata, _id=_id)
760 self.format_on_edit(content, indata)
761
762 if "name" in indata:
763 self.auth.update_project(content["_id"], indata["name"])
764 except ValidationError as e:
765 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
766
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100767
768class RoleTopicAuth(BaseTopic):
769 topic = "roles_operations"
770 topic_msg = "roles"
771 schema_new = roles_new_schema
772 schema_edit = roles_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100773 multiproject = False
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100774
775 def __init__(self, db, fs, msg, auth, ops):
776 BaseTopic.__init__(self, db, fs, msg)
777 self.auth = auth
778 self.operations = ops
779
780 @staticmethod
781 def validate_role_definition(operations, role_definitions):
782 """
783 Validates the role definition against the operations defined in
784 the resources to operations files.
785
786 :param operations: operations list
787 :param role_definitions: role definition to test
788 :return: None if ok, raises ValidationError exception on error
789 """
Eduardo Sousa867a2ee2019-05-29 09:53:36 +0100790 ignore_fields = ["_id", "_admin", "name"]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100791 for role_def in role_definitions.keys():
Eduardo Sousa37de0912019-05-23 02:17:22 +0100792 if role_def in ignore_fields:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100793 continue
Eduardo Sousac7689372019-06-04 16:01:46 +0100794 if role_def == "root":
Eduardo Sousa37de0912019-05-23 02:17:22 +0100795 if isinstance(role_definitions[role_def], bool):
796 continue
797 else:
798 raise ValidationError("Operation authorization \".\" should be True/False.")
Eduardo Sousac7689372019-06-04 16:01:46 +0100799 if role_def[-1] == ":":
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100800 raise ValidationError("Operation cannot end with \".\"")
Eduardo Sousac5a18892019-06-06 14:51:23 +0100801
delacruzramoc061f562019-04-05 11:00:02 +0200802 role_def_matches = [op for op in operations if op.startswith(role_def)]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100803
804 if len(role_def_matches) == 0:
805 raise ValidationError("No matching operation found.")
806
Eduardo Sousa37de0912019-05-23 02:17:22 +0100807 if not isinstance(role_definitions[role_def], bool):
808 raise ValidationError("Operation authorization {} should be True/False.".format(role_def))
809
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100810 def _validate_input_new(self, input, force=False):
811 """
812 Validates input user content for a new entry.
813
814 :param input: user input content for the new topic
815 :param force: may be used for being more tolerant
816 :return: The same input content, or a changed version of it.
817 """
818 if self.schema_new:
819 validate_input(input, self.schema_new)
Eduardo Sousa37de0912019-05-23 02:17:22 +0100820 self.validate_role_definition(self.operations, input)
Eduardo Sousac4650362019-06-04 13:24:22 +0100821
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100822 return input
823
824 def _validate_input_edit(self, input, force=False):
825 """
826 Validates input user content for updating an entry.
827
828 :param input: user input content for the new topic
829 :param force: may be used for being more tolerant
830 :return: The same input content, or a changed version of it.
831 """
832 if self.schema_edit:
833 validate_input(input, self.schema_edit)
Eduardo Sousa37de0912019-05-23 02:17:22 +0100834 self.validate_role_definition(self.operations, input)
Eduardo Sousac4650362019-06-04 13:24:22 +0100835
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100836 return input
837
tierno65ca36d2019-02-12 19:27:52 +0100838 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100839 """
840 Check that the data to be inserted is valid
841
tierno65ca36d2019-02-12 19:27:52 +0100842 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100843 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100844 :return: None or raises EngineException
845 """
846 role = indata.get("name")
847 role_list = list(map(lambda x: x["name"], self.auth.get_role_list()))
848
849 if role in role_list:
850 raise EngineException("role '{}' exists".format(role), HTTPStatus.CONFLICT)
851
tierno65ca36d2019-02-12 19:27:52 +0100852 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100853 """
854 Check that the data to be edited/uploaded is valid
855
tierno65ca36d2019-02-12 19:27:52 +0100856 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100857 :param final_content: data once modified
858 :param edit_content: incremental data that contains the modifications to apply
859 :param _id: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100860 :return: None or raises EngineException
861 """
862 roles = self.auth.get_role_list()
863 system_admin_role = [role for role in roles
Eduardo Sousac7689372019-06-04 16:01:46 +0100864 if role["name"] == "system_admin"][0]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100865
866 if _id == system_admin_role["_id"]:
867 raise EngineException("You cannot edit system_admin role", http_code=HTTPStatus.FORBIDDEN)
868
tiernob4844ab2019-05-23 08:42:12 +0000869 def check_conflict_on_del(self, session, _id, db_content):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100870 """
871 Check if deletion can be done because of dependencies if it is not force. To override
872
tierno65ca36d2019-02-12 19:27:52 +0100873 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100874 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000875 :param db_content: The database content of this item _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100876 :return: None if ok or raises EngineException with the conflict
877 """
878 roles = self.auth.get_role_list()
879 system_admin_role = [role for role in roles
Eduardo Sousa2bf65932019-06-06 14:20:58 +0100880 if role["name"] == "system_admin"][0]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100881
882 if _id == system_admin_role["_id"]:
883 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
884
885 @staticmethod
886 def format_on_new(content, project_id=None, make_public=False):
887 """
888 Modifies content descriptor to include _admin
889
890 :param content: descriptor to be modified
891 :param project_id: if included, it add project read/write permissions
892 :param make_public: if included it is generated as public for reading.
893 :return: None, but content is modified
894 """
895 now = time()
896 if "_admin" not in content:
897 content["_admin"] = {}
898 if not content["_admin"].get("created"):
899 content["_admin"]["created"] = now
900 content["_admin"]["modified"] = now
Eduardo Sousac4650362019-06-04 13:24:22 +0100901
902 if ":" in content.keys():
903 content["root"] = content[":"]
904 del content[":"]
905
Eduardo Sousa871f8882019-05-29 14:43:05 +0100906 if "root" not in content.keys():
907 content["root"] = False
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100908
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100909 @staticmethod
910 def format_on_edit(final_content, edit_content):
911 """
912 Modifies final_content descriptor to include the modified date.
913
914 :param final_content: final descriptor generated
915 :param edit_content: alterations to be include
916 :return: None, but final_content is modified
917 """
918 final_content["_admin"]["modified"] = time()
919
920 ignore_fields = ["_id", "name", "_admin"]
921 delete_keys = [key for key in final_content.keys() if key not in ignore_fields]
922
923 for key in delete_keys:
924 del final_content[key]
925
926 # Saving the role definition
Eduardo Sousa37de0912019-05-23 02:17:22 +0100927 for role_def, value in edit_content.items():
Eduardo Sousac4650362019-06-04 13:24:22 +0100928 final_content[role_def] = value
929
Eduardo Sousa871f8882019-05-29 14:43:05 +0100930 if ":" in final_content.keys():
931 final_content["root"] = final_content[":"]
932 del final_content[":"]
Eduardo Sousac4650362019-06-04 13:24:22 +0100933
Eduardo Sousa871f8882019-05-29 14:43:05 +0100934 if "root" not in final_content.keys():
935 final_content["root"] = False
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100936
937 @staticmethod
938 def format_on_show(content):
939 """
940 Modifies the content of the role information to separate the role
941 metadata from the role definition. Eases the reading process of the
942 role definition.
943
944 :param definition: role definition to be processed
945 """
Eduardo Sousab1d73122019-05-29 11:09:16 +0100946 content["_id"] = str(content["_id"])
947
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100948 def show(self, session, _id):
949 """
950 Get complete information on an topic
951
tierno65ca36d2019-02-12 19:27:52 +0100952 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100953 :param _id: server internal id
954 :return: dictionary, raise exception if not found.
955 """
Eduardo Sousadb04d1e2019-05-31 13:43:41 +0100956 filter_db = {"_id": _id}
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100957
958 role = self.db.get_one(self.topic, filter_db)
959 new_role = dict(role)
960 self.format_on_show(new_role)
961
962 return new_role
963
964 def list(self, session, filter_q=None):
965 """
966 Get a list of the topic that matches a filter
967
tierno65ca36d2019-02-12 19:27:52 +0100968 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100969 :param filter_q: filter of data to be applied
970 :return: The list, it can be empty if no one match the filter.
971 """
972 if not filter_q:
973 filter_q = {}
974
Eduardo Sousac4650362019-06-04 13:24:22 +0100975 if ":" in filter_q:
976 filter_q["root"] = filter_q[":"]
Eduardo Sousa867a2ee2019-05-29 09:53:36 +0100977
Eduardo Sousac4650362019-06-04 13:24:22 +0100978 for key in filter_q.keys():
979 if key == "name":
980 continue
981 filter_q[key] = filter_q[key] in ["True", "true"]
Eduardo Sousa867a2ee2019-05-29 09:53:36 +0100982
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100983 roles = self.db.get_list(self.topic, filter_q)
984 new_roles = []
985
986 for role in roles:
987 new_role = dict(role)
988 self.format_on_show(new_role)
989 new_roles.append(new_role)
990
991 return new_roles
992
tierno65ca36d2019-02-12 19:27:52 +0100993 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100994 """
995 Creates a new entry into database.
996
997 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100998 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100999 :param indata: data to be inserted
1000 :param kwargs: used to override the indata descriptor
1001 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001002 :return: _id: identity of the inserted data.
1003 """
1004 try:
1005 content = BaseTopic._remove_envelop(indata)
1006
1007 # Override descriptor with query string kwargs
1008 BaseTopic._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +01001009 content = self._validate_input_new(content, session["force"])
1010 self.check_conflict_on_new(session, content)
1011 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001012 role_name = content["name"]
1013 role = self.auth.create_role(role_name)
1014 content["_id"] = role["_id"]
1015 _id = self.db.create(self.topic, content)
1016 rollback.append({"topic": self.topic, "_id": _id})
1017 # self._send_msg("create", content)
1018 return _id
1019 except ValidationError as e:
1020 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1021
tierno65ca36d2019-02-12 19:27:52 +01001022 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001023 """
1024 Delete item by its internal _id
1025
tierno65ca36d2019-02-12 19:27:52 +01001026 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001027 :param _id: server internal id
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001028 :param dry_run: make checking but do not delete
1029 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1030 """
tiernob4844ab2019-05-23 08:42:12 +00001031 self.check_conflict_on_del(session, _id, None)
Eduardo Sousadb04d1e2019-05-31 13:43:41 +01001032 filter_q = {"_id": _id}
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001033 if not dry_run:
1034 self.auth.delete_role(_id)
1035 v = self.db.del_one(self.topic, filter_q)
1036 return v
1037 return None
1038
tierno65ca36d2019-02-12 19:27:52 +01001039 def edit(self, session, _id, indata=None, kwargs=None, content=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001040 """
1041 Updates a role entry.
1042
tierno65ca36d2019-02-12 19:27:52 +01001043 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001044 :param _id:
1045 :param indata: data to be inserted
1046 :param kwargs: used to override the indata descriptor
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001047 :param content:
1048 :return: _id: identity of the inserted data.
1049 """
1050 indata = self._remove_envelop(indata)
1051
1052 # Override descriptor with query string kwargs
1053 if kwargs:
tiernob4844ab2019-05-23 08:42:12 +00001054 self._update_input_with_kwargs(indata, kwargs)
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001055 try:
tierno65ca36d2019-02-12 19:27:52 +01001056 indata = self._validate_input_edit(indata, force=session["force"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001057
1058 if not content:
1059 content = self.show(session, _id)
tierno65ca36d2019-02-12 19:27:52 +01001060 self.check_conflict_on_edit(session, content, indata, _id=_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001061 self.format_on_edit(content, indata)
1062 self.db.replace(self.topic, _id, content)
1063 return id
1064 except ValidationError as e:
1065 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)