blob: 2bca416d1cd931d9e7c3275ae6d914556815cd60 [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
tierno65ca36d2019-02-12 19:27:52 +010067 def check_conflict_on_del(self, session, _id):
tiernob24258a2018-10-04 18:39:49 +020068 if _id == session["username"]:
69 raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
70
71 @staticmethod
72 def format_on_new(content, project_id=None, make_public=False):
73 BaseTopic.format_on_new(content, make_public=False)
delacruzramoc061f562019-04-05 11:00:02 +020074 # Removed so that the UUID is kept, to allow User Name modification
75 # content["_id"] = content["username"]
tiernob24258a2018-10-04 18:39:49 +020076 salt = uuid4().hex
77 content["_admin"]["salt"] = salt
78 if content.get("password"):
79 content["password"] = sha256(content["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
80
81 @staticmethod
82 def format_on_edit(final_content, edit_content):
83 BaseTopic.format_on_edit(final_content, edit_content)
84 if edit_content.get("password"):
85 salt = uuid4().hex
86 final_content["_admin"]["salt"] = salt
87 final_content["password"] = sha256(edit_content["password"].encode('utf-8') +
88 salt.encode('utf-8')).hexdigest()
89
tierno65ca36d2019-02-12 19:27:52 +010090 def edit(self, session, _id, indata=None, kwargs=None, content=None):
tiernob24258a2018-10-04 18:39:49 +020091 if not session["admin"]:
92 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +020093 # Names that look like UUIDs are not allowed
94 name = (indata if indata else kwargs).get("username")
95 if is_valid_uuid(name):
96 raise EngineException("Usernames that look like UUIDs are not allowed",
97 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno65ca36d2019-02-12 19:27:52 +010098 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
tiernob24258a2018-10-04 18:39:49 +020099
tierno65ca36d2019-02-12 19:27:52 +0100100 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200101 if not session["admin"]:
102 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200103 # Names that look like UUIDs are not allowed
104 name = indata["username"] if indata else kwargs["username"]
105 if is_valid_uuid(name):
106 raise EngineException("Usernames that look like UUIDs are not allowed",
107 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno65ca36d2019-02-12 19:27:52 +0100108 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
tiernob24258a2018-10-04 18:39:49 +0200109
110
111class ProjectTopic(BaseTopic):
112 topic = "projects"
113 topic_msg = "projects"
114 schema_new = project_new_schema
115 schema_edit = project_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100116 multiproject = False
tiernob24258a2018-10-04 18:39:49 +0200117
118 def __init__(self, db, fs, msg):
119 BaseTopic.__init__(self, db, fs, msg)
120
tierno65ca36d2019-02-12 19:27:52 +0100121 @staticmethod
122 def _get_project_filter(session):
123 """
124 Generates a filter dictionary for querying database users.
125 Current policy is admin can show all, non admin, only its own user.
126 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
127 :return:
128 """
129 if session["admin"]: # allows all
130 return {}
131 else:
132 return {"_id.cont": session["project_id"]}
133
134 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200135 if not indata.get("name"):
136 raise EngineException("missing 'name'")
137 # check name not exists
138 if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
139 raise EngineException("name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT)
140
141 @staticmethod
142 def format_on_new(content, project_id=None, make_public=False):
143 BaseTopic.format_on_new(content, None)
delacruzramoc061f562019-04-05 11:00:02 +0200144 # Removed so that the UUID is kept, to allow Project Name modification
145 # content["_id"] = content["name"]
tiernob24258a2018-10-04 18:39:49 +0200146
tierno65ca36d2019-02-12 19:27:52 +0100147 def check_conflict_on_del(self, session, _id):
148 if _id in session["project_id"]:
tiernob24258a2018-10-04 18:39:49 +0200149 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
tierno65ca36d2019-02-12 19:27:52 +0100150 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200151 return
152 _filter = {"projects": _id}
153 if self.db.get_list("users", _filter):
154 raise EngineException("There is some USER that contains this project", http_code=HTTPStatus.CONFLICT)
155
tierno65ca36d2019-02-12 19:27:52 +0100156 def edit(self, session, _id, indata=None, kwargs=None, content=None):
tiernob24258a2018-10-04 18:39:49 +0200157 if not session["admin"]:
158 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200159 # Names that look like UUIDs are not allowed
160 name = (indata if indata else kwargs).get("name")
161 if is_valid_uuid(name):
162 raise EngineException("Project names that look like UUIDs are not allowed",
163 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno65ca36d2019-02-12 19:27:52 +0100164 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
tiernob24258a2018-10-04 18:39:49 +0200165
tierno65ca36d2019-02-12 19:27:52 +0100166 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200167 if not session["admin"]:
168 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200169 # Names that look like UUIDs are not allowed
170 name = indata["name"] if indata else kwargs["name"]
171 if is_valid_uuid(name):
172 raise EngineException("Project names that look like UUIDs are not allowed",
173 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno65ca36d2019-02-12 19:27:52 +0100174 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
tiernob24258a2018-10-04 18:39:49 +0200175
176
177class VimAccountTopic(BaseTopic):
178 topic = "vim_accounts"
179 topic_msg = "vim_account"
180 schema_new = vim_account_new_schema
181 schema_edit = vim_account_edit_schema
tierno92c1c7d2018-11-12 15:22:37 +0100182 vim_config_encrypted = ("admin_password", "nsx_password", "vcenter_password")
tierno65ca36d2019-02-12 19:27:52 +0100183 multiproject = True
tiernob24258a2018-10-04 18:39:49 +0200184
185 def __init__(self, db, fs, msg):
186 BaseTopic.__init__(self, db, fs, msg)
187
tierno65ca36d2019-02-12 19:27:52 +0100188 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200189 self.check_unique_name(session, indata["name"], _id=None)
190
tierno65ca36d2019-02-12 19:27:52 +0100191 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
192 if not session["force"] and edit_content.get("name"):
tiernob24258a2018-10-04 18:39:49 +0200193 self.check_unique_name(session, edit_content["name"], _id=_id)
194
tierno92c1c7d2018-11-12 15:22:37 +0100195 # encrypt passwords
196 schema_version = final_content.get("schema_version")
197 if schema_version:
198 if edit_content.get("vim_password"):
199 final_content["vim_password"] = self.db.encrypt(edit_content["vim_password"],
200 schema_version=schema_version, salt=_id)
201 if edit_content.get("config"):
202 for p in self.vim_config_encrypted:
203 if edit_content["config"].get(p):
204 final_content["config"][p] = self.db.encrypt(edit_content["config"][p],
205 schema_version=schema_version, salt=_id)
206
207 def format_on_new(self, content, project_id=None, make_public=False):
208 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
209 content["schema_version"] = schema_version = "1.1"
210
211 # encrypt passwords
212 if content.get("vim_password"):
213 content["vim_password"] = self.db.encrypt(content["vim_password"], schema_version=schema_version,
214 salt=content["_id"])
215 if content.get("config"):
216 for p in self.vim_config_encrypted:
217 if content["config"].get(p):
218 content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version,
219 salt=content["_id"])
220
tiernob24258a2018-10-04 18:39:49 +0200221 content["_admin"]["operationalState"] = "PROCESSING"
222
tierno65ca36d2019-02-12 19:27:52 +0100223 def delete(self, session, _id, dry_run=False):
tiernob24258a2018-10-04 18:39:49 +0200224 """
225 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100226 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200227 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200228 :param dry_run: make checking but do not delete
229 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
230 """
231 # TODO add admin to filter, validate rights
tierno65ca36d2019-02-12 19:27:52 +0100232 if dry_run or session["force"]: # delete completely
233 return BaseTopic.delete(self, session, _id, dry_run)
tiernob24258a2018-10-04 18:39:49 +0200234 else: # if not, sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100235 v = BaseTopic.delete(self, session, _id, dry_run=True)
tiernob24258a2018-10-04 18:39:49 +0200236 self.db.set_one("vim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
237 self._send_msg("delete", {"_id": _id})
238 return v # TODO indicate an offline operation to return 202 ACCEPTED
239
240
tierno55ba2e62018-12-11 17:22:22 +0000241class WimAccountTopic(BaseTopic):
242 topic = "wim_accounts"
243 topic_msg = "wim_account"
244 schema_new = wim_account_new_schema
245 schema_edit = wim_account_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100246 multiproject = True
tierno55ba2e62018-12-11 17:22:22 +0000247 wim_config_encrypted = ()
248
249 def __init__(self, db, fs, msg):
250 BaseTopic.__init__(self, db, fs, msg)
251
tierno65ca36d2019-02-12 19:27:52 +0100252 def check_conflict_on_new(self, session, indata):
tierno55ba2e62018-12-11 17:22:22 +0000253 self.check_unique_name(session, indata["name"], _id=None)
254
tierno65ca36d2019-02-12 19:27:52 +0100255 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
256 if not session["force"] and edit_content.get("name"):
tierno55ba2e62018-12-11 17:22:22 +0000257 self.check_unique_name(session, edit_content["name"], _id=_id)
258
259 # encrypt passwords
260 schema_version = final_content.get("schema_version")
261 if schema_version:
262 if edit_content.get("wim_password"):
263 final_content["wim_password"] = self.db.encrypt(edit_content["wim_password"],
264 schema_version=schema_version, salt=_id)
265 if edit_content.get("config"):
266 for p in self.wim_config_encrypted:
267 if edit_content["config"].get(p):
268 final_content["config"][p] = self.db.encrypt(edit_content["config"][p],
269 schema_version=schema_version, salt=_id)
270
271 def format_on_new(self, content, project_id=None, make_public=False):
272 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
273 content["schema_version"] = schema_version = "1.1"
274
275 # encrypt passwords
276 if content.get("wim_password"):
277 content["wim_password"] = self.db.encrypt(content["wim_password"], schema_version=schema_version,
278 salt=content["_id"])
279 if content.get("config"):
280 for p in self.wim_config_encrypted:
281 if content["config"].get(p):
282 content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version,
283 salt=content["_id"])
284
285 content["_admin"]["operationalState"] = "PROCESSING"
286
tierno65ca36d2019-02-12 19:27:52 +0100287 def delete(self, session, _id, dry_run=False):
tierno55ba2e62018-12-11 17:22:22 +0000288 """
289 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100290 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno55ba2e62018-12-11 17:22:22 +0000291 :param _id: server internal id
tierno55ba2e62018-12-11 17:22:22 +0000292 :param dry_run: make checking but do not delete
293 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
294 """
295 # TODO add admin to filter, validate rights
tierno65ca36d2019-02-12 19:27:52 +0100296 if dry_run or session["force"]: # delete completely
297 return BaseTopic.delete(self, session, _id, dry_run)
tierno55ba2e62018-12-11 17:22:22 +0000298 else: # if not, sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100299 v = BaseTopic.delete(self, session, _id, dry_run=True)
tierno55ba2e62018-12-11 17:22:22 +0000300 self.db.set_one("wim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
301 self._send_msg("delete", {"_id": _id})
302 return v # TODO indicate an offline operation to return 202 ACCEPTED
303
304
tiernob24258a2018-10-04 18:39:49 +0200305class SdnTopic(BaseTopic):
306 topic = "sdns"
307 topic_msg = "sdn"
308 schema_new = sdn_new_schema
309 schema_edit = sdn_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100310 multiproject = True
tiernob24258a2018-10-04 18:39:49 +0200311
312 def __init__(self, db, fs, msg):
313 BaseTopic.__init__(self, db, fs, msg)
314
tierno65ca36d2019-02-12 19:27:52 +0100315 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200316 self.check_unique_name(session, indata["name"], _id=None)
317
tierno65ca36d2019-02-12 19:27:52 +0100318 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
319 if not session["force"] and edit_content.get("name"):
tiernob24258a2018-10-04 18:39:49 +0200320 self.check_unique_name(session, edit_content["name"], _id=_id)
321
tierno92c1c7d2018-11-12 15:22:37 +0100322 # encrypt passwords
323 schema_version = final_content.get("schema_version")
324 if schema_version and edit_content.get("password"):
325 final_content["password"] = self.db.encrypt(edit_content["password"], schema_version=schema_version,
326 salt=_id)
327
328 def format_on_new(self, content, project_id=None, make_public=False):
329 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
330 content["schema_version"] = schema_version = "1.1"
331 # encrypt passwords
332 if content.get("password"):
333 content["password"] = self.db.encrypt(content["password"], schema_version=schema_version,
334 salt=content["_id"])
335
tiernob24258a2018-10-04 18:39:49 +0200336 content["_admin"]["operationalState"] = "PROCESSING"
337
tierno65ca36d2019-02-12 19:27:52 +0100338 def delete(self, session, _id, dry_run=False):
tiernob24258a2018-10-04 18:39:49 +0200339 """
340 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100341 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200342 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200343 :param dry_run: make checking but do not delete
344 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
345 """
tierno65ca36d2019-02-12 19:27:52 +0100346 if dry_run or session["force"]: # delete completely
347 return BaseTopic.delete(self, session, _id, dry_run)
tiernob24258a2018-10-04 18:39:49 +0200348 else: # if not sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100349 v = BaseTopic.delete(self, session, _id, dry_run=True)
tiernob24258a2018-10-04 18:39:49 +0200350 self.db.set_one("sdns", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
351 self._send_msg("delete", {"_id": _id})
352 return v # TODO indicate an offline operation to return 202 ACCEPTED
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100353
354
355class UserTopicAuth(UserTopic):
tierno65ca36d2019-02-12 19:27:52 +0100356 # topic = "users"
357 # topic_msg = "users"
358 # schema_new = user_new_schema
359 # schema_edit = user_edit_schema
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100360
361 def __init__(self, db, fs, msg, auth):
362 UserTopic.__init__(self, db, fs, msg)
363 self.auth = auth
364
tierno65ca36d2019-02-12 19:27:52 +0100365 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100366 """
367 Check that the data to be inserted is valid
368
tierno65ca36d2019-02-12 19:27:52 +0100369 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100370 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100371 :return: None or raises EngineException
372 """
373 username = indata.get("username")
374 user_list = list(map(lambda x: x["username"], self.auth.get_user_list()))
375
376 if username in user_list:
377 raise EngineException("username '{}' exists".format(username), HTTPStatus.CONFLICT)
378
tierno65ca36d2019-02-12 19:27:52 +0100379 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100380 """
381 Check that the data to be edited/uploaded is valid
382
tierno65ca36d2019-02-12 19:27:52 +0100383 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100384 :param final_content: data once modified
385 :param edit_content: incremental data that contains the modifications to apply
386 :param _id: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100387 :return: None or raises EngineException
388 """
389 users = self.auth.get_user_list()
390 admin_user = [user for user in users if user["name"] == "admin"][0]
391
392 if _id == admin_user["_id"] and edit_content["project_role_mappings"]:
393 elem = {
394 "project": "admin",
395 "role": "system_admin"
396 }
397 if elem not in edit_content:
398 raise EngineException("You cannot remove system_admin role from admin user",
399 http_code=HTTPStatus.FORBIDDEN)
400
tierno65ca36d2019-02-12 19:27:52 +0100401 def check_conflict_on_del(self, session, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100402 """
403 Check if deletion can be done because of dependencies if it is not force. To override
404
tierno65ca36d2019-02-12 19:27:52 +0100405 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100406 :param _id: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100407 :return: None if ok or raises EngineException with the conflict
408 """
409 if _id == session["username"]:
410 raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
411
412 @staticmethod
413 def format_on_new(content, project_id=None, make_public=False):
414 """
415 Modifies content descriptor to include _id.
416
417 NOTE: No password salt required because the authentication backend
418 should handle these security concerns.
419
420 :param content: descriptor to be modified
421 :param make_public: if included it is generated as public for reading.
422 :return: None, but content is modified
423 """
424 BaseTopic.format_on_new(content, make_public=False)
425 content["_id"] = content["username"]
426 content["password"] = content["password"]
427
428 @staticmethod
429 def format_on_edit(final_content, edit_content):
430 """
431 Modifies final_content descriptor to include the modified date.
432
433 NOTE: No password salt required because the authentication backend
434 should handle these security concerns.
435
436 :param final_content: final descriptor generated
437 :param edit_content: alterations to be include
438 :return: None, but final_content is modified
439 """
440 BaseTopic.format_on_edit(final_content, edit_content)
441 if "password" in edit_content:
442 final_content["password"] = edit_content["password"]
443 else:
444 final_content["project_role_mappings"] = edit_content["project_role_mappings"]
445
tierno65ca36d2019-02-12 19:27:52 +0100446 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100447 """
448 Creates a new entry into the authentication backend.
449
450 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
451
452 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100453 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100454 :param indata: data to be inserted
455 :param kwargs: used to override the indata descriptor
456 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100457 :return: _id: identity of the inserted data.
458 """
459 try:
460 content = BaseTopic._remove_envelop(indata)
461
462 # Override descriptor with query string kwargs
463 BaseTopic._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100464 content = self._validate_input_new(content, session["force"])
465 self.check_conflict_on_new(session, content)
466 self.format_on_new(content, session["project_id"], make_public=session["public"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100467 _id = self.auth.create_user(content["username"], content["password"])
468 rollback.append({"topic": self.topic, "_id": _id})
469 del content["password"]
470 # self._send_msg("create", content)
471 return _id
472 except ValidationError as e:
473 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
474
475 def show(self, session, _id):
476 """
477 Get complete information on an topic
478
tierno65ca36d2019-02-12 19:27:52 +0100479 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100480 :param _id: server internal id
481 :return: dictionary, raise exception if not found.
482 """
483 users = [user for user in self.auth.get_user_list() if user["_id"] == _id]
484
485 if len(users) == 1:
486 return users[0]
487 elif len(users) > 1:
488 raise EngineException("Too many users found", HTTPStatus.CONFLICT)
489 else:
490 raise EngineException("User not found", HTTPStatus.NOT_FOUND)
491
tierno65ca36d2019-02-12 19:27:52 +0100492 def edit(self, session, _id, indata=None, kwargs=None, content=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100493 """
494 Updates an user entry.
495
tierno65ca36d2019-02-12 19:27:52 +0100496 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100497 :param _id:
498 :param indata: data to be inserted
499 :param kwargs: used to override the indata descriptor
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100500 :param content:
501 :return: _id: identity of the inserted data.
502 """
503 indata = self._remove_envelop(indata)
504
505 # Override descriptor with query string kwargs
506 if kwargs:
507 BaseTopic._update_input_with_kwargs(indata, kwargs)
508 try:
tierno65ca36d2019-02-12 19:27:52 +0100509 indata = self._validate_input_edit(indata, force=session["force"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100510
511 if not content:
512 content = self.show(session, _id)
tierno65ca36d2019-02-12 19:27:52 +0100513 self.check_conflict_on_edit(session, content, indata, _id=_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100514 self.format_on_edit(content, indata)
515
516 if "password" in content:
517 self.auth.change_password(content["name"], content["password"])
518 else:
519 users = self.auth.get_user_list()
520 user = [user for user in users if user["_id"] == content["_id"]][0]
521 original_mapping = []
522 edit_mapping = content["project_role_mappings"]
523
524 for project in user["projects"]:
525 for role in project["roles"]:
526 original_mapping += {
527 "project": project["name"],
528 "role": role["name"]
529 }
530
531 mappings_to_remove = [mapping for mapping in original_mapping
532 if mapping not in edit_mapping]
533
534 mappings_to_add = [mapping for mapping in edit_mapping
535 if mapping not in original_mapping]
536
537 for mapping in mappings_to_remove:
538 self.auth.remove_role_from_user(
539 user["name"],
540 mapping["project"],
541 mapping["role"]
542 )
543
544 for mapping in mappings_to_add:
545 self.auth.assign_role_to_user(
546 user["name"],
547 mapping["project"],
548 mapping["role"]
549 )
550
551 return content["_id"]
552 except ValidationError as e:
553 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
554
555 def list(self, session, filter_q=None):
556 """
557 Get a list of the topic that matches a filter
tierno65ca36d2019-02-12 19:27:52 +0100558 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100559 :param filter_q: filter of data to be applied
560 :return: The list, it can be empty if no one match the filter.
561 """
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100562 if not filter_q:
563 filter_q = {}
564
565 return self.auth.get_user_list(filter_q)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100566
tierno65ca36d2019-02-12 19:27:52 +0100567 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100568 """
569 Delete item by its internal _id
570
tierno65ca36d2019-02-12 19:27:52 +0100571 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100572 :param _id: server internal id
573 :param force: indicates if deletion must be forced in case of conflict
574 :param dry_run: make checking but do not delete
575 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
576 """
tierno65ca36d2019-02-12 19:27:52 +0100577 self.check_conflict_on_del(session, _id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100578 if not dry_run:
579 v = self.auth.delete_user(_id)
580 return v
581 return None
582
583
584class ProjectTopicAuth(ProjectTopic):
tierno65ca36d2019-02-12 19:27:52 +0100585 # topic = "projects"
586 # topic_msg = "projects"
587 # schema_new = project_new_schema
588 # schema_edit = project_edit_schema
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100589
590 def __init__(self, db, fs, msg, auth):
591 ProjectTopic.__init__(self, db, fs, msg)
592 self.auth = auth
593
tierno65ca36d2019-02-12 19:27:52 +0100594 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100595 """
596 Check that the data to be inserted is valid
597
tierno65ca36d2019-02-12 19:27:52 +0100598 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100599 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100600 :return: None or raises EngineException
601 """
602 project = indata.get("name")
603 project_list = list(map(lambda x: x["name"], self.auth.get_project_list()))
604
605 if project in project_list:
606 raise EngineException("project '{}' exists".format(project), HTTPStatus.CONFLICT)
607
tierno65ca36d2019-02-12 19:27:52 +0100608 def check_conflict_on_del(self, session, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100609 """
610 Check if deletion can be done because of dependencies if it is not force. To override
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: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100614 :return: None if ok or raises EngineException with the conflict
615 """
616 projects = self.auth.get_project_list()
617 current_project = [project for project in projects
618 if project["name"] == session["project_id"]][0]
619
620 if _id == current_project["_id"]:
621 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
622
tierno65ca36d2019-02-12 19:27:52 +0100623 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100624 """
625 Creates a new entry into the authentication backend.
626
627 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
628
629 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100630 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100631 :param indata: data to be inserted
632 :param kwargs: used to override the indata descriptor
633 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100634 :return: _id: identity of the inserted data.
635 """
636 try:
637 content = BaseTopic._remove_envelop(indata)
638
639 # Override descriptor with query string kwargs
640 BaseTopic._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100641 content = self._validate_input_new(content, session["force"])
642 self.check_conflict_on_new(session, content)
643 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100644 _id = self.auth.create_project(content["name"])
645 rollback.append({"topic": self.topic, "_id": _id})
646 # self._send_msg("create", content)
647 return _id
648 except ValidationError as e:
649 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
650
651 def show(self, session, _id):
652 """
653 Get complete information on an topic
654
tierno65ca36d2019-02-12 19:27:52 +0100655 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100656 :param _id: server internal id
657 :return: dictionary, raise exception if not found.
658 """
659 projects = [project for project in self.auth.get_project_list() if project["_id"] == _id]
660
661 if len(projects) == 1:
662 return projects[0]
663 elif len(projects) > 1:
664 raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
665 else:
666 raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
667
668 def list(self, session, filter_q=None):
669 """
670 Get a list of the topic that matches a filter
671
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 filter_q: filter of data to be applied
674 :return: The list, it can be empty if no one match the filter.
675 """
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100676 if not filter_q:
677 filter_q = {}
678
679 return self.auth.get_project_list(filter_q)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100680
tierno65ca36d2019-02-12 19:27:52 +0100681 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100682 """
683 Delete item by its internal _id
684
tierno65ca36d2019-02-12 19:27:52 +0100685 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100686 :param _id: server internal id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100687 :param dry_run: make checking but do not delete
688 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
689 """
tierno65ca36d2019-02-12 19:27:52 +0100690 self.check_conflict_on_del(session, _id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100691 if not dry_run:
692 v = self.auth.delete_project(_id)
693 return v
694 return None
695
696
697class RoleTopicAuth(BaseTopic):
698 topic = "roles_operations"
699 topic_msg = "roles"
700 schema_new = roles_new_schema
701 schema_edit = roles_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100702 multiproject = False
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100703
704 def __init__(self, db, fs, msg, auth, ops):
705 BaseTopic.__init__(self, db, fs, msg)
706 self.auth = auth
707 self.operations = ops
708
709 @staticmethod
710 def validate_role_definition(operations, role_definitions):
711 """
712 Validates the role definition against the operations defined in
713 the resources to operations files.
714
715 :param operations: operations list
716 :param role_definitions: role definition to test
717 :return: None if ok, raises ValidationError exception on error
718 """
719 for role_def in role_definitions.keys():
720 if role_def == ".":
721 continue
722 if role_def[-1] == ".":
723 raise ValidationError("Operation cannot end with \".\"")
724
delacruzramoc061f562019-04-05 11:00:02 +0200725 role_def_matches = [op for op in operations if op.startswith(role_def)]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100726
727 if len(role_def_matches) == 0:
728 raise ValidationError("No matching operation found.")
729
730 def _validate_input_new(self, input, force=False):
731 """
732 Validates input user content for a new entry.
733
734 :param input: user input content for the new topic
735 :param force: may be used for being more tolerant
736 :return: The same input content, or a changed version of it.
737 """
738 if self.schema_new:
739 validate_input(input, self.schema_new)
740 if "definition" in input and input["definition"]:
741 self.validate_role_definition(self.operations, input["definition"])
742 return input
743
744 def _validate_input_edit(self, input, force=False):
745 """
746 Validates input user content for updating an entry.
747
748 :param input: user input content for the new topic
749 :param force: may be used for being more tolerant
750 :return: The same input content, or a changed version of it.
751 """
752 if self.schema_edit:
753 validate_input(input, self.schema_edit)
754 if "definition" in input and input["definition"]:
755 self.validate_role_definition(self.operations, input["definition"])
756 return input
757
tierno65ca36d2019-02-12 19:27:52 +0100758 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100759 """
760 Check that the data to be inserted is valid
761
tierno65ca36d2019-02-12 19:27:52 +0100762 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100763 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100764 :return: None or raises EngineException
765 """
766 role = indata.get("name")
767 role_list = list(map(lambda x: x["name"], self.auth.get_role_list()))
768
769 if role in role_list:
770 raise EngineException("role '{}' exists".format(role), HTTPStatus.CONFLICT)
771
tierno65ca36d2019-02-12 19:27:52 +0100772 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100773 """
774 Check that the data to be edited/uploaded is valid
775
tierno65ca36d2019-02-12 19:27:52 +0100776 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100777 :param final_content: data once modified
778 :param edit_content: incremental data that contains the modifications to apply
779 :param _id: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100780 :return: None or raises EngineException
781 """
782 roles = self.auth.get_role_list()
783 system_admin_role = [role for role in roles
784 if roles["name"] == "system_admin"][0]
785
786 if _id == system_admin_role["_id"]:
787 raise EngineException("You cannot edit system_admin role", http_code=HTTPStatus.FORBIDDEN)
788
tierno65ca36d2019-02-12 19:27:52 +0100789 def check_conflict_on_del(self, session, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100790 """
791 Check if deletion can be done because of dependencies if it is not force. To override
792
tierno65ca36d2019-02-12 19:27:52 +0100793 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100794 :param _id: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100795 :return: None if ok or raises EngineException with the conflict
796 """
797 roles = self.auth.get_role_list()
798 system_admin_role = [role for role in roles
799 if roles["name"] == "system_admin"][0]
800
801 if _id == system_admin_role["_id"]:
802 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
803
804 @staticmethod
805 def format_on_new(content, project_id=None, make_public=False):
806 """
807 Modifies content descriptor to include _admin
808
809 :param content: descriptor to be modified
810 :param project_id: if included, it add project read/write permissions
811 :param make_public: if included it is generated as public for reading.
812 :return: None, but content is modified
813 """
814 now = time()
815 if "_admin" not in content:
816 content["_admin"] = {}
817 if not content["_admin"].get("created"):
818 content["_admin"]["created"] = now
819 content["_admin"]["modified"] = now
820 content["root"] = False
821
822 # Saving the role definition
823 if "definition" in content and content["definition"]:
824 for role_def, value in content["definition"].items():
825 if role_def == ".":
826 content["root"] = value
827 else:
828 content[role_def.replace(".", ":")] = value
829
830 # Cleaning undesired values
831 if "definition" in content:
832 del content["definition"]
833
834 @staticmethod
835 def format_on_edit(final_content, edit_content):
836 """
837 Modifies final_content descriptor to include the modified date.
838
839 :param final_content: final descriptor generated
840 :param edit_content: alterations to be include
841 :return: None, but final_content is modified
842 """
843 final_content["_admin"]["modified"] = time()
844
845 ignore_fields = ["_id", "name", "_admin"]
846 delete_keys = [key for key in final_content.keys() if key not in ignore_fields]
847
848 for key in delete_keys:
849 del final_content[key]
850
851 # Saving the role definition
852 if "definition" in edit_content and edit_content["definition"]:
853 for role_def, value in edit_content["definition"].items():
854 if role_def == ".":
855 final_content["root"] = value
856 else:
857 final_content[role_def.replace(".", ":")] = value
858
859 if "root" not in final_content:
860 final_content["root"] = False
861
862 @staticmethod
863 def format_on_show(content):
864 """
865 Modifies the content of the role information to separate the role
866 metadata from the role definition. Eases the reading process of the
867 role definition.
868
869 :param definition: role definition to be processed
870 """
871 ignore_fields = ["_admin", "_id", "name", "root"]
872 content_keys = list(content.keys())
873 definition = dict(content)
874
875 for key in content_keys:
876 if key in ignore_fields:
877 del definition[key]
878 if ":" not in key:
879 del content[key]
880 continue
881 definition[key.replace(":", ".")] = definition[key]
882 del definition[key]
883 del content[key]
884
885 content["definition"] = definition
886
887 def show(self, session, _id):
888 """
889 Get complete information on an topic
890
tierno65ca36d2019-02-12 19:27:52 +0100891 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100892 :param _id: server internal id
893 :return: dictionary, raise exception if not found.
894 """
895 filter_db = self._get_project_filter(session, write=False, show_all=True)
896 filter_db["_id"] = _id
897
898 role = self.db.get_one(self.topic, filter_db)
899 new_role = dict(role)
900 self.format_on_show(new_role)
901
902 return new_role
903
904 def list(self, session, filter_q=None):
905 """
906 Get a list of the topic that matches a filter
907
tierno65ca36d2019-02-12 19:27:52 +0100908 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100909 :param filter_q: filter of data to be applied
910 :return: The list, it can be empty if no one match the filter.
911 """
912 if not filter_q:
913 filter_q = {}
914
915 roles = self.db.get_list(self.topic, filter_q)
916 new_roles = []
917
918 for role in roles:
919 new_role = dict(role)
920 self.format_on_show(new_role)
921 new_roles.append(new_role)
922
923 return new_roles
924
tierno65ca36d2019-02-12 19:27:52 +0100925 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100926 """
927 Creates a new entry into database.
928
929 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100930 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100931 :param indata: data to be inserted
932 :param kwargs: used to override the indata descriptor
933 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100934 :return: _id: identity of the inserted data.
935 """
936 try:
937 content = BaseTopic._remove_envelop(indata)
938
939 # Override descriptor with query string kwargs
940 BaseTopic._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100941 content = self._validate_input_new(content, session["force"])
942 self.check_conflict_on_new(session, content)
943 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100944 role_name = content["name"]
945 role = self.auth.create_role(role_name)
946 content["_id"] = role["_id"]
947 _id = self.db.create(self.topic, content)
948 rollback.append({"topic": self.topic, "_id": _id})
949 # self._send_msg("create", content)
950 return _id
951 except ValidationError as e:
952 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
953
tierno65ca36d2019-02-12 19:27:52 +0100954 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100955 """
956 Delete item by its internal _id
957
tierno65ca36d2019-02-12 19:27:52 +0100958 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100959 :param _id: server internal id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100960 :param dry_run: make checking but do not delete
961 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
962 """
tierno65ca36d2019-02-12 19:27:52 +0100963 self.check_conflict_on_del(session, _id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100964 filter_q = self._get_project_filter(session, write=True, show_all=True)
965 filter_q["_id"] = _id
966 if not dry_run:
967 self.auth.delete_role(_id)
968 v = self.db.del_one(self.topic, filter_q)
969 return v
970 return None
971
tierno65ca36d2019-02-12 19:27:52 +0100972 def edit(self, session, _id, indata=None, kwargs=None, content=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100973 """
974 Updates a role entry.
975
tierno65ca36d2019-02-12 19:27:52 +0100976 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100977 :param _id:
978 :param indata: data to be inserted
979 :param kwargs: used to override the indata descriptor
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100980 :param content:
981 :return: _id: identity of the inserted data.
982 """
983 indata = self._remove_envelop(indata)
984
985 # Override descriptor with query string kwargs
986 if kwargs:
987 BaseTopic._update_input_with_kwargs(indata, kwargs)
988 try:
tierno65ca36d2019-02-12 19:27:52 +0100989 indata = self._validate_input_edit(indata, force=session["force"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100990
991 if not content:
992 content = self.show(session, _id)
tierno65ca36d2019-02-12 19:27:52 +0100993 self.check_conflict_on_edit(session, content, indata, _id=_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100994 self.format_on_edit(content, indata)
995 self.db.replace(self.topic, _id, content)
996 return id
997 except ValidationError as e:
998 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)