Fix Bug 2308: Incorrect status update for user update
[osm/NBI.git] / osm_nbi / authconn_internal.py
1 # -*- 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 """
23 AuthconnInternal implements implements the connector for
24 OSM Internal Authentication Backend and leverages the RBAC model
25 """
26
27 __author__ = (
28 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>, "
29 "Alfonso Tierno <alfonso.tiernosepulveda@telefoncia.com"
30 )
31 __date__ = "$06-jun-2019 11:16:08$"
32
33 import logging
34 import re
35
36 from osm_nbi.authconn import (
37 Authconn,
38 AuthException,
39 AuthconnConflictException,
40 ) # , AuthconnOperationException
41 from osm_common.dbbase import DbException
42 from osm_nbi.base_topic import BaseTopic
43 from osm_nbi.utils import cef_event, cef_event_builder
44 from osm_nbi.validation import is_valid_uuid
45 from time import time, sleep
46 from http import HTTPStatus
47 from uuid import uuid4
48 from hashlib import sha256
49 from copy import deepcopy
50 from random import choice as random_choice
51
52
53 class AuthconnInternal(Authconn):
54 token_time_window = 2 # seconds
55 token_delay = 1 # seconds to wait upon second request within time window
56
57 users_collection = "users"
58 roles_collection = "roles"
59 projects_collection = "projects"
60 tokens_collection = "tokens"
61
62 def __init__(self, config, db, role_permissions):
63 Authconn.__init__(self, config, db, role_permissions)
64 self.logger = logging.getLogger("nbi.authenticator.internal")
65
66 self.db = db
67 # self.msg = msg
68 # self.token_cache = token_cache
69
70 # To be Confirmed
71 self.sess = None
72 self.cef_logger = cef_event_builder(config)
73
74 def validate_token(self, token):
75 """
76 Check if the token is valid.
77
78 :param token: token to validate
79 :return: dictionary with information associated with the token:
80 "_id": token id
81 "project_id": project id
82 "project_name": project name
83 "user_id": user id
84 "username": user name
85 "roles": list with dict containing {name, id}
86 "expires": expiration date
87 If the token is not valid an exception is raised.
88 """
89
90 try:
91 if not token:
92 raise AuthException(
93 "Needed a token or Authorization HTTP header",
94 http_code=HTTPStatus.UNAUTHORIZED,
95 )
96
97 now = time()
98
99 # get from database if not in cache
100 # if not token_info:
101 token_info = self.db.get_one(self.tokens_collection, {"_id": token})
102 if token_info["expires"] < now:
103 raise AuthException(
104 "Expired Token or Authorization HTTP header",
105 http_code=HTTPStatus.UNAUTHORIZED,
106 )
107
108 return token_info
109
110 except DbException as e:
111 if e.http_code == HTTPStatus.NOT_FOUND:
112 raise AuthException(
113 "Invalid Token or Authorization HTTP header",
114 http_code=HTTPStatus.UNAUTHORIZED,
115 )
116 else:
117 raise
118 except AuthException:
119 raise
120 except Exception:
121 self.logger.exception(
122 "Error during token validation using internal backend"
123 )
124 raise AuthException(
125 "Error during token validation using internal backend",
126 http_code=HTTPStatus.UNAUTHORIZED,
127 )
128
129 def revoke_token(self, token):
130 """
131 Invalidate a token.
132
133 :param token: token to be revoked
134 """
135 try:
136 # self.token_cache.pop(token, None)
137 self.db.del_one(self.tokens_collection, {"_id": token})
138 return True
139 except DbException as e:
140 if e.http_code == HTTPStatus.NOT_FOUND:
141 raise AuthException(
142 "Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND
143 )
144 else:
145 # raise
146 exmsg = "Error during token revocation using internal backend"
147 self.logger.exception(exmsg)
148 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
149
150 def validate_user(self, user, password):
151 """
152 Validate username and password via appropriate backend.
153 :param user: username of the user.
154 :param password: password to be validated.
155 """
156 user_rows = self.db.get_list(
157 self.users_collection, {BaseTopic.id_field("users", user): user}
158 )
159 now = time()
160 user_content = None
161 if user:
162 user_rows = self.db.get_list(
163 self.users_collection,
164 {BaseTopic.id_field(self.users_collection, user): user},
165 )
166 if user_rows:
167 user_content = user_rows[0]
168 # Updating user_status for every system_admin id role login
169 mapped_roles = user_content.get("project_role_mappings")
170 for role in mapped_roles:
171 role_id = role.get("role")
172 role_assigned = self.db.get_one(
173 self.roles_collection,
174 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
175 )
176
177 if role_assigned.get("permissions")["admin"]:
178 if role_assigned.get("permissions")["default"]:
179 if self.config.get("user_management"):
180 filt = {}
181 users = self.db.get_list(self.users_collection, filt)
182 for user_info in users:
183 if not user_info.get("username") == "admin":
184 if not user_info.get("_admin").get(
185 "account_expire_time"
186 ):
187 expire = now + 86400 * self.config.get(
188 "account_expire_days"
189 )
190 self.db.set_one(
191 self.users_collection,
192 {"_id": user_info["_id"]},
193 {"_admin.account_expire_time": expire},
194 )
195 else:
196 if now > user_info.get("_admin").get(
197 "account_expire_time"
198 ):
199 self.db.set_one(
200 self.users_collection,
201 {"_id": user_info["_id"]},
202 {"_admin.user_status": "expired"},
203 )
204 break
205
206 # To add "admin" user_status key while upgrading osm setup with feature enabled
207 if user_content.get("username") == "admin":
208 if self.config.get("user_management"):
209 self.db.set_one(
210 self.users_collection,
211 {"_id": user_content["_id"]},
212 {"_admin.user_status": "always-active"},
213 )
214
215 if not user_content.get("username") == "admin":
216 if self.config.get("user_management"):
217 if not user_content.get("_admin").get("account_expire_time"):
218 account_expire_time = now + 86400 * self.config.get(
219 "account_expire_days"
220 )
221 self.db.set_one(
222 self.users_collection,
223 {"_id": user_content["_id"]},
224 {"_admin.account_expire_time": account_expire_time},
225 )
226 else:
227 account_expire_time = user_content.get("_admin").get(
228 "account_expire_time"
229 )
230
231 if now > account_expire_time:
232 self.db.set_one(
233 self.users_collection,
234 {"_id": user_content["_id"]},
235 {"_admin.user_status": "expired"},
236 )
237 raise AuthException(
238 "Account expired", http_code=HTTPStatus.UNAUTHORIZED
239 )
240
241 if user_content.get("_admin").get("user_status") == "locked":
242 raise AuthException(
243 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
244 )
245 elif user_content.get("_admin").get("user_status") == "expired":
246 raise AuthException(
247 "Failed to login as the account is expired"
248 )
249
250 salt = user_content["_admin"]["salt"]
251 shadow_password = sha256(
252 password.encode("utf-8") + salt.encode("utf-8")
253 ).hexdigest()
254 if shadow_password != user_content["password"]:
255 count = 1
256 if user_content.get("_admin").get("retry_count") >= 0:
257 count += user_content.get("_admin").get("retry_count")
258 self.db.set_one(
259 self.users_collection,
260 {"_id": user_content["_id"]},
261 {"_admin.retry_count": count},
262 )
263 self.logger.debug(
264 "Failed Authentications count: {}".format(count)
265 )
266
267 if user_content.get("username") == "admin":
268 user_content = None
269 else:
270 if not self.config.get("user_management"):
271 user_content = None
272 else:
273 if (
274 user_content.get("_admin").get("retry_count")
275 >= self.config["max_pwd_attempt"] - 1
276 ):
277 self.db.set_one(
278 self.users_collection,
279 {"_id": user_content["_id"]},
280 {"_admin.user_status": "locked"},
281 )
282 raise AuthException(
283 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
284 )
285 else:
286 user_content = None
287 return user_content
288
289 def authenticate(self, credentials, token_info=None):
290 """
291 Authenticate a user using username/password or previous token_info plus project; its creates a new token
292
293 :param credentials: dictionary that contains:
294 username: name, id or None
295 password: password or None
296 project_id: name, id, or None. If None first found project will be used to get an scope token
297 other items are allowed and ignored
298 :param token_info: previous token_info to obtain authorization
299 :return: the scoped token info or raises an exception. The token is a dictionary with:
300 _id: token string id,
301 username: username,
302 project_id: scoped_token project_id,
303 project_name: scoped_token project_name,
304 expires: epoch time when it expires,
305 """
306
307 now = time()
308 user_content = None
309 user = credentials.get("username")
310 password = credentials.get("password")
311 project = credentials.get("project_id")
312
313 # Try using username/password
314 if user:
315 user_content = self.validate_user(user, password)
316 if not user_content:
317 cef_event(
318 self.cef_logger,
319 {
320 "name": "User login",
321 "sourceUserName": user,
322 "message": "Invalid username/password Project={} Outcome=Failure".format(
323 project
324 ),
325 "severity": "3",
326 },
327 )
328 self.logger.exception("{}".format(self.cef_logger))
329 raise AuthException(
330 "Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED
331 )
332 if not user_content.get("_admin", None):
333 raise AuthException(
334 "No default project for this user.",
335 http_code=HTTPStatus.UNAUTHORIZED,
336 )
337 elif token_info:
338 user_rows = self.db.get_list(
339 self.users_collection, {"username": token_info["username"]}
340 )
341 if user_rows:
342 user_content = user_rows[0]
343 else:
344 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
345 else:
346 raise AuthException(
347 "Provide credentials: username/password or Authorization Bearer token",
348 http_code=HTTPStatus.UNAUTHORIZED,
349 )
350 # Delay upon second request within time window
351 if (
352 now - user_content["_admin"].get("last_token_time", 0)
353 < self.token_time_window
354 ):
355 sleep(self.token_delay)
356 # user_content["_admin"]["last_token_time"] = now
357 # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions
358 user_data = {
359 "_admin.last_token_time": now,
360 "_admin.retry_count": 0,
361 }
362 self.db.set_one(
363 self.users_collection,
364 {"_id": user_content["_id"]},
365 user_data,
366 )
367
368 token_id = "".join(
369 random_choice(
370 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
371 )
372 for _ in range(0, 32)
373 )
374
375 # projects = user_content.get("projects", [])
376 prm_list = user_content.get("project_role_mappings", [])
377
378 if not project:
379 project = prm_list[0]["project"] if prm_list else None
380 if not project:
381 raise AuthException(
382 "can't find a default project for this user",
383 http_code=HTTPStatus.UNAUTHORIZED,
384 )
385
386 projects = [prm["project"] for prm in prm_list]
387
388 proj = self.db.get_one(
389 self.projects_collection, {BaseTopic.id_field("projects", project): project}
390 )
391 project_name = proj["name"]
392 project_id = proj["_id"]
393 if project_name not in projects and project_id not in projects:
394 raise AuthException(
395 "project {} not allowed for this user".format(project),
396 http_code=HTTPStatus.UNAUTHORIZED,
397 )
398
399 # TODO remove admin, this vill be used by roles RBAC
400 if project_name == "admin":
401 token_admin = True
402 else:
403 token_admin = proj.get("admin", False)
404
405 # add token roles
406 roles = []
407 roles_list = []
408 for prm in prm_list:
409 if prm["project"] in [project_id, project_name]:
410 role = self.db.get_one(
411 self.roles_collection,
412 {BaseTopic.id_field("roles", prm["role"]): prm["role"]},
413 )
414 rid = role["_id"]
415 if rid not in roles:
416 rnm = role["name"]
417 roles.append(rid)
418 roles_list.append({"name": rnm, "id": rid})
419 if not roles_list:
420 rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})[
421 "_id"
422 ]
423 roles_list = [{"name": "project_admin", "id": rid}]
424
425 login_count = user_content.get("_admin").get("retry_count")
426 last_token_time = user_content.get("_admin").get("last_token_time")
427
428 admin_show = False
429 user_show = False
430 if self.config.get("user_management"):
431 for role in roles_list:
432 role_id = role.get("id")
433 permission = self.db.get_one(
434 self.roles_collection,
435 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
436 )
437 if permission.get("permissions")["admin"]:
438 if permission.get("permissions")["default"]:
439 admin_show = True
440 break
441 else:
442 user_show = True
443 new_token = {
444 "issued_at": now,
445 "expires": now + 3600,
446 "_id": token_id,
447 "id": token_id,
448 "project_id": proj["_id"],
449 "project_name": proj["name"],
450 "username": user_content["username"],
451 "user_id": user_content["_id"],
452 "admin": token_admin,
453 "roles": roles_list,
454 "login_count": login_count,
455 "last_login": last_token_time,
456 "admin_show": admin_show,
457 "user_show": user_show,
458 }
459
460 self.db.create(self.tokens_collection, new_token)
461 return deepcopy(new_token)
462
463 def get_role_list(self, filter_q={}):
464 """
465 Get role list.
466
467 :return: returns the list of roles.
468 """
469 return self.db.get_list(self.roles_collection, filter_q)
470
471 def create_role(self, role_info):
472 """
473 Create a role.
474
475 :param role_info: full role info.
476 :return: returns the role id.
477 :raises AuthconnOperationException: if role creation failed.
478 """
479 # TODO: Check that role name does not exist ?
480 rid = str(uuid4())
481 role_info["_id"] = rid
482 rid = self.db.create(self.roles_collection, role_info)
483 return rid
484
485 def delete_role(self, role_id):
486 """
487 Delete a role.
488
489 :param role_id: role identifier.
490 :raises AuthconnOperationException: if role deletion failed.
491 """
492 rc = self.db.del_one(self.roles_collection, {"_id": role_id})
493 self.db.del_list(self.tokens_collection, {"roles.id": role_id})
494 return rc
495
496 def update_role(self, role_info):
497 """
498 Update a role.
499
500 :param role_info: full role info.
501 :return: returns the role name and id.
502 :raises AuthconnOperationException: if user creation failed.
503 """
504 rid = role_info["_id"]
505 self.db.set_one(self.roles_collection, {"_id": rid}, role_info)
506 return {"_id": rid, "name": role_info["name"]}
507
508 def create_user(self, user_info):
509 """
510 Create a user.
511
512 :param user_info: full user info.
513 :return: returns the username and id of the user.
514 """
515 BaseTopic.format_on_new(user_info, make_public=False)
516 salt = uuid4().hex
517 user_info["_admin"]["salt"] = salt
518 user_info["_admin"]["user_status"] = "active"
519 present = time()
520 if not user_info["username"] == "admin":
521 if self.config.get("user_management"):
522 user_info["_admin"]["modified"] = present
523 user_info["_admin"]["password_expire_time"] = present
524 account_expire_time = present + 86400 * self.config.get(
525 "account_expire_days"
526 )
527 user_info["_admin"]["account_expire_time"] = account_expire_time
528
529 user_info["_admin"]["retry_count"] = 0
530 user_info["_admin"]["last_token_time"] = present
531 if "password" in user_info:
532 user_info["password"] = sha256(
533 user_info["password"].encode("utf-8") + salt.encode("utf-8")
534 ).hexdigest()
535 user_info["_admin"]["password_history"] = {salt: user_info["password"]}
536 # "projects" are not stored any more
537 if "projects" in user_info:
538 del user_info["projects"]
539 self.db.create(self.users_collection, user_info)
540 return {"username": user_info["username"], "_id": user_info["_id"]}
541
542 def update_user(self, user_info):
543 """
544 Change the user name and/or password.
545
546 :param user_info: user info modifications
547 """
548 uid = user_info["_id"]
549 old_pwd = user_info.get("old_password")
550 unlock = user_info.get("unlock")
551 renew = user_info.get("renew")
552 permission_id = user_info.get("system_admin_id")
553 now = time()
554
555 user_data = self.db.get_one(
556 self.users_collection, {BaseTopic.id_field("users", uid): uid}
557 )
558 if old_pwd:
559 salt = user_data["_admin"]["salt"]
560 shadow_password = sha256(
561 old_pwd.encode("utf-8") + salt.encode("utf-8")
562 ).hexdigest()
563 if shadow_password != user_data["password"]:
564 raise AuthconnConflictException(
565 "Incorrect password", http_code=HTTPStatus.CONFLICT
566 )
567 # Unlocking the user
568 if unlock:
569 system_user = None
570 unlock_state = False
571 if not permission_id:
572 raise AuthconnConflictException(
573 "system_admin_id is the required field to unlock the user",
574 http_code=HTTPStatus.CONFLICT,
575 )
576 else:
577 system_user = self.db.get_one(
578 self.users_collection,
579 {
580 BaseTopic.id_field(
581 self.users_collection, permission_id
582 ): permission_id
583 },
584 )
585 mapped_roles = system_user.get("project_role_mappings")
586 for role in mapped_roles:
587 role_id = role.get("role")
588 role_assigned = self.db.get_one(
589 self.roles_collection,
590 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
591 )
592 if role_assigned.get("permissions")["admin"]:
593 if role_assigned.get("permissions")["default"]:
594 user_data["_admin"]["retry_count"] = 0
595 if now > user_data["_admin"]["account_expire_time"]:
596 user_data["_admin"]["user_status"] = "expired"
597 else:
598 user_data["_admin"]["user_status"] = "active"
599 unlock_state = True
600 break
601 if not unlock_state:
602 raise AuthconnConflictException(
603 "User '{}' does not have the privilege to unlock the user".format(
604 permission_id
605 ),
606 http_code=HTTPStatus.CONFLICT,
607 )
608 # Renewing the user
609 if renew:
610 system_user = None
611 renew_state = False
612 if not permission_id:
613 raise AuthconnConflictException(
614 "system_admin_id is the required field to renew the user",
615 http_code=HTTPStatus.CONFLICT,
616 )
617 else:
618 system_user = self.db.get_one(
619 self.users_collection,
620 {
621 BaseTopic.id_field(
622 self.users_collection, permission_id
623 ): permission_id
624 },
625 )
626 mapped_roles = system_user.get("project_role_mappings")
627 for role in mapped_roles:
628 role_id = role.get("role")
629 role_assigned = self.db.get_one(
630 self.roles_collection,
631 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
632 )
633 if role_assigned.get("permissions")["admin"]:
634 if role_assigned.get("permissions")["default"]:
635 present = time()
636 account_expire = (
637 present + 86400 * self.config["account_expire_days"]
638 )
639 user_data["_admin"]["modified"] = present
640 user_data["_admin"]["account_expire_time"] = account_expire
641 if (
642 user_data["_admin"]["retry_count"]
643 >= self.config["max_pwd_attempt"]
644 ):
645 user_data["_admin"]["user_status"] = "locked"
646 else:
647 user_data["_admin"]["user_status"] = "active"
648 renew_state = True
649 break
650 if not renew_state:
651 raise AuthconnConflictException(
652 "User '{}' does not have the privilege to renew the user".format(
653 permission_id
654 ),
655 http_code=HTTPStatus.CONFLICT,
656 )
657 BaseTopic.format_on_edit(user_data, user_info)
658 # User Name
659 usnm = user_info.get("username")
660 if usnm:
661 user_data["username"] = usnm
662 # If password is given and is not already encripted
663 pswd = user_info.get("password")
664 if pswd and (
665 len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd)
666 ): # TODO: Improve check?
667 cef_event(
668 self.cef_logger,
669 {
670 "name": "Change Password",
671 "sourceUserName": user_data["username"],
672 "message": "Changing Password for user, Outcome=Success",
673 "severity": "2",
674 },
675 )
676 self.logger.info("{}".format(self.cef_logger))
677 salt = uuid4().hex
678 if "_admin" not in user_data:
679 user_data["_admin"] = {}
680 if user_data.get("_admin").get("password_history"):
681 old_pwds = user_data.get("_admin").get("password_history")
682 else:
683 old_pwds = {}
684 for k, v in old_pwds.items():
685 shadow_password = sha256(
686 pswd.encode("utf-8") + k.encode("utf-8")
687 ).hexdigest()
688 if v == shadow_password:
689 raise AuthconnConflictException(
690 "Password is used before", http_code=HTTPStatus.CONFLICT
691 )
692 user_data["_admin"]["salt"] = salt
693 user_data["password"] = sha256(
694 pswd.encode("utf-8") + salt.encode("utf-8")
695 ).hexdigest()
696 if len(old_pwds) >= 3:
697 old_pwds.pop(list(old_pwds.keys())[0])
698 old_pwds.update({salt: user_data["password"]})
699 user_data["_admin"]["password_history"] = old_pwds
700 if not user_data["username"] == "admin":
701 if self.config.get("user_management"):
702 present = time()
703 if self.config.get("pwd_expire_days"):
704 expire = present + 86400 * self.config.get("pwd_expire_days")
705 user_data["_admin"]["modified"] = present
706 user_data["_admin"]["password_expire_time"] = expire
707 # Project-Role Mappings
708 # TODO: Check that user_info NEVER includes "project_role_mappings"
709 if "project_role_mappings" not in user_data:
710 user_data["project_role_mappings"] = []
711 for prm in user_info.get("add_project_role_mappings", []):
712 user_data["project_role_mappings"].append(prm)
713 for prm in user_info.get("remove_project_role_mappings", []):
714 for pidf in ["project", "project_name"]:
715 for ridf in ["role", "role_name"]:
716 try:
717 user_data["project_role_mappings"].remove(
718 {"role": prm[ridf], "project": prm[pidf]}
719 )
720 except KeyError:
721 pass
722 except ValueError:
723 pass
724 idf = BaseTopic.id_field("users", uid)
725 self.db.set_one(self.users_collection, {idf: uid}, user_data)
726 if user_info.get("remove_project_role_mappings"):
727 idf = "user_id" if idf == "_id" else idf
728 self.db.del_list(self.tokens_collection, {idf: uid})
729
730 def delete_user(self, user_id):
731 """
732 Delete user.
733
734 :param user_id: user identifier.
735 :raises AuthconnOperationException: if user deletion failed.
736 """
737 self.db.del_one(self.users_collection, {"_id": user_id})
738 self.db.del_list(self.tokens_collection, {"user_id": user_id})
739 return True
740
741 def get_user_list(self, filter_q=None):
742 """
743 Get user list.
744
745 :param filter_q: dictionary to filter user list by:
746 name (username is also admitted). If a user id is equal to the filter name, it is also provided
747 other
748 :return: returns a list of users.
749 """
750 filt = filter_q or {}
751 if "name" in filt: # backward compatibility
752 filt["username"] = filt.pop("name")
753 if filt.get("username") and is_valid_uuid(filt["username"]):
754 # username cannot be a uuid. If this is the case, change from username to _id
755 filt["_id"] = filt.pop("username")
756 users = self.db.get_list(self.users_collection, filt)
757 project_id_name = {}
758 role_id_name = {}
759 for user in users:
760 prms = user.get("project_role_mappings")
761 projects = user.get("projects")
762 if prms:
763 projects = []
764 # add project_name and role_name. Generate projects for backward compatibility
765 for prm in prms:
766 project_id = prm["project"]
767 if project_id not in project_id_name:
768 pr = self.db.get_one(
769 self.projects_collection,
770 {BaseTopic.id_field("projects", project_id): project_id},
771 fail_on_empty=False,
772 )
773 project_id_name[project_id] = pr["name"] if pr else None
774 prm["project_name"] = project_id_name[project_id]
775 if prm["project_name"] not in projects:
776 projects.append(prm["project_name"])
777
778 role_id = prm["role"]
779 if role_id not in role_id_name:
780 role = self.db.get_one(
781 self.roles_collection,
782 {BaseTopic.id_field("roles", role_id): role_id},
783 fail_on_empty=False,
784 )
785 role_id_name[role_id] = role["name"] if role else None
786 prm["role_name"] = role_id_name[role_id]
787 user["projects"] = projects # for backward compatibility
788 elif projects:
789 # user created with an old version. Create a project_role mapping with role project_admin
790 user["project_role_mappings"] = []
791 role = self.db.get_one(
792 self.roles_collection,
793 {BaseTopic.id_field("roles", "project_admin"): "project_admin"},
794 )
795 for p_id_name in projects:
796 pr = self.db.get_one(
797 self.projects_collection,
798 {BaseTopic.id_field("projects", p_id_name): p_id_name},
799 )
800 prm = {
801 "project": pr["_id"],
802 "project_name": pr["name"],
803 "role_name": "project_admin",
804 "role": role["_id"],
805 }
806 user["project_role_mappings"].append(prm)
807 else:
808 user["projects"] = []
809 user["project_role_mappings"] = []
810
811 return users
812
813 def get_project_list(self, filter_q={}):
814 """
815 Get role list.
816
817 :return: returns the list of projects.
818 """
819 return self.db.get_list(self.projects_collection, filter_q)
820
821 def create_project(self, project_info):
822 """
823 Create a project.
824
825 :param project: full project info.
826 :return: the internal id of the created project
827 :raises AuthconnOperationException: if project creation failed.
828 """
829 pid = self.db.create(self.projects_collection, project_info)
830 return pid
831
832 def delete_project(self, project_id):
833 """
834 Delete a project.
835
836 :param project_id: project identifier.
837 :raises AuthconnOperationException: if project deletion failed.
838 """
839 idf = BaseTopic.id_field("projects", project_id)
840 r = self.db.del_one(self.projects_collection, {idf: project_id})
841 idf = "project_id" if idf == "_id" else "project_name"
842 self.db.del_list(self.tokens_collection, {idf: project_id})
843 return r
844
845 def update_project(self, project_id, project_info):
846 """
847 Change the name of a project
848
849 :param project_id: project to be changed
850 :param project_info: full project info
851 :return: None
852 :raises AuthconnOperationException: if project update failed.
853 """
854 self.db.set_one(
855 self.projects_collection,
856 {BaseTopic.id_field("projects", project_id): project_id},
857 project_info,
858 )