blob: 85fe9ff20141666c79ede243ff6b93f6e247e06b [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
delacruzramoceb8baf2019-06-21 14:25:38 +020028from authconn_keystone import AuthconnKeystone
tiernob24258a2018-10-04 18:39:49 +020029
30__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
31
32
33class UserTopic(BaseTopic):
34 topic = "users"
35 topic_msg = "users"
36 schema_new = user_new_schema
37 schema_edit = user_edit_schema
tierno65ca36d2019-02-12 19:27:52 +010038 multiproject = False
tiernob24258a2018-10-04 18:39:49 +020039
40 def __init__(self, db, fs, msg):
41 BaseTopic.__init__(self, db, fs, msg)
42
43 @staticmethod
tierno65ca36d2019-02-12 19:27:52 +010044 def _get_project_filter(session):
tiernob24258a2018-10-04 18:39:49 +020045 """
46 Generates a filter dictionary for querying database users.
47 Current policy is admin can show all, non admin, only its own user.
tierno65ca36d2019-02-12 19:27:52 +010048 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +020049 :return:
50 """
51 if session["admin"]: # allows all
52 return {}
53 else:
54 return {"username": session["username"]}
55
tierno65ca36d2019-02-12 19:27:52 +010056 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +020057 # check username not exists
58 if self.db.get_one(self.topic, {"username": indata.get("username")}, fail_on_empty=False, fail_on_more=False):
59 raise EngineException("username '{}' exists".format(indata["username"]), HTTPStatus.CONFLICT)
60 # check projects
tierno65ca36d2019-02-12 19:27:52 +010061 if not session["force"]:
delacruzramoceb8baf2019-06-21 14:25:38 +020062 for p in indata.get("projects") or []:
delacruzramoc061f562019-04-05 11:00:02 +020063 # To allow project addressing by Name as well as ID
64 if not self.db.get_one("projects", {BaseTopic.id_field("projects", p): p}, fail_on_empty=False,
65 fail_on_more=False):
66 raise EngineException("project '{}' does not exist".format(p), HTTPStatus.CONFLICT)
tiernob24258a2018-10-04 18:39:49 +020067
tiernob4844ab2019-05-23 08:42:12 +000068 def check_conflict_on_del(self, session, _id, db_content):
69 """
70 Check if deletion can be done because of dependencies if it is not force. To override
71 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
72 :param _id: internal _id
73 :param db_content: The database content of this item _id
74 :return: None if ok or raises EngineException with the conflict
75 """
tiernob24258a2018-10-04 18:39:49 +020076 if _id == session["username"]:
77 raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
78
79 @staticmethod
80 def format_on_new(content, project_id=None, make_public=False):
81 BaseTopic.format_on_new(content, make_public=False)
delacruzramoc061f562019-04-05 11:00:02 +020082 # Removed so that the UUID is kept, to allow User Name modification
83 # content["_id"] = content["username"]
tiernob24258a2018-10-04 18:39:49 +020084 salt = uuid4().hex
85 content["_admin"]["salt"] = salt
86 if content.get("password"):
87 content["password"] = sha256(content["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
Eduardo Sousa339ed782019-05-28 14:25:00 +010088 if content.get("project_role_mappings"):
89 projects = [mapping[0] for mapping in content["project_role_mappings"]]
90
91 if content.get("projects"):
92 content["projects"] += projects
93 else:
94 content["projects"] = projects
tiernob24258a2018-10-04 18:39:49 +020095
96 @staticmethod
97 def format_on_edit(final_content, edit_content):
98 BaseTopic.format_on_edit(final_content, edit_content)
99 if edit_content.get("password"):
100 salt = uuid4().hex
101 final_content["_admin"]["salt"] = salt
102 final_content["password"] = sha256(edit_content["password"].encode('utf-8') +
103 salt.encode('utf-8')).hexdigest()
104
tierno65ca36d2019-02-12 19:27:52 +0100105 def edit(self, session, _id, indata=None, kwargs=None, content=None):
tiernob24258a2018-10-04 18:39:49 +0200106 if not session["admin"]:
107 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200108 # Names that look like UUIDs are not allowed
109 name = (indata if indata else kwargs).get("username")
110 if is_valid_uuid(name):
111 raise EngineException("Usernames that look like UUIDs are not allowed",
112 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno65ca36d2019-02-12 19:27:52 +0100113 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
tiernob24258a2018-10-04 18:39:49 +0200114
tierno65ca36d2019-02-12 19:27:52 +0100115 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200116 if not session["admin"]:
117 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200118 # Names that look like UUIDs are not allowed
119 name = indata["username"] if indata else kwargs["username"]
120 if is_valid_uuid(name):
121 raise EngineException("Usernames that look like UUIDs are not allowed",
122 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno65ca36d2019-02-12 19:27:52 +0100123 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
tiernob24258a2018-10-04 18:39:49 +0200124
125
126class ProjectTopic(BaseTopic):
127 topic = "projects"
128 topic_msg = "projects"
129 schema_new = project_new_schema
130 schema_edit = project_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100131 multiproject = False
tiernob24258a2018-10-04 18:39:49 +0200132
133 def __init__(self, db, fs, msg):
134 BaseTopic.__init__(self, db, fs, msg)
135
tierno65ca36d2019-02-12 19:27:52 +0100136 @staticmethod
137 def _get_project_filter(session):
138 """
139 Generates a filter dictionary for querying database users.
140 Current policy is admin can show all, non admin, only its own user.
141 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
142 :return:
143 """
144 if session["admin"]: # allows all
145 return {}
146 else:
147 return {"_id.cont": session["project_id"]}
148
149 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200150 if not indata.get("name"):
151 raise EngineException("missing 'name'")
152 # check name not exists
153 if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
154 raise EngineException("name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT)
155
156 @staticmethod
157 def format_on_new(content, project_id=None, make_public=False):
158 BaseTopic.format_on_new(content, None)
delacruzramoc061f562019-04-05 11:00:02 +0200159 # Removed so that the UUID is kept, to allow Project Name modification
160 # content["_id"] = content["name"]
tiernob24258a2018-10-04 18:39:49 +0200161
tiernob4844ab2019-05-23 08:42:12 +0000162 def check_conflict_on_del(self, session, _id, db_content):
163 """
164 Check if deletion can be done because of dependencies if it is not force. To override
165 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
166 :param _id: internal _id
167 :param db_content: The database content of this item _id
168 :return: None if ok or raises EngineException with the conflict
169 """
tierno65ca36d2019-02-12 19:27:52 +0100170 if _id in session["project_id"]:
tiernob24258a2018-10-04 18:39:49 +0200171 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
tierno65ca36d2019-02-12 19:27:52 +0100172 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200173 return
174 _filter = {"projects": _id}
175 if self.db.get_list("users", _filter):
176 raise EngineException("There is some USER that contains this project", http_code=HTTPStatus.CONFLICT)
177
tierno65ca36d2019-02-12 19:27:52 +0100178 def edit(self, session, _id, indata=None, kwargs=None, content=None):
tiernob24258a2018-10-04 18:39:49 +0200179 if not session["admin"]:
180 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200181 # Names that look like UUIDs are not allowed
182 name = (indata if indata else kwargs).get("name")
183 if is_valid_uuid(name):
184 raise EngineException("Project names that look like UUIDs are not allowed",
185 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno65ca36d2019-02-12 19:27:52 +0100186 return BaseTopic.edit(self, session, _id, indata=indata, kwargs=kwargs, content=content)
tiernob24258a2018-10-04 18:39:49 +0200187
tierno65ca36d2019-02-12 19:27:52 +0100188 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200189 if not session["admin"]:
190 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoc061f562019-04-05 11:00:02 +0200191 # Names that look like UUIDs are not allowed
192 name = indata["name"] if indata else kwargs["name"]
193 if is_valid_uuid(name):
194 raise EngineException("Project names that look like UUIDs are not allowed",
195 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno65ca36d2019-02-12 19:27:52 +0100196 return BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
tiernob24258a2018-10-04 18:39:49 +0200197
198
199class VimAccountTopic(BaseTopic):
200 topic = "vim_accounts"
201 topic_msg = "vim_account"
202 schema_new = vim_account_new_schema
203 schema_edit = vim_account_edit_schema
tierno92c1c7d2018-11-12 15:22:37 +0100204 vim_config_encrypted = ("admin_password", "nsx_password", "vcenter_password")
tierno65ca36d2019-02-12 19:27:52 +0100205 multiproject = True
tiernob24258a2018-10-04 18:39:49 +0200206
207 def __init__(self, db, fs, msg):
208 BaseTopic.__init__(self, db, fs, msg)
209
tierno65ca36d2019-02-12 19:27:52 +0100210 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200211 self.check_unique_name(session, indata["name"], _id=None)
212
tierno65ca36d2019-02-12 19:27:52 +0100213 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
214 if not session["force"] and edit_content.get("name"):
tiernob24258a2018-10-04 18:39:49 +0200215 self.check_unique_name(session, edit_content["name"], _id=_id)
216
tierno92c1c7d2018-11-12 15:22:37 +0100217 # encrypt passwords
218 schema_version = final_content.get("schema_version")
219 if schema_version:
220 if edit_content.get("vim_password"):
221 final_content["vim_password"] = self.db.encrypt(edit_content["vim_password"],
222 schema_version=schema_version, salt=_id)
223 if edit_content.get("config"):
224 for p in self.vim_config_encrypted:
225 if edit_content["config"].get(p):
226 final_content["config"][p] = self.db.encrypt(edit_content["config"][p],
227 schema_version=schema_version, salt=_id)
228
229 def format_on_new(self, content, project_id=None, make_public=False):
230 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
231 content["schema_version"] = schema_version = "1.1"
232
233 # encrypt passwords
234 if content.get("vim_password"):
235 content["vim_password"] = self.db.encrypt(content["vim_password"], schema_version=schema_version,
236 salt=content["_id"])
237 if content.get("config"):
238 for p in self.vim_config_encrypted:
239 if content["config"].get(p):
240 content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version,
241 salt=content["_id"])
242
tiernob24258a2018-10-04 18:39:49 +0200243 content["_admin"]["operationalState"] = "PROCESSING"
244
tierno65ca36d2019-02-12 19:27:52 +0100245 def delete(self, session, _id, dry_run=False):
tiernob24258a2018-10-04 18:39:49 +0200246 """
247 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100248 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200249 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200250 :param dry_run: make checking but do not delete
251 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
252 """
253 # TODO add admin to filter, validate rights
tierno65ca36d2019-02-12 19:27:52 +0100254 if dry_run or session["force"]: # delete completely
255 return BaseTopic.delete(self, session, _id, dry_run)
tiernob24258a2018-10-04 18:39:49 +0200256 else: # if not, sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100257 v = BaseTopic.delete(self, session, _id, dry_run=True)
tiernob24258a2018-10-04 18:39:49 +0200258 self.db.set_one("vim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
259 self._send_msg("delete", {"_id": _id})
260 return v # TODO indicate an offline operation to return 202 ACCEPTED
261
262
tierno55ba2e62018-12-11 17:22:22 +0000263class WimAccountTopic(BaseTopic):
264 topic = "wim_accounts"
265 topic_msg = "wim_account"
266 schema_new = wim_account_new_schema
267 schema_edit = wim_account_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100268 multiproject = True
tierno55ba2e62018-12-11 17:22:22 +0000269 wim_config_encrypted = ()
270
271 def __init__(self, db, fs, msg):
272 BaseTopic.__init__(self, db, fs, msg)
273
tierno65ca36d2019-02-12 19:27:52 +0100274 def check_conflict_on_new(self, session, indata):
tierno55ba2e62018-12-11 17:22:22 +0000275 self.check_unique_name(session, indata["name"], _id=None)
276
tierno65ca36d2019-02-12 19:27:52 +0100277 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
278 if not session["force"] and edit_content.get("name"):
tierno55ba2e62018-12-11 17:22:22 +0000279 self.check_unique_name(session, edit_content["name"], _id=_id)
280
281 # encrypt passwords
282 schema_version = final_content.get("schema_version")
283 if schema_version:
284 if edit_content.get("wim_password"):
285 final_content["wim_password"] = self.db.encrypt(edit_content["wim_password"],
286 schema_version=schema_version, salt=_id)
287 if edit_content.get("config"):
288 for p in self.wim_config_encrypted:
289 if edit_content["config"].get(p):
290 final_content["config"][p] = self.db.encrypt(edit_content["config"][p],
291 schema_version=schema_version, salt=_id)
292
293 def format_on_new(self, content, project_id=None, make_public=False):
294 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
295 content["schema_version"] = schema_version = "1.1"
296
297 # encrypt passwords
298 if content.get("wim_password"):
299 content["wim_password"] = self.db.encrypt(content["wim_password"], schema_version=schema_version,
300 salt=content["_id"])
301 if content.get("config"):
302 for p in self.wim_config_encrypted:
303 if content["config"].get(p):
304 content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version,
305 salt=content["_id"])
306
307 content["_admin"]["operationalState"] = "PROCESSING"
308
tierno65ca36d2019-02-12 19:27:52 +0100309 def delete(self, session, _id, dry_run=False):
tierno55ba2e62018-12-11 17:22:22 +0000310 """
311 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100312 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno55ba2e62018-12-11 17:22:22 +0000313 :param _id: server internal id
tierno55ba2e62018-12-11 17:22:22 +0000314 :param dry_run: make checking but do not delete
315 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
316 """
317 # TODO add admin to filter, validate rights
tierno65ca36d2019-02-12 19:27:52 +0100318 if dry_run or session["force"]: # delete completely
319 return BaseTopic.delete(self, session, _id, dry_run)
tierno55ba2e62018-12-11 17:22:22 +0000320 else: # if not, sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100321 v = BaseTopic.delete(self, session, _id, dry_run=True)
tierno55ba2e62018-12-11 17:22:22 +0000322 self.db.set_one("wim_accounts", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
323 self._send_msg("delete", {"_id": _id})
324 return v # TODO indicate an offline operation to return 202 ACCEPTED
325
326
tiernob24258a2018-10-04 18:39:49 +0200327class SdnTopic(BaseTopic):
328 topic = "sdns"
329 topic_msg = "sdn"
330 schema_new = sdn_new_schema
331 schema_edit = sdn_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100332 multiproject = True
tiernob24258a2018-10-04 18:39:49 +0200333
334 def __init__(self, db, fs, msg):
335 BaseTopic.__init__(self, db, fs, msg)
336
tierno65ca36d2019-02-12 19:27:52 +0100337 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200338 self.check_unique_name(session, indata["name"], _id=None)
339
tierno65ca36d2019-02-12 19:27:52 +0100340 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
341 if not session["force"] and edit_content.get("name"):
tiernob24258a2018-10-04 18:39:49 +0200342 self.check_unique_name(session, edit_content["name"], _id=_id)
343
tierno92c1c7d2018-11-12 15:22:37 +0100344 # encrypt passwords
345 schema_version = final_content.get("schema_version")
346 if schema_version and edit_content.get("password"):
347 final_content["password"] = self.db.encrypt(edit_content["password"], schema_version=schema_version,
348 salt=_id)
349
350 def format_on_new(self, content, project_id=None, make_public=False):
351 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
352 content["schema_version"] = schema_version = "1.1"
353 # encrypt passwords
354 if content.get("password"):
355 content["password"] = self.db.encrypt(content["password"], schema_version=schema_version,
356 salt=content["_id"])
357
tiernob24258a2018-10-04 18:39:49 +0200358 content["_admin"]["operationalState"] = "PROCESSING"
359
tierno65ca36d2019-02-12 19:27:52 +0100360 def delete(self, session, _id, dry_run=False):
tiernob24258a2018-10-04 18:39:49 +0200361 """
362 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100363 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200364 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200365 :param dry_run: make checking but do not delete
366 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
367 """
tierno65ca36d2019-02-12 19:27:52 +0100368 if dry_run or session["force"]: # delete completely
369 return BaseTopic.delete(self, session, _id, dry_run)
tiernob24258a2018-10-04 18:39:49 +0200370 else: # if not sent to kafka
tierno65ca36d2019-02-12 19:27:52 +0100371 v = BaseTopic.delete(self, session, _id, dry_run=True)
tiernob24258a2018-10-04 18:39:49 +0200372 self.db.set_one("sdns", {"_id": _id}, {"_admin.to_delete": True}) # TODO change status
373 self._send_msg("delete", {"_id": _id})
374 return v # TODO indicate an offline operation to return 202 ACCEPTED
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100375
376
377class UserTopicAuth(UserTopic):
tierno65ca36d2019-02-12 19:27:52 +0100378 # topic = "users"
379 # topic_msg = "users"
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100380 schema_new = user_new_schema
381 schema_edit = user_edit_schema
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100382
383 def __init__(self, db, fs, msg, auth):
384 UserTopic.__init__(self, db, fs, msg)
385 self.auth = auth
386
tierno65ca36d2019-02-12 19:27:52 +0100387 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100388 """
389 Check that the data to be inserted is valid
390
tierno65ca36d2019-02-12 19:27:52 +0100391 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100392 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100393 :return: None or raises EngineException
394 """
395 username = indata.get("username")
tiernocf042d32019-06-13 09:06:40 +0000396 if is_valid_uuid(username):
delacruzramoceb8baf2019-06-21 14:25:38 +0200397 raise EngineException("username '{}' cannot have a uuid format".format(username),
tiernocf042d32019-06-13 09:06:40 +0000398 HTTPStatus.UNPROCESSABLE_ENTITY)
399
400 # Check that username is not used, regardless keystone already checks this
401 if self.auth.get_user_list(filter_q={"name": username}):
402 raise EngineException("username '{}' is already used".format(username), HTTPStatus.CONFLICT)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100403
Eduardo Sousa339ed782019-05-28 14:25:00 +0100404 if "projects" in indata.keys():
tierno701018c2019-06-25 11:13:14 +0000405 # convert to new format project_role_mappings
406 if not indata.get("project_role_mappings"):
407 indata["project_role_mappings"] = []
408 for project in indata["projects"]:
409 indata["project_role_mappings"].append({"project": project, "role": "project_user"})
410 # raise EngineException("Format invalid: the keyword 'projects' is not allowed for keystone authentication",
411 # HTTPStatus.BAD_REQUEST)
Eduardo Sousa339ed782019-05-28 14:25:00 +0100412
tierno65ca36d2019-02-12 19:27:52 +0100413 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100414 """
415 Check that the data to be edited/uploaded is valid
416
tierno65ca36d2019-02-12 19:27:52 +0100417 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100418 :param final_content: data once modified
419 :param edit_content: incremental data that contains the modifications to apply
420 :param _id: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100421 :return: None or raises EngineException
422 """
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100423
tiernocf042d32019-06-13 09:06:40 +0000424 if "username" in edit_content:
425 username = edit_content.get("username")
426 if is_valid_uuid(username):
delacruzramoceb8baf2019-06-21 14:25:38 +0200427 raise EngineException("username '{}' cannot have an uuid format".format(username),
tiernocf042d32019-06-13 09:06:40 +0000428 HTTPStatus.UNPROCESSABLE_ENTITY)
429
430 # Check that username is not used, regardless keystone already checks this
431 if self.auth.get_user_list(filter_q={"name": username}):
432 raise EngineException("username '{}' is already used".format(username), HTTPStatus.CONFLICT)
433
434 if final_content["username"] == "admin":
435 for mapping in edit_content.get("remove_project_role_mappings", ()):
436 if mapping["project"] == "admin" and mapping.get("role") in (None, "system_admin"):
437 # TODO make this also available for project id and role id
438 raise EngineException("You cannot remove system_admin role from admin user",
439 http_code=HTTPStatus.FORBIDDEN)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100440
tiernob4844ab2019-05-23 08:42:12 +0000441 def check_conflict_on_del(self, session, _id, db_content):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100442 """
443 Check if deletion can be done because of dependencies if it is not force. To override
tierno65ca36d2019-02-12 19:27:52 +0100444 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100445 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000446 :param db_content: The database content of this item _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100447 :return: None if ok or raises EngineException with the conflict
448 """
tiernocf042d32019-06-13 09:06:40 +0000449 if db_content["username"] == session["username"]:
450 raise EngineException("You cannot delete your own login user ", http_code=HTTPStatus.CONFLICT)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100451
tiernocf042d32019-06-13 09:06:40 +0000452 # @staticmethod
453 # def format_on_new(content, project_id=None, make_public=False):
454 # """
455 # Modifies content descriptor to include _id.
456 #
457 # NOTE: No password salt required because the authentication backend
458 # should handle these security concerns.
459 #
460 # :param content: descriptor to be modified
461 # :param make_public: if included it is generated as public for reading.
462 # :return: None, but content is modified
463 # """
464 # BaseTopic.format_on_new(content, make_public=False)
465 # content["_id"] = content["username"]
466 # content["password"] = content["password"]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100467
tiernocf042d32019-06-13 09:06:40 +0000468 # @staticmethod
469 # def format_on_edit(final_content, edit_content):
470 # """
471 # Modifies final_content descriptor to include the modified date.
472 #
473 # NOTE: No password salt required because the authentication backend
474 # should handle these security concerns.
475 #
476 # :param final_content: final descriptor generated
477 # :param edit_content: alterations to be include
478 # :return: None, but final_content is modified
479 # """
480 # BaseTopic.format_on_edit(final_content, edit_content)
481 # if "password" in edit_content:
482 # final_content["password"] = edit_content["password"]
483 # else:
484 # final_content["project_role_mappings"] = edit_content["project_role_mappings"]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100485
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100486 @staticmethod
487 def format_on_show(content):
488 """
Eduardo Sousa44603902019-06-04 08:10:32 +0100489 Modifies the content of the role information to separate the role
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100490 metadata from the role definition.
491 """
492 project_role_mappings = []
493
494 for project in content["projects"]:
495 for role in project["roles"]:
tiernocf042d32019-06-13 09:06:40 +0000496 project_role_mappings.append({"project": project["_id"],
497 "project_name": project["name"],
498 "role": role["_id"],
499 "role_name": role["name"]})
Eduardo Sousa44603902019-06-04 08:10:32 +0100500
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100501 del content["projects"]
502 content["project_role_mappings"] = project_role_mappings
503
Eduardo Sousa0b1d61b2019-05-30 19:55:52 +0100504 return content
505
tierno65ca36d2019-02-12 19:27:52 +0100506 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100507 """
508 Creates a new entry into the authentication backend.
509
510 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
511
512 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100513 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100514 :param indata: data to be inserted
515 :param kwargs: used to override the indata descriptor
516 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100517 :return: _id: identity of the inserted data.
518 """
519 try:
520 content = BaseTopic._remove_envelop(indata)
521
522 # Override descriptor with query string kwargs
523 BaseTopic._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100524 content = self._validate_input_new(content, session["force"])
525 self.check_conflict_on_new(session, content)
tiernocf042d32019-06-13 09:06:40 +0000526 # self.format_on_new(content, session["project_id"], make_public=session["public"])
Eduardo Sousa44603902019-06-04 08:10:32 +0100527 _id = self.auth.create_user(content["username"], content["password"])["_id"]
528
Eduardo Sousaa519a962019-06-06 15:00:50 +0100529 if "project_role_mappings" in content.keys():
530 for mapping in content["project_role_mappings"]:
531 self.auth.assign_role_to_user(_id, mapping["project"], mapping["role"])
Eduardo Sousa44603902019-06-04 08:10:32 +0100532
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100533 rollback.append({"topic": self.topic, "_id": _id})
tiernocf042d32019-06-13 09:06:40 +0000534 # del content["password"]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100535 # self._send_msg("create", content)
536 return _id
537 except ValidationError as e:
538 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
539
540 def show(self, session, _id):
541 """
542 Get complete information on an topic
543
tierno65ca36d2019-02-12 19:27:52 +0100544 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100545 :param _id: server internal id
546 :return: dictionary, raise exception if not found.
547 """
tiernocf042d32019-06-13 09:06:40 +0000548 # Allow _id to be a name or uuid
549 filter_q = {self.id_field(self.topic, _id): _id}
550 users = self.auth.get_user_list(filter_q)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100551
552 if len(users) == 1:
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100553 return self.format_on_show(users[0])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100554 elif len(users) > 1:
555 raise EngineException("Too many users found", HTTPStatus.CONFLICT)
556 else:
557 raise EngineException("User not found", HTTPStatus.NOT_FOUND)
558
tierno65ca36d2019-02-12 19:27:52 +0100559 def edit(self, session, _id, indata=None, kwargs=None, content=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100560 """
561 Updates an user entry.
562
tierno65ca36d2019-02-12 19:27:52 +0100563 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100564 :param _id:
565 :param indata: data to be inserted
566 :param kwargs: used to override the indata descriptor
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100567 :param content:
568 :return: _id: identity of the inserted data.
569 """
570 indata = self._remove_envelop(indata)
571
572 # Override descriptor with query string kwargs
573 if kwargs:
574 BaseTopic._update_input_with_kwargs(indata, kwargs)
575 try:
tierno65ca36d2019-02-12 19:27:52 +0100576 indata = self._validate_input_edit(indata, force=session["force"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100577
578 if not content:
579 content = self.show(session, _id)
tierno65ca36d2019-02-12 19:27:52 +0100580 self.check_conflict_on_edit(session, content, indata, _id=_id)
tiernocf042d32019-06-13 09:06:40 +0000581 # self.format_on_edit(content, indata)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100582
tiernocf042d32019-06-13 09:06:40 +0000583 if "password" in indata or "username" in indata:
584 self.auth.update_user(_id, new_name=indata.get("username"), new_password=indata.get("password"))
585 if not indata.get("remove_project_role_mappings") and not indata.get("add_project_role_mappings") and \
586 not indata.get("project_role_mappings"):
587 return _id
588 if indata.get("project_role_mappings") and \
589 (indata.get("remove_project_role_mappings") or indata.get("add_project_role_mappings")):
590 raise EngineException("Option 'project_role_mappings' is incompatible with 'add_project_role_mappings"
591 "' or 'remove_project_role_mappings'", http_code=HTTPStatus.BAD_REQUEST)
Eduardo Sousa44603902019-06-04 08:10:32 +0100592
tiernocf042d32019-06-13 09:06:40 +0000593 user = self.show(session, _id)
594 original_mapping = user["project_role_mappings"]
Eduardo Sousa44603902019-06-04 08:10:32 +0100595
tiernocf042d32019-06-13 09:06:40 +0000596 mappings_to_add = []
597 mappings_to_remove = []
Eduardo Sousa44603902019-06-04 08:10:32 +0100598
tiernocf042d32019-06-13 09:06:40 +0000599 # remove
600 for to_remove in indata.get("remove_project_role_mappings", ()):
601 for mapping in original_mapping:
602 if to_remove["project"] in (mapping["project"], mapping["project_name"]):
603 if not to_remove.get("role") or to_remove["role"] in (mapping["role"], mapping["role_name"]):
604 mappings_to_remove.append(mapping)
Eduardo Sousa44603902019-06-04 08:10:32 +0100605
tiernocf042d32019-06-13 09:06:40 +0000606 # add
607 for to_add in indata.get("add_project_role_mappings", ()):
608 for mapping in original_mapping:
609 if to_add["project"] in (mapping["project"], mapping["project_name"]) and \
610 to_add["role"] in (mapping["role"], mapping["role_name"]):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100611
tiernocf042d32019-06-13 09:06:40 +0000612 if mapping in mappings_to_remove: # do not remove
613 mappings_to_remove.remove(mapping)
614 break # do not add, it is already at user
615 else:
616 mappings_to_add.append(to_add)
617
618 # set
619 if indata.get("project_role_mappings"):
620 for to_set in indata["project_role_mappings"]:
621 for mapping in original_mapping:
622 if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
623 to_set["role"] in (mapping["role"], mapping["role_name"]):
624
625 if mapping in mappings_to_remove: # do not remove
626 mappings_to_remove.remove(mapping)
627 break # do not add, it is already at user
628 else:
629 mappings_to_add.append(to_set)
630 for mapping in original_mapping:
631 for to_set in indata["project_role_mappings"]:
632 if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
633 to_set["role"] in (mapping["role"], mapping["role_name"]):
634 break
635 else:
636 # delete
637 if mapping not in mappings_to_remove: # do not remove
638 mappings_to_remove.append(mapping)
639
640 for mapping in mappings_to_remove:
641 self.auth.remove_role_from_user(
642 _id,
643 mapping["project"],
644 mapping["role"]
645 )
646
647 for mapping in mappings_to_add:
648 self.auth.assign_role_to_user(
649 _id,
650 mapping["project"],
651 mapping["role"]
652 )
653
654 return "_id"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100655 except ValidationError as e:
656 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
657
658 def list(self, session, filter_q=None):
659 """
660 Get a list of the topic that matches a filter
tierno65ca36d2019-02-12 19:27:52 +0100661 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100662 :param filter_q: filter of data to be applied
663 :return: The list, it can be empty if no one match the filter.
664 """
Eduardo Sousaa16a4fa2019-05-23 01:41:18 +0100665 users = [self.format_on_show(user) for user in self.auth.get_user_list(filter_q)]
666
667 return users
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100668
tierno65ca36d2019-02-12 19:27:52 +0100669 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100670 """
671 Delete item by its internal _id
672
tierno65ca36d2019-02-12 19:27:52 +0100673 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100674 :param _id: server internal id
675 :param force: indicates if deletion must be forced in case of conflict
676 :param dry_run: make checking but do not delete
677 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
678 """
tiernocf042d32019-06-13 09:06:40 +0000679 # Allow _id to be a name or uuid
680 filter_q = {self.id_field(self.topic, _id): _id}
681 user_list = self.auth.get_user_list(filter_q)
682 if not user_list:
683 raise EngineException("User '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
684 _id = user_list[0]["_id"]
685 self.check_conflict_on_del(session, _id, user_list[0])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100686 if not dry_run:
687 v = self.auth.delete_user(_id)
688 return v
689 return None
690
691
692class ProjectTopicAuth(ProjectTopic):
tierno65ca36d2019-02-12 19:27:52 +0100693 # topic = "projects"
694 # topic_msg = "projects"
Eduardo Sousa44603902019-06-04 08:10:32 +0100695 schema_new = project_new_schema
696 schema_edit = project_edit_schema
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100697
698 def __init__(self, db, fs, msg, auth):
699 ProjectTopic.__init__(self, db, fs, msg)
700 self.auth = auth
701
tierno65ca36d2019-02-12 19:27:52 +0100702 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100703 """
704 Check that the data to be inserted is valid
705
tierno65ca36d2019-02-12 19:27:52 +0100706 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100707 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100708 :return: None or raises EngineException
709 """
tiernocf042d32019-06-13 09:06:40 +0000710 project_name = indata.get("name")
711 if is_valid_uuid(project_name):
delacruzramoceb8baf2019-06-21 14:25:38 +0200712 raise EngineException("project name '{}' cannot have an uuid format".format(project_name),
tiernocf042d32019-06-13 09:06:40 +0000713 HTTPStatus.UNPROCESSABLE_ENTITY)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100714
tiernocf042d32019-06-13 09:06:40 +0000715 project_list = self.auth.get_project_list(filter_q={"name": project_name})
716
717 if project_list:
718 raise EngineException("project '{}' exists".format(project_name), HTTPStatus.CONFLICT)
719
720 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
721 """
722 Check that the data to be edited/uploaded is valid
723
724 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
725 :param final_content: data once modified
726 :param edit_content: incremental data that contains the modifications to apply
727 :param _id: internal _id
728 :return: None or raises EngineException
729 """
730
731 project_name = edit_content.get("name")
732 if project_name:
733 if is_valid_uuid(project_name):
734 raise EngineException("project name '{}' cannot be an uuid format".format(project_name),
735 HTTPStatus.UNPROCESSABLE_ENTITY)
736
737 # Check that project name is not used, regardless keystone already checks this
738 if self.auth.get_project_list(filter_q={"name": project_name}):
739 raise EngineException("project '{}' is already used".format(project_name), HTTPStatus.CONFLICT)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100740
tiernob4844ab2019-05-23 08:42:12 +0000741 def check_conflict_on_del(self, session, _id, db_content):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100742 """
743 Check if deletion can be done because of dependencies if it is not force. To override
744
tierno65ca36d2019-02-12 19:27:52 +0100745 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100746 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000747 :param db_content: The database content of this item _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100748 :return: None if ok or raises EngineException with the conflict
749 """
tierno38dcfeb2019-06-10 16:44:00 +0000750 # projects = self.auth.get_project_list()
751 # current_project = [project for project in projects
752 # if project["name"] in session["project_id"]][0]
tiernocf042d32019-06-13 09:06:40 +0000753 # TODO check that any user is using this project, raise CONFLICT exception
tierno38dcfeb2019-06-10 16:44:00 +0000754 if _id == session["project_id"]:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100755 raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
756
tierno65ca36d2019-02-12 19:27:52 +0100757 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100758 """
759 Creates a new entry into the authentication backend.
760
761 NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
762
763 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100764 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100765 :param indata: data to be inserted
766 :param kwargs: used to override the indata descriptor
767 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100768 :return: _id: identity of the inserted data.
769 """
770 try:
771 content = BaseTopic._remove_envelop(indata)
772
773 # Override descriptor with query string kwargs
774 BaseTopic._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100775 content = self._validate_input_new(content, session["force"])
776 self.check_conflict_on_new(session, content)
777 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100778 _id = self.auth.create_project(content["name"])
779 rollback.append({"topic": self.topic, "_id": _id})
780 # self._send_msg("create", content)
781 return _id
782 except ValidationError as e:
783 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
784
785 def show(self, session, _id):
786 """
787 Get complete information on an topic
788
tierno65ca36d2019-02-12 19:27:52 +0100789 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100790 :param _id: server internal id
791 :return: dictionary, raise exception if not found.
792 """
tiernocf042d32019-06-13 09:06:40 +0000793 # Allow _id to be a name or uuid
794 filter_q = {self.id_field(self.topic, _id): _id}
795 projects = self.auth.get_project_list(filter_q=filter_q)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100796
797 if len(projects) == 1:
798 return projects[0]
799 elif len(projects) > 1:
800 raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
801 else:
802 raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
803
804 def list(self, session, filter_q=None):
805 """
806 Get a list of the topic that matches a filter
807
tierno65ca36d2019-02-12 19:27:52 +0100808 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100809 :param filter_q: filter of data to be applied
810 :return: The list, it can be empty if no one match the filter.
811 """
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100812 return self.auth.get_project_list(filter_q)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100813
tierno65ca36d2019-02-12 19:27:52 +0100814 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100815 """
816 Delete item by its internal _id
817
tierno65ca36d2019-02-12 19:27:52 +0100818 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100819 :param _id: server internal id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100820 :param dry_run: make checking but do not delete
821 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
822 """
tiernocf042d32019-06-13 09:06:40 +0000823 # Allow _id to be a name or uuid
824 filter_q = {self.id_field(self.topic, _id): _id}
825 project_list = self.auth.get_project_list(filter_q)
826 if not project_list:
827 raise EngineException("Project '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
828 _id = project_list[0]["_id"]
829 self.check_conflict_on_del(session, _id, project_list[0])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100830 if not dry_run:
831 v = self.auth.delete_project(_id)
832 return v
833 return None
834
tierno4015b472019-06-10 13:57:29 +0000835 def edit(self, session, _id, indata=None, kwargs=None, content=None):
836 """
837 Updates a project entry.
838
839 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
840 :param _id:
841 :param indata: data to be inserted
842 :param kwargs: used to override the indata descriptor
843 :param content:
844 :return: _id: identity of the inserted data.
845 """
846 indata = self._remove_envelop(indata)
847
848 # Override descriptor with query string kwargs
849 if kwargs:
850 BaseTopic._update_input_with_kwargs(indata, kwargs)
851 try:
852 indata = self._validate_input_edit(indata, force=session["force"])
853
854 if not content:
855 content = self.show(session, _id)
856 self.check_conflict_on_edit(session, content, indata, _id=_id)
tiernocf042d32019-06-13 09:06:40 +0000857 # self.format_on_edit(content, indata)
tierno4015b472019-06-10 13:57:29 +0000858
859 if "name" in indata:
860 self.auth.update_project(content["_id"], indata["name"])
861 except ValidationError as e:
862 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
863
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100864
865class RoleTopicAuth(BaseTopic):
delacruzramoceb8baf2019-06-21 14:25:38 +0200866 topic = "roles"
867 topic_msg = None # "roles"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100868 schema_new = roles_new_schema
869 schema_edit = roles_edit_schema
tierno65ca36d2019-02-12 19:27:52 +0100870 multiproject = False
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100871
872 def __init__(self, db, fs, msg, auth, ops):
873 BaseTopic.__init__(self, db, fs, msg)
874 self.auth = auth
875 self.operations = ops
delacruzramoceb8baf2019-06-21 14:25:38 +0200876 self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100877
878 @staticmethod
879 def validate_role_definition(operations, role_definitions):
880 """
881 Validates the role definition against the operations defined in
882 the resources to operations files.
883
884 :param operations: operations list
885 :param role_definitions: role definition to test
886 :return: None if ok, raises ValidationError exception on error
887 """
tierno1f029d82019-06-13 22:37:04 +0000888 if not role_definitions.get("permissions"):
889 return
890 ignore_fields = ["admin", "default"]
891 for role_def in role_definitions["permissions"].keys():
Eduardo Sousa37de0912019-05-23 02:17:22 +0100892 if role_def in ignore_fields:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100893 continue
Eduardo Sousac7689372019-06-04 16:01:46 +0100894 if role_def[-1] == ":":
tierno1f029d82019-06-13 22:37:04 +0000895 raise ValidationError("Operation cannot end with ':'")
Eduardo Sousac5a18892019-06-06 14:51:23 +0100896
delacruzramoc061f562019-04-05 11:00:02 +0200897 role_def_matches = [op for op in operations if op.startswith(role_def)]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100898
899 if len(role_def_matches) == 0:
tierno1f029d82019-06-13 22:37:04 +0000900 raise ValidationError("Invalid permission '{}'".format(role_def))
Eduardo Sousa37de0912019-05-23 02:17:22 +0100901
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100902 def _validate_input_new(self, input, force=False):
903 """
904 Validates input user content for a new entry.
905
906 :param input: user input content for the new topic
907 :param force: may be used for being more tolerant
908 :return: The same input content, or a changed version of it.
909 """
910 if self.schema_new:
911 validate_input(input, self.schema_new)
Eduardo Sousa37de0912019-05-23 02:17:22 +0100912 self.validate_role_definition(self.operations, input)
Eduardo Sousac4650362019-06-04 13:24:22 +0100913
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100914 return input
915
916 def _validate_input_edit(self, input, force=False):
917 """
918 Validates input user content for updating an entry.
919
920 :param input: user input content for the new topic
921 :param force: may be used for being more tolerant
922 :return: The same input content, or a changed version of it.
923 """
924 if self.schema_edit:
925 validate_input(input, self.schema_edit)
Eduardo Sousa37de0912019-05-23 02:17:22 +0100926 self.validate_role_definition(self.operations, input)
Eduardo Sousac4650362019-06-04 13:24:22 +0100927
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100928 return input
929
tierno65ca36d2019-02-12 19:27:52 +0100930 def check_conflict_on_new(self, session, indata):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100931 """
932 Check that the data to be inserted is valid
933
tierno65ca36d2019-02-12 19:27:52 +0100934 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100935 :param indata: data to be inserted
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100936 :return: None or raises EngineException
937 """
tierno1f029d82019-06-13 22:37:04 +0000938 # check name not exists
939 if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
940 raise EngineException("role name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100941
tierno65ca36d2019-02-12 19:27:52 +0100942 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100943 """
944 Check that the data to be edited/uploaded is valid
945
tierno65ca36d2019-02-12 19:27:52 +0100946 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100947 :param final_content: data once modified
948 :param edit_content: incremental data that contains the modifications to apply
949 :param _id: internal _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100950 :return: None or raises EngineException
951 """
tierno1f029d82019-06-13 22:37:04 +0000952 if "default" not in final_content["permissions"]:
953 final_content["permissions"]["default"] = False
954 if "admin" not in final_content["permissions"]:
955 final_content["permissions"]["admin"] = False
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100956
tierno1f029d82019-06-13 22:37:04 +0000957 # check name not exists
958 if "name" in edit_content:
959 role_name = edit_content["name"]
960 if self.db.get_one(self.topic, {"name": role_name, "_id.ne": _id}, fail_on_empty=False, fail_on_more=False):
961 raise EngineException("role name '{}' exists".format(role_name), HTTPStatus.CONFLICT)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100962
tiernob4844ab2019-05-23 08:42:12 +0000963 def check_conflict_on_del(self, session, _id, db_content):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100964 """
965 Check if deletion can be done because of dependencies if it is not force. To override
966
tierno65ca36d2019-02-12 19:27:52 +0100967 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100968 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000969 :param db_content: The database content of this item _id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100970 :return: None if ok or raises EngineException with the conflict
971 """
972 roles = self.auth.get_role_list()
delacruzramoceb8baf2019-06-21 14:25:38 +0200973 system_admin_roles = [role for role in roles if role["name"] == "system_admin"]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100974
delacruzramoceb8baf2019-06-21 14:25:38 +0200975 if system_admin_roles and _id == system_admin_roles[0]["_id"]:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100976 raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
977
978 @staticmethod
979 def format_on_new(content, project_id=None, make_public=False):
980 """
981 Modifies content descriptor to include _admin
982
983 :param content: descriptor to be modified
984 :param project_id: if included, it add project read/write permissions
985 :param make_public: if included it is generated as public for reading.
986 :return: None, but content is modified
987 """
988 now = time()
989 if "_admin" not in content:
990 content["_admin"] = {}
991 if not content["_admin"].get("created"):
992 content["_admin"]["created"] = now
993 content["_admin"]["modified"] = now
Eduardo Sousac4650362019-06-04 13:24:22 +0100994
tierno1f029d82019-06-13 22:37:04 +0000995 if "permissions" not in content:
996 content["permissions"] = {}
Eduardo Sousac4650362019-06-04 13:24:22 +0100997
tierno1f029d82019-06-13 22:37:04 +0000998 if "default" not in content["permissions"]:
999 content["permissions"]["default"] = False
1000 if "admin" not in content["permissions"]:
1001 content["permissions"]["admin"] = False
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001002
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001003 @staticmethod
1004 def format_on_edit(final_content, edit_content):
1005 """
1006 Modifies final_content descriptor to include the modified date.
1007
1008 :param final_content: final descriptor generated
1009 :param edit_content: alterations to be include
1010 :return: None, but final_content is modified
1011 """
1012 final_content["_admin"]["modified"] = time()
1013
tierno1f029d82019-06-13 22:37:04 +00001014 if "permissions" not in final_content:
1015 final_content["permissions"] = {}
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001016
tierno1f029d82019-06-13 22:37:04 +00001017 if "default" not in final_content["permissions"]:
1018 final_content["permissions"]["default"] = False
1019 if "admin" not in final_content["permissions"]:
1020 final_content["permissions"]["admin"] = False
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001021
tierno1f029d82019-06-13 22:37:04 +00001022 # @staticmethod
1023 # def format_on_show(content):
1024 # """
1025 # Modifies the content of the role information to separate the role
1026 # metadata from the role definition. Eases the reading process of the
1027 # role definition.
1028 #
1029 # :param definition: role definition to be processed
1030 # """
1031 # content["_id"] = str(content["_id"])
1032 #
1033 # def show(self, session, _id):
1034 # """
1035 # Get complete information on an topic
1036 #
1037 # :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1038 # :param _id: server internal id
1039 # :return: dictionary, raise exception if not found.
1040 # """
1041 # filter_db = {"_id": _id}
delacruzramoceb8baf2019-06-21 14:25:38 +02001042 # filter_db = { BaseTopic.id_field(self.topic, _id): _id } # To allow role addressing by name
tierno1f029d82019-06-13 22:37:04 +00001043 #
1044 # role = self.db.get_one(self.topic, filter_db)
1045 # new_role = dict(role)
1046 # self.format_on_show(new_role)
1047 #
1048 # return new_role
Eduardo Sousac4650362019-06-04 13:24:22 +01001049
tierno1f029d82019-06-13 22:37:04 +00001050 # def list(self, session, filter_q=None):
1051 # """
1052 # Get a list of the topic that matches a filter
1053 #
1054 # :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1055 # :param filter_q: filter of data to be applied
1056 # :return: The list, it can be empty if no one match the filter.
1057 # """
1058 # if not filter_q:
1059 # filter_q = {}
1060 #
1061 # if ":" in filter_q:
1062 # filter_q["root"] = filter_q[":"]
1063 #
1064 # for key in filter_q.keys():
1065 # if key == "name":
1066 # continue
1067 # filter_q[key] = filter_q[key] in ["True", "true"]
1068 #
1069 # roles = self.db.get_list(self.topic, filter_q)
1070 # new_roles = []
1071 #
1072 # for role in roles:
1073 # new_role = dict(role)
1074 # self.format_on_show(new_role)
1075 # new_roles.append(new_role)
1076 #
1077 # return new_roles
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001078
tierno65ca36d2019-02-12 19:27:52 +01001079 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001080 """
1081 Creates a new entry into database.
1082
1083 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +01001084 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001085 :param indata: data to be inserted
1086 :param kwargs: used to override the indata descriptor
1087 :param headers: http request headers
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001088 :return: _id: identity of the inserted data.
1089 """
1090 try:
tierno1f029d82019-06-13 22:37:04 +00001091 content = self._remove_envelop(indata)
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001092
1093 # Override descriptor with query string kwargs
tierno1f029d82019-06-13 22:37:04 +00001094 self._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +01001095 content = self._validate_input_new(content, session["force"])
1096 self.check_conflict_on_new(session, content)
1097 self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001098 role_name = content["name"]
tierno1f029d82019-06-13 22:37:04 +00001099 role_id = self.auth.create_role(role_name)
1100 content["_id"] = role_id
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001101 _id = self.db.create(self.topic, content)
1102 rollback.append({"topic": self.topic, "_id": _id})
1103 # self._send_msg("create", content)
1104 return _id
1105 except ValidationError as e:
1106 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
1107
tierno65ca36d2019-02-12 19:27:52 +01001108 def delete(self, session, _id, dry_run=False):
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001109 """
1110 Delete item by its internal _id
1111
tierno65ca36d2019-02-12 19:27:52 +01001112 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001113 :param _id: server internal id
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001114 :param dry_run: make checking but do not delete
1115 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
1116 """
tiernob4844ab2019-05-23 08:42:12 +00001117 self.check_conflict_on_del(session, _id, None)
delacruzramoceb8baf2019-06-21 14:25:38 +02001118 # filter_q = {"_id": _id}
1119 filter_q = {BaseTopic.id_field(self.topic, _id): _id} # To allow role addressing by name
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001120 if not dry_run:
1121 self.auth.delete_role(_id)
1122 v = self.db.del_one(self.topic, filter_q)
1123 return v
1124 return None
1125
tierno65ca36d2019-02-12 19:27:52 +01001126 def edit(self, session, _id, indata=None, kwargs=None, content=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001127 """
1128 Updates a role entry.
1129
tierno65ca36d2019-02-12 19:27:52 +01001130 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001131 :param _id:
1132 :param indata: data to be inserted
1133 :param kwargs: used to override the indata descriptor
Eduardo Sousa5c01e192019-05-08 02:35:47 +01001134 :param content:
1135 :return: _id: identity of the inserted data.
1136 """
tierno1f029d82019-06-13 22:37:04 +00001137 _id = super().edit(session, _id, indata, kwargs, content)
1138 if indata.get("name"):
1139 self.auth.update_role(_id, name=indata.get("name"))