Code Coverage

Cobertura Coverage Report > osm_nbi >

authconn_internal.py

Trend

File Coverage summary

NameClassesLinesConditionals
authconn_internal.py
100%
1/1
32%
118/374
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
authconn_internal.py
32%
118/374
N/A

Source

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