blob: b3de1cd77bdfb0625700048021fd7aaa704c5951 [file] [log] [blame]
delacruzramoceb8baf2019-06-21 14:25:38 +02001# -*- coding: utf-8 -*-
2
3# Copyright 2018 Telefonica S.A.
4# Copyright 2018 ALTRAN Innovación S.L.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17#
18# For those usages not covered by the Apache License, Version 2.0 please
19# contact: esousa@whitestack.com or glavado@whitestack.com
20##
21
22"""
23AuthconnInternal implements implements the connector for
24OSM Internal Authentication Backend and leverages the RBAC model
25"""
26
tierno23acf402019-08-28 13:36:34 +000027__author__ = "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>, " \
28 "Alfonso Tierno <alfonso.tiernosepulveda@telefoncia.com"
delacruzramoceb8baf2019-06-21 14:25:38 +020029__date__ = "$06-jun-2019 11:16:08$"
30
tierno5ec768a2020-03-31 09:46:44 +000031import logging
32import re
33
tierno23acf402019-08-28 13:36:34 +000034from osm_nbi.authconn import Authconn, AuthException # , AuthconnOperationException
delacruzramoceb8baf2019-06-21 14:25:38 +020035from osm_common.dbbase import DbException
tierno23acf402019-08-28 13:36:34 +000036from osm_nbi.base_topic import BaseTopic
tierno5ec768a2020-03-31 09:46:44 +000037from osm_nbi.validation import is_valid_uuid
delacruzramoad682a52019-12-10 16:26:34 +010038from time import time, sleep
delacruzramoceb8baf2019-06-21 14:25:38 +020039from http import HTTPStatus
40from uuid import uuid4
41from hashlib import sha256
42from copy import deepcopy
43from random import choice as random_choice
44
45
46class AuthconnInternal(Authconn):
delacruzramoad682a52019-12-10 16:26:34 +010047 token_time_window = 2 # seconds
48 token_delay = 1 # seconds to wait upon second request within time window
delacruzramoceb8baf2019-06-21 14:25:38 +020049
K Sai Kiran7ddb0732020-10-30 11:14:44 +053050 users_collection = "users"
51 roles_collection = "roles"
52 projects_collection = "projects"
53 tokens_collection = "tokens"
54
tierno9e87a7f2020-03-23 09:24:10 +000055 def __init__(self, config, db, role_permissions):
56 Authconn.__init__(self, config, db, role_permissions)
delacruzramoceb8baf2019-06-21 14:25:38 +020057 self.logger = logging.getLogger("nbi.authenticator.internal")
58
delacruzramoceb8baf2019-06-21 14:25:38 +020059 self.db = db
delacruzramoad682a52019-12-10 16:26:34 +010060 # self.msg = msg
61 # self.token_cache = token_cache
delacruzramoceb8baf2019-06-21 14:25:38 +020062
63 # To be Confirmed
delacruzramoceb8baf2019-06-21 14:25:38 +020064 self.sess = None
65
delacruzramoceb8baf2019-06-21 14:25:38 +020066 def validate_token(self, token):
67 """
68 Check if the token is valid.
69
70 :param token: token to validate
71 :return: dictionary with information associated with the token:
72 "_id": token id
73 "project_id": project id
74 "project_name": project name
75 "user_id": user id
76 "username": user name
77 "roles": list with dict containing {name, id}
78 "expires": expiration date
79 If the token is not valid an exception is raised.
80 """
81
82 try:
83 if not token:
84 raise AuthException("Needed a token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
85
delacruzramoceb8baf2019-06-21 14:25:38 +020086 now = time()
delacruzramoceb8baf2019-06-21 14:25:38 +020087
88 # get from database if not in cache
delacruzramoad682a52019-12-10 16:26:34 +010089 # if not token_info:
K Sai Kiran7ddb0732020-10-30 11:14:44 +053090 token_info = self.db.get_one(self.tokens_collection, {"_id": token})
delacruzramoad682a52019-12-10 16:26:34 +010091 if token_info["expires"] < now:
92 raise AuthException("Expired Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoceb8baf2019-06-21 14:25:38 +020093
tierno701018c2019-06-25 11:13:14 +000094 return token_info
delacruzramoceb8baf2019-06-21 14:25:38 +020095
96 except DbException as e:
97 if e.http_code == HTTPStatus.NOT_FOUND:
98 raise AuthException("Invalid Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
99 else:
100 raise
101 except AuthException:
tiernoe1eb3b22019-08-26 15:59:24 +0000102 raise
delacruzramoceb8baf2019-06-21 14:25:38 +0200103 except Exception:
104 self.logger.exception("Error during token validation using internal backend")
105 raise AuthException("Error during token validation using internal backend",
106 http_code=HTTPStatus.UNAUTHORIZED)
107
108 def revoke_token(self, token):
109 """
110 Invalidate a token.
111
112 :param token: token to be revoked
113 """
114 try:
delacruzramoad682a52019-12-10 16:26:34 +0100115 # self.token_cache.pop(token, None)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530116 self.db.del_one(self.tokens_collection, {"_id": token})
delacruzramoceb8baf2019-06-21 14:25:38 +0200117 return True
118 except DbException as e:
119 if e.http_code == HTTPStatus.NOT_FOUND:
120 raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
121 else:
122 # raise
delacruzramoad682a52019-12-10 16:26:34 +0100123 exmsg = "Error during token revocation using internal backend"
124 self.logger.exception(exmsg)
125 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoceb8baf2019-06-21 14:25:38 +0200126
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530127 def validate_user(self, user, password):
128 """
129 Validate username and password via appropriate backend.
130 :param user: username of the user.
131 :param password: password to be validated.
132 """
133 user_rows = self.db.get_list(self.users_collection, {BaseTopic.id_field("users", user): user})
134 user_content = None
135 if user_rows:
136 user_content = user_rows[0]
137 salt = user_content["_admin"]["salt"]
138 shadow_password = sha256(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
139 if shadow_password != user_content["password"]:
140 user_content = None
141 return user_content
142
tierno6486f742020-02-13 16:30:14 +0000143 def authenticate(self, credentials, token_info=None):
delacruzramoceb8baf2019-06-21 14:25:38 +0200144 """
tierno701018c2019-06-25 11:13:14 +0000145 Authenticate a user using username/password or previous token_info plus project; its creates a new token
delacruzramoceb8baf2019-06-21 14:25:38 +0200146
tierno6486f742020-02-13 16:30:14 +0000147 :param credentials: dictionary that contains:
148 username: name, id or None
149 password: password or None
150 project_id: name, id, or None. If None first found project will be used to get an scope token
151 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000152 :param token_info: previous token_info to obtain authorization
delacruzramoceb8baf2019-06-21 14:25:38 +0200153 :return: the scoped token info or raises an exception. The token is a dictionary with:
154 _id: token string id,
155 username: username,
156 project_id: scoped_token project_id,
157 project_name: scoped_token project_name,
158 expires: epoch time when it expires,
159 """
160
161 now = time()
162 user_content = None
tierno6486f742020-02-13 16:30:14 +0000163 user = credentials.get("username")
164 password = credentials.get("password")
165 project = credentials.get("project_id")
delacruzramoceb8baf2019-06-21 14:25:38 +0200166
delacruzramo01b15d32019-07-02 14:37:47 +0200167 # Try using username/password
168 if user:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530169 user_content = self.validate_user(user, password)
delacruzramo01b15d32019-07-02 14:37:47 +0200170 if not user_content:
171 raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530172 if not user_content.get("_admin", None):
173 raise AuthException("No default project for this user.", http_code=HTTPStatus.UNAUTHORIZED)
delacruzramo01b15d32019-07-02 14:37:47 +0200174 elif token_info:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530175 user_rows = self.db.get_list(self.users_collection, {"username": token_info["username"]})
delacruzramo01b15d32019-07-02 14:37:47 +0200176 if user_rows:
177 user_content = user_rows[0]
delacruzramoceb8baf2019-06-21 14:25:38 +0200178 else:
delacruzramo01b15d32019-07-02 14:37:47 +0200179 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
180 else:
181 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
182 http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoad682a52019-12-10 16:26:34 +0100183 # Delay upon second request within time window
184 if now - user_content["_admin"].get("last_token_time", 0) < self.token_time_window:
185 sleep(self.token_delay)
186 # user_content["_admin"]["last_token_time"] = now
187 # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530188 self.db.set_one(self.users_collection,
189 {"_id": user_content["_id"]}, {"_admin.last_token_time": now})
delacruzramoad682a52019-12-10 16:26:34 +0100190
delacruzramo01b15d32019-07-02 14:37:47 +0200191 token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
192 for _ in range(0, 32))
delacruzramoceb8baf2019-06-21 14:25:38 +0200193
delacruzramo01b15d32019-07-02 14:37:47 +0200194 # projects = user_content.get("projects", [])
195 prm_list = user_content.get("project_role_mappings", [])
delacruzramoceb8baf2019-06-21 14:25:38 +0200196
delacruzramo01b15d32019-07-02 14:37:47 +0200197 if not project:
198 project = prm_list[0]["project"] if prm_list else None
199 if not project:
200 raise AuthException("can't find a default project for this user", http_code=HTTPStatus.UNAUTHORIZED)
tierno701018c2019-06-25 11:13:14 +0000201
delacruzramo01b15d32019-07-02 14:37:47 +0200202 projects = [prm["project"] for prm in prm_list]
delacruzramoceb8baf2019-06-21 14:25:38 +0200203
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530204 proj = self.db.get_one(self.projects_collection,
205 {BaseTopic.id_field("projects", project): project})
delacruzramo01b15d32019-07-02 14:37:47 +0200206 project_name = proj["name"]
207 project_id = proj["_id"]
208 if project_name not in projects and project_id not in projects:
209 raise AuthException("project {} not allowed for this user".format(project),
210 http_code=HTTPStatus.UNAUTHORIZED)
tierno701018c2019-06-25 11:13:14 +0000211
delacruzramo01b15d32019-07-02 14:37:47 +0200212 # TODO remove admin, this vill be used by roles RBAC
213 if project_name == "admin":
214 token_admin = True
215 else:
216 token_admin = proj.get("admin", False)
delacruzramoceb8baf2019-06-21 14:25:38 +0200217
delacruzramo01b15d32019-07-02 14:37:47 +0200218 # add token roles
219 roles = []
220 roles_list = []
221 for prm in prm_list:
222 if prm["project"] in [project_id, project_name]:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530223 role = self.db.get_one(self.roles_collection,
224 {BaseTopic.id_field("roles", prm["role"]): prm["role"]})
delacruzramo01b15d32019-07-02 14:37:47 +0200225 rid = role["_id"]
226 if rid not in roles:
227 rnm = role["name"]
228 roles.append(rid)
229 roles_list.append({"name": rnm, "id": rid})
230 if not roles_list:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530231 rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})["_id"]
delacruzramo01b15d32019-07-02 14:37:47 +0200232 roles_list = [{"name": "project_admin", "id": rid}]
delacruzramoceb8baf2019-06-21 14:25:38 +0200233
delacruzramo01b15d32019-07-02 14:37:47 +0200234 new_token = {"issued_at": now,
235 "expires": now + 3600,
236 "_id": token_id,
237 "id": token_id,
238 "project_id": proj["_id"],
239 "project_name": proj["name"],
240 "username": user_content["username"],
241 "user_id": user_content["_id"],
242 "admin": token_admin,
243 "roles": roles_list,
244 }
delacruzramoceb8baf2019-06-21 14:25:38 +0200245
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530246 self.db.create(self.tokens_collection, new_token)
delacruzramo01b15d32019-07-02 14:37:47 +0200247 return deepcopy(new_token)
248
249 def get_role_list(self, filter_q={}):
delacruzramoceb8baf2019-06-21 14:25:38 +0200250 """
251 Get role list.
252
253 :return: returns the list of roles.
254 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530255 return self.db.get_list(self.roles_collection, filter_q)
delacruzramoceb8baf2019-06-21 14:25:38 +0200256
delacruzramo01b15d32019-07-02 14:37:47 +0200257 def create_role(self, role_info):
delacruzramoceb8baf2019-06-21 14:25:38 +0200258 """
259 Create a role.
260
delacruzramo01b15d32019-07-02 14:37:47 +0200261 :param role_info: full role info.
262 :return: returns the role id.
delacruzramoceb8baf2019-06-21 14:25:38 +0200263 :raises AuthconnOperationException: if role creation failed.
264 """
delacruzramoceb8baf2019-06-21 14:25:38 +0200265 # TODO: Check that role name does not exist ?
delacruzramo01b15d32019-07-02 14:37:47 +0200266 rid = str(uuid4())
267 role_info["_id"] = rid
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530268 rid = self.db.create(self.roles_collection, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200269 return rid
delacruzramoceb8baf2019-06-21 14:25:38 +0200270
271 def delete_role(self, role_id):
272 """
273 Delete a role.
274
275 :param role_id: role identifier.
276 :raises AuthconnOperationException: if role deletion failed.
277 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530278 rc = self.db.del_one(self.roles_collection, {"_id": role_id})
279 self.db.del_list(self.tokens_collection, {"roles.id": role_id})
delacruzramoad682a52019-12-10 16:26:34 +0100280 return rc
delacruzramo01b15d32019-07-02 14:37:47 +0200281
282 def update_role(self, role_info):
283 """
284 Update a role.
285
286 :param role_info: full role info.
287 :return: returns the role name and id.
288 :raises AuthconnOperationException: if user creation failed.
289 """
290 rid = role_info["_id"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530291 self.db.set_one(self.roles_collection, {"_id": rid}, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200292 return {"_id": rid, "name": role_info["name"]}
293
294 def create_user(self, user_info):
295 """
296 Create a user.
297
298 :param user_info: full user info.
299 :return: returns the username and id of the user.
300 """
301 BaseTopic.format_on_new(user_info, make_public=False)
302 salt = uuid4().hex
303 user_info["_admin"]["salt"] = salt
304 if "password" in user_info:
305 user_info["password"] = sha256(user_info["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
306 # "projects" are not stored any more
307 if "projects" in user_info:
308 del user_info["projects"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530309 self.db.create(self.users_collection, user_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200310 return {"username": user_info["username"], "_id": user_info["_id"]}
311
312 def update_user(self, user_info):
313 """
314 Change the user name and/or password.
315
316 :param user_info: user info modifications
317 """
318 uid = user_info["_id"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530319 user_data = self.db.get_one(self.users_collection, {BaseTopic.id_field("users", uid): uid})
delacruzramo01b15d32019-07-02 14:37:47 +0200320 BaseTopic.format_on_edit(user_data, user_info)
321 # User Name
322 usnm = user_info.get("username")
323 if usnm:
324 user_data["username"] = usnm
325 # If password is given and is not already encripted
326 pswd = user_info.get("password")
327 if pswd and (len(pswd) != 64 or not re.match('[a-fA-F0-9]*', pswd)): # TODO: Improve check?
328 salt = uuid4().hex
329 if "_admin" not in user_data:
330 user_data["_admin"] = {}
331 user_data["_admin"]["salt"] = salt
332 user_data["password"] = sha256(pswd.encode('utf-8') + salt.encode('utf-8')).hexdigest()
333 # Project-Role Mappings
334 # TODO: Check that user_info NEVER includes "project_role_mappings"
335 if "project_role_mappings" not in user_data:
336 user_data["project_role_mappings"] = []
337 for prm in user_info.get("add_project_role_mappings", []):
338 user_data["project_role_mappings"].append(prm)
339 for prm in user_info.get("remove_project_role_mappings", []):
340 for pidf in ["project", "project_name"]:
341 for ridf in ["role", "role_name"]:
342 try:
343 user_data["project_role_mappings"].remove({"role": prm[ridf], "project": prm[pidf]})
344 except KeyError:
345 pass
346 except ValueError:
347 pass
delacruzramo3d6881c2019-12-04 13:42:26 +0100348 idf = BaseTopic.id_field("users", uid)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530349 self.db.set_one(self.users_collection, {idf: uid}, user_data)
delacruzramo3d6881c2019-12-04 13:42:26 +0100350 if user_info.get("remove_project_role_mappings"):
delacruzramoad682a52019-12-10 16:26:34 +0100351 idf = "user_id" if idf == "_id" else idf
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530352 self.db.del_list(self.tokens_collection, {idf: uid})
delacruzramo01b15d32019-07-02 14:37:47 +0200353
354 def delete_user(self, user_id):
355 """
356 Delete user.
357
358 :param user_id: user identifier.
359 :raises AuthconnOperationException: if user deletion failed.
360 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530361 self.db.del_one(self.users_collection, {"_id": user_id})
362 self.db.del_list(self.tokens_collection, {"user_id": user_id})
delacruzramoceb8baf2019-06-21 14:25:38 +0200363 return True
delacruzramo01b15d32019-07-02 14:37:47 +0200364
365 def get_user_list(self, filter_q=None):
366 """
367 Get user list.
368
tierno5ec768a2020-03-31 09:46:44 +0000369 :param filter_q: dictionary to filter user list by:
370 name (username is also admitted). If a user id is equal to the filter name, it is also provided
371 other
delacruzramo01b15d32019-07-02 14:37:47 +0200372 :return: returns a list of users.
373 """
374 filt = filter_q or {}
tierno5ec768a2020-03-31 09:46:44 +0000375 if "name" in filt: # backward compatibility
376 filt["username"] = filt.pop("name")
377 if filt.get("username") and is_valid_uuid(filt["username"]):
378 # username cannot be a uuid. If this is the case, change from username to _id
379 filt["_id"] = filt.pop("username")
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530380 users = self.db.get_list(self.users_collection, filt)
tierno1546f2a2019-08-20 15:38:11 +0000381 project_id_name = {}
382 role_id_name = {}
delacruzramo01b15d32019-07-02 14:37:47 +0200383 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000384 prms = user.get("project_role_mappings")
385 projects = user.get("projects")
386 if prms:
387 projects = []
388 # add project_name and role_name. Generate projects for backward compatibility
delacruzramo01b15d32019-07-02 14:37:47 +0200389 for prm in prms:
tierno1546f2a2019-08-20 15:38:11 +0000390 project_id = prm["project"]
391 if project_id not in project_id_name:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530392 pr = self.db.get_one(self.projects_collection,
393 {BaseTopic.id_field("projects", project_id): project_id},
tierno1546f2a2019-08-20 15:38:11 +0000394 fail_on_empty=False)
395 project_id_name[project_id] = pr["name"] if pr else None
396 prm["project_name"] = project_id_name[project_id]
397 if prm["project_name"] not in projects:
398 projects.append(prm["project_name"])
399
400 role_id = prm["role"]
401 if role_id not in role_id_name:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530402 role = self.db.get_one(self.roles_collection,
403 {BaseTopic.id_field("roles", role_id): role_id},
tierno1546f2a2019-08-20 15:38:11 +0000404 fail_on_empty=False)
405 role_id_name[role_id] = role["name"] if role else None
406 prm["role_name"] = role_id_name[role_id]
407 user["projects"] = projects # for backward compatibility
408 elif projects:
409 # user created with an old version. Create a project_role mapping with role project_admin
410 user["project_role_mappings"] = []
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530411 role = self.db.get_one(self.roles_collection,
412 {BaseTopic.id_field("roles", "project_admin"): "project_admin"})
tierno1546f2a2019-08-20 15:38:11 +0000413 for p_id_name in projects:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530414 pr = self.db.get_one(self.projects_collection,
415 {BaseTopic.id_field("projects", p_id_name): p_id_name})
tierno1546f2a2019-08-20 15:38:11 +0000416 prm = {"project": pr["_id"],
417 "project_name": pr["name"],
418 "role_name": "project_admin",
419 "role": role["_id"]
420 }
421 user["project_role_mappings"].append(prm)
422 else:
423 user["projects"] = []
424 user["project_role_mappings"] = []
425
delacruzramo01b15d32019-07-02 14:37:47 +0200426 return users
427
428 def get_project_list(self, filter_q={}):
429 """
430 Get role list.
431
432 :return: returns the list of projects.
433 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530434 return self.db.get_list(self.projects_collection, filter_q)
delacruzramo01b15d32019-07-02 14:37:47 +0200435
436 def create_project(self, project_info):
437 """
438 Create a project.
439
440 :param project: full project info.
441 :return: the internal id of the created project
442 :raises AuthconnOperationException: if project creation failed.
443 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530444 pid = self.db.create(self.projects_collection, project_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200445 return pid
446
447 def delete_project(self, project_id):
448 """
449 Delete a project.
450
451 :param project_id: project identifier.
452 :raises AuthconnOperationException: if project deletion failed.
453 """
delacruzramoad682a52019-12-10 16:26:34 +0100454 idf = BaseTopic.id_field("projects", project_id)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530455 r = self.db.del_one(self.projects_collection, {idf: project_id})
delacruzramoad682a52019-12-10 16:26:34 +0100456 idf = "project_id" if idf == "_id" else "project_name"
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530457 self.db.del_list(self.tokens_collection, {idf: project_id})
delacruzramo01b15d32019-07-02 14:37:47 +0200458 return r
459
460 def update_project(self, project_id, project_info):
461 """
462 Change the name of a project
463
464 :param project_id: project to be changed
465 :param project_info: full project info
466 :return: None
467 :raises AuthconnOperationException: if project update failed.
468 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530469 self.db.set_one(self.projects_collection, {BaseTopic.id_field("projects", project_id): project_id},
470 project_info)