blob: 94e6e47aa12f2bb0bd03bfb5183ac24c3bdeb45b [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
garciadeblas4568a372021-03-24 09:19:48 +010027__author__ = (
28 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>, "
29 "Alfonso Tierno <alfonso.tiernosepulveda@telefoncia.com"
30)
delacruzramoceb8baf2019-06-21 14:25:38 +020031__date__ = "$06-jun-2019 11:16:08$"
32
tierno5ec768a2020-03-31 09:46:44 +000033import logging
34import re
35
garciadeblasf2af4a12023-01-24 16:56:54 +010036from osm_nbi.authconn import (
37 Authconn,
38 AuthException,
39 AuthconnConflictException,
40) # , AuthconnOperationException
delacruzramoceb8baf2019-06-21 14:25:38 +020041from osm_common.dbbase import DbException
tierno23acf402019-08-28 13:36:34 +000042from osm_nbi.base_topic import BaseTopic
elumalai7802ff82023-04-24 20:38:32 +053043from osm_nbi.utils import cef_event, cef_event_builder
tierno5ec768a2020-03-31 09:46:44 +000044from osm_nbi.validation import is_valid_uuid
delacruzramoad682a52019-12-10 16:26:34 +010045from time import time, sleep
delacruzramoceb8baf2019-06-21 14:25:38 +020046from http import HTTPStatus
47from uuid import uuid4
48from hashlib import sha256
49from copy import deepcopy
50from random import choice as random_choice
51
52
53class AuthconnInternal(Authconn):
garciadeblas4568a372021-03-24 09:19:48 +010054 token_time_window = 2 # seconds
55 token_delay = 1 # seconds to wait upon second request within time window
delacruzramoceb8baf2019-06-21 14:25:38 +020056
K Sai Kiran7ddb0732020-10-30 11:14:44 +053057 users_collection = "users"
58 roles_collection = "roles"
59 projects_collection = "projects"
60 tokens_collection = "tokens"
61
tierno9e87a7f2020-03-23 09:24:10 +000062 def __init__(self, config, db, role_permissions):
63 Authconn.__init__(self, config, db, role_permissions)
delacruzramoceb8baf2019-06-21 14:25:38 +020064 self.logger = logging.getLogger("nbi.authenticator.internal")
65
delacruzramoceb8baf2019-06-21 14:25:38 +020066 self.db = db
delacruzramoad682a52019-12-10 16:26:34 +010067 # self.msg = msg
68 # self.token_cache = token_cache
delacruzramoceb8baf2019-06-21 14:25:38 +020069
70 # To be Confirmed
delacruzramoceb8baf2019-06-21 14:25:38 +020071 self.sess = None
elumalai7802ff82023-04-24 20:38:32 +053072 self.cef_logger = cef_event_builder(config)
delacruzramoceb8baf2019-06-21 14:25:38 +020073
delacruzramoceb8baf2019-06-21 14:25:38 +020074 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:
garciadeblas4568a372021-03-24 09:19:48 +010092 raise AuthException(
93 "Needed a token or Authorization HTTP header",
94 http_code=HTTPStatus.UNAUTHORIZED,
95 )
delacruzramoceb8baf2019-06-21 14:25:38 +020096
delacruzramoceb8baf2019-06-21 14:25:38 +020097 now = time()
delacruzramoceb8baf2019-06-21 14:25:38 +020098
99 # get from database if not in cache
delacruzramoad682a52019-12-10 16:26:34 +0100100 # if not token_info:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530101 token_info = self.db.get_one(self.tokens_collection, {"_id": token})
delacruzramoad682a52019-12-10 16:26:34 +0100102 if token_info["expires"] < now:
garciadeblas4568a372021-03-24 09:19:48 +0100103 raise AuthException(
104 "Expired Token or Authorization HTTP header",
105 http_code=HTTPStatus.UNAUTHORIZED,
106 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200107
tierno701018c2019-06-25 11:13:14 +0000108 return token_info
delacruzramoceb8baf2019-06-21 14:25:38 +0200109
110 except DbException as e:
111 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100112 raise AuthException(
113 "Invalid Token or Authorization HTTP header",
114 http_code=HTTPStatus.UNAUTHORIZED,
115 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200116 else:
117 raise
118 except AuthException:
tiernoe1eb3b22019-08-26 15:59:24 +0000119 raise
delacruzramoceb8baf2019-06-21 14:25:38 +0200120 except Exception:
garciadeblas4568a372021-03-24 09:19:48 +0100121 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 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200128
129 def revoke_token(self, token):
130 """
131 Invalidate a token.
132
133 :param token: token to be revoked
134 """
135 try:
delacruzramoad682a52019-12-10 16:26:34 +0100136 # self.token_cache.pop(token, None)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530137 self.db.del_one(self.tokens_collection, {"_id": token})
delacruzramoceb8baf2019-06-21 14:25:38 +0200138 return True
139 except DbException as e:
140 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100141 raise AuthException(
142 "Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND
143 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200144 else:
145 # raise
delacruzramoad682a52019-12-10 16:26:34 +0100146 exmsg = "Error during token revocation using internal backend"
147 self.logger.exception(exmsg)
148 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoceb8baf2019-06-21 14:25:38 +0200149
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530150 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 """
garciadeblas4568a372021-03-24 09:19:48 +0100156 user_rows = self.db.get_list(
157 self.users_collection, {BaseTopic.id_field("users", user): user}
158 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200159 now = time()
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530160 user_content = None
garciadeblas6d83f8f2023-06-19 22:34:49 +0200161 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
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530287 return user_content
288
tierno6486f742020-02-13 16:30:14 +0000289 def authenticate(self, credentials, token_info=None):
delacruzramoceb8baf2019-06-21 14:25:38 +0200290 """
tierno701018c2019-06-25 11:13:14 +0000291 Authenticate a user using username/password or previous token_info plus project; its creates a new token
delacruzramoceb8baf2019-06-21 14:25:38 +0200292
tierno6486f742020-02-13 16:30:14 +0000293 :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
tierno701018c2019-06-25 11:13:14 +0000298 :param token_info: previous token_info to obtain authorization
delacruzramoceb8baf2019-06-21 14:25:38 +0200299 :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
tierno6486f742020-02-13 16:30:14 +0000309 user = credentials.get("username")
310 password = credentials.get("password")
311 project = credentials.get("project_id")
delacruzramoceb8baf2019-06-21 14:25:38 +0200312
delacruzramo01b15d32019-07-02 14:37:47 +0200313 # Try using username/password
314 if user:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530315 user_content = self.validate_user(user, password)
delacruzramo01b15d32019-07-02 14:37:47 +0200316 if not user_content:
elumalai7802ff82023-04-24 20:38:32 +0530317 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))
garciadeblas4568a372021-03-24 09:19:48 +0100329 raise AuthException(
330 "Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED
331 )
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530332 if not user_content.get("_admin", None):
garciadeblas4568a372021-03-24 09:19:48 +0100333 raise AuthException(
334 "No default project for this user.",
335 http_code=HTTPStatus.UNAUTHORIZED,
336 )
delacruzramo01b15d32019-07-02 14:37:47 +0200337 elif token_info:
garciadeblas4568a372021-03-24 09:19:48 +0100338 user_rows = self.db.get_list(
339 self.users_collection, {"username": token_info["username"]}
340 )
delacruzramo01b15d32019-07-02 14:37:47 +0200341 if user_rows:
342 user_content = user_rows[0]
delacruzramoceb8baf2019-06-21 14:25:38 +0200343 else:
delacruzramo01b15d32019-07-02 14:37:47 +0200344 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
345 else:
garciadeblas4568a372021-03-24 09:19:48 +0100346 raise AuthException(
347 "Provide credentials: username/password or Authorization Bearer token",
348 http_code=HTTPStatus.UNAUTHORIZED,
349 )
delacruzramoad682a52019-12-10 16:26:34 +0100350 # Delay upon second request within time window
garciadeblas4568a372021-03-24 09:19:48 +0100351 if (
352 now - user_content["_admin"].get("last_token_time", 0)
353 < self.token_time_window
354 ):
delacruzramoad682a52019-12-10 16:26:34 +0100355 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
garciadeblas6d83f8f2023-06-19 22:34:49 +0200358 user_data = {
359 "_admin.last_token_time": now,
360 "_admin.retry_count": 0,
361 }
garciadeblas4568a372021-03-24 09:19:48 +0100362 self.db.set_one(
363 self.users_collection,
364 {"_id": user_content["_id"]},
garciadeblas6d83f8f2023-06-19 22:34:49 +0200365 user_data,
garciadeblas4568a372021-03-24 09:19:48 +0100366 )
delacruzramoad682a52019-12-10 16:26:34 +0100367
garciadeblas4568a372021-03-24 09:19:48 +0100368 token_id = "".join(
369 random_choice(
370 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
371 )
372 for _ in range(0, 32)
373 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200374
delacruzramo01b15d32019-07-02 14:37:47 +0200375 # projects = user_content.get("projects", [])
376 prm_list = user_content.get("project_role_mappings", [])
delacruzramoceb8baf2019-06-21 14:25:38 +0200377
delacruzramo01b15d32019-07-02 14:37:47 +0200378 if not project:
379 project = prm_list[0]["project"] if prm_list else None
380 if not project:
garciadeblas4568a372021-03-24 09:19:48 +0100381 raise AuthException(
382 "can't find a default project for this user",
383 http_code=HTTPStatus.UNAUTHORIZED,
384 )
tierno701018c2019-06-25 11:13:14 +0000385
delacruzramo01b15d32019-07-02 14:37:47 +0200386 projects = [prm["project"] for prm in prm_list]
delacruzramoceb8baf2019-06-21 14:25:38 +0200387
garciadeblas4568a372021-03-24 09:19:48 +0100388 proj = self.db.get_one(
389 self.projects_collection, {BaseTopic.id_field("projects", project): project}
390 )
delacruzramo01b15d32019-07-02 14:37:47 +0200391 project_name = proj["name"]
392 project_id = proj["_id"]
393 if project_name not in projects and project_id not in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100394 raise AuthException(
395 "project {} not allowed for this user".format(project),
396 http_code=HTTPStatus.UNAUTHORIZED,
397 )
tierno701018c2019-06-25 11:13:14 +0000398
delacruzramo01b15d32019-07-02 14:37:47 +0200399 # 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)
delacruzramoceb8baf2019-06-21 14:25:38 +0200404
delacruzramo01b15d32019-07-02 14:37:47 +0200405 # add token roles
406 roles = []
407 roles_list = []
408 for prm in prm_list:
409 if prm["project"] in [project_id, project_name]:
garciadeblas4568a372021-03-24 09:19:48 +0100410 role = self.db.get_one(
411 self.roles_collection,
412 {BaseTopic.id_field("roles", prm["role"]): prm["role"]},
413 )
delacruzramo01b15d32019-07-02 14:37:47 +0200414 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:
garciadeblas4568a372021-03-24 09:19:48 +0100420 rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})[
421 "_id"
422 ]
delacruzramo01b15d32019-07-02 14:37:47 +0200423 roles_list = [{"name": "project_admin", "id": rid}]
delacruzramoceb8baf2019-06-21 14:25:38 +0200424
garciadeblas6d83f8f2023-06-19 22:34:49 +0200425 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
garciadeblas4568a372021-03-24 09:19:48 +0100443 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,
garciadeblas6d83f8f2023-06-19 22:34:49 +0200454 "login_count": login_count,
455 "last_login": last_token_time,
456 "admin_show": admin_show,
457 "user_show": user_show,
garciadeblas4568a372021-03-24 09:19:48 +0100458 }
delacruzramoceb8baf2019-06-21 14:25:38 +0200459
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530460 self.db.create(self.tokens_collection, new_token)
delacruzramo01b15d32019-07-02 14:37:47 +0200461 return deepcopy(new_token)
462
463 def get_role_list(self, filter_q={}):
delacruzramoceb8baf2019-06-21 14:25:38 +0200464 """
465 Get role list.
466
467 :return: returns the list of roles.
468 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530469 return self.db.get_list(self.roles_collection, filter_q)
delacruzramoceb8baf2019-06-21 14:25:38 +0200470
delacruzramo01b15d32019-07-02 14:37:47 +0200471 def create_role(self, role_info):
delacruzramoceb8baf2019-06-21 14:25:38 +0200472 """
473 Create a role.
474
delacruzramo01b15d32019-07-02 14:37:47 +0200475 :param role_info: full role info.
476 :return: returns the role id.
delacruzramoceb8baf2019-06-21 14:25:38 +0200477 :raises AuthconnOperationException: if role creation failed.
478 """
delacruzramoceb8baf2019-06-21 14:25:38 +0200479 # TODO: Check that role name does not exist ?
delacruzramo01b15d32019-07-02 14:37:47 +0200480 rid = str(uuid4())
481 role_info["_id"] = rid
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530482 rid = self.db.create(self.roles_collection, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200483 return rid
delacruzramoceb8baf2019-06-21 14:25:38 +0200484
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 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530492 rc = self.db.del_one(self.roles_collection, {"_id": role_id})
493 self.db.del_list(self.tokens_collection, {"roles.id": role_id})
delacruzramoad682a52019-12-10 16:26:34 +0100494 return rc
delacruzramo01b15d32019-07-02 14:37:47 +0200495
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"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530505 self.db.set_one(self.roles_collection, {"_id": rid}, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200506 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
garciadeblas6d83f8f2023-06-19 22:34:49 +0200518 user_info["_admin"]["user_status"] = "active"
selvi.ja9a1fc82022-04-04 06:54:30 +0000519 present = time()
520 if not user_info["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200521 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
delacruzramo01b15d32019-07-02 14:37:47 +0200531 if "password" in user_info:
garciadeblas4568a372021-03-24 09:19:48 +0100532 user_info["password"] = sha256(
533 user_info["password"].encode("utf-8") + salt.encode("utf-8")
534 ).hexdigest()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200535 user_info["_admin"]["password_history"] = {salt: user_info["password"]}
delacruzramo01b15d32019-07-02 14:37:47 +0200536 # "projects" are not stored any more
537 if "projects" in user_info:
538 del user_info["projects"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530539 self.db.create(self.users_collection, user_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200540 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"]
selvi.ja9a1fc82022-04-04 06:54:30 +0000549 old_pwd = user_info.get("old_password")
garciadeblas6d83f8f2023-06-19 22:34:49 +0200550 unlock = user_info.get("unlock")
551 renew = user_info.get("renew")
552 permission_id = user_info.get("system_admin_id")
Adurti0c9b0102023-11-08 11:16:32 +0000553 now = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200554
garciadeblas4568a372021-03-24 09:19:48 +0100555 user_data = self.db.get_one(
556 self.users_collection, {BaseTopic.id_field("users", uid): uid}
557 )
selvi.ja9a1fc82022-04-04 06:54:30 +0000558 if old_pwd:
559 salt = user_data["_admin"]["salt"]
garciadeblasf2af4a12023-01-24 16:56:54 +0100560 shadow_password = sha256(
561 old_pwd.encode("utf-8") + salt.encode("utf-8")
562 ).hexdigest()
selvi.ja9a1fc82022-04-04 06:54:30 +0000563 if shadow_password != user_data["password"]:
564 raise AuthconnConflictException(
garciadeblasf2af4a12023-01-24 16:56:54 +0100565 "Incorrect password", http_code=HTTPStatus.CONFLICT
selvi.ja9a1fc82022-04-04 06:54:30 +0000566 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200567 # 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
Adurti0c9b0102023-11-08 11:16:32 +0000595 if now > user_data["_admin"]["account_expire_time"]:
596 user_data["_admin"]["user_status"] = "expired"
597 else:
598 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200599 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
Adurti0c9b0102023-11-08 11:16:32 +0000641 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"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200648 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 )
delacruzramo01b15d32019-07-02 14:37:47 +0200657 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")
garciadeblas4568a372021-03-24 09:19:48 +0100664 if pswd and (
665 len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd)
666 ): # TODO: Improve check?
elumalai7802ff82023-04-24 20:38:32 +0530667 cef_event(
668 self.cef_logger,
669 {
670 "name": "Change Password",
671 "sourceUserName": user_data["username"],
garciadeblasf53612b2024-07-12 14:44:37 +0200672 "message": "User {} changing Password for user {}, Outcome=Success".format(
673 user_info.get("session_user"), user_data["username"]
674 ),
elumalai7802ff82023-04-24 20:38:32 +0530675 "severity": "2",
676 },
677 )
678 self.logger.info("{}".format(self.cef_logger))
delacruzramo01b15d32019-07-02 14:37:47 +0200679 salt = uuid4().hex
680 if "_admin" not in user_data:
681 user_data["_admin"] = {}
garciadeblas6d83f8f2023-06-19 22:34:49 +0200682 if user_data.get("_admin").get("password_history"):
683 old_pwds = user_data.get("_admin").get("password_history")
684 else:
685 old_pwds = {}
686 for k, v in old_pwds.items():
687 shadow_password = sha256(
688 pswd.encode("utf-8") + k.encode("utf-8")
689 ).hexdigest()
690 if v == shadow_password:
691 raise AuthconnConflictException(
692 "Password is used before", http_code=HTTPStatus.CONFLICT
693 )
delacruzramo01b15d32019-07-02 14:37:47 +0200694 user_data["_admin"]["salt"] = salt
garciadeblas4568a372021-03-24 09:19:48 +0100695 user_data["password"] = sha256(
696 pswd.encode("utf-8") + salt.encode("utf-8")
697 ).hexdigest()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200698 if len(old_pwds) >= 3:
699 old_pwds.pop(list(old_pwds.keys())[0])
700 old_pwds.update({salt: user_data["password"]})
701 user_data["_admin"]["password_history"] = old_pwds
selvi.ja9a1fc82022-04-04 06:54:30 +0000702 if not user_data["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200703 if self.config.get("user_management"):
selvi.ja9a1fc82022-04-04 06:54:30 +0000704 present = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200705 if self.config.get("pwd_expire_days"):
706 expire = present + 86400 * self.config.get("pwd_expire_days")
707 user_data["_admin"]["modified"] = present
708 user_data["_admin"]["password_expire_time"] = expire
delacruzramo01b15d32019-07-02 14:37:47 +0200709 # Project-Role Mappings
710 # TODO: Check that user_info NEVER includes "project_role_mappings"
711 if "project_role_mappings" not in user_data:
712 user_data["project_role_mappings"] = []
713 for prm in user_info.get("add_project_role_mappings", []):
714 user_data["project_role_mappings"].append(prm)
715 for prm in user_info.get("remove_project_role_mappings", []):
716 for pidf in ["project", "project_name"]:
717 for ridf in ["role", "role_name"]:
718 try:
garciadeblas4568a372021-03-24 09:19:48 +0100719 user_data["project_role_mappings"].remove(
720 {"role": prm[ridf], "project": prm[pidf]}
721 )
delacruzramo01b15d32019-07-02 14:37:47 +0200722 except KeyError:
723 pass
724 except ValueError:
725 pass
delacruzramo3d6881c2019-12-04 13:42:26 +0100726 idf = BaseTopic.id_field("users", uid)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530727 self.db.set_one(self.users_collection, {idf: uid}, user_data)
delacruzramo3d6881c2019-12-04 13:42:26 +0100728 if user_info.get("remove_project_role_mappings"):
delacruzramoad682a52019-12-10 16:26:34 +0100729 idf = "user_id" if idf == "_id" else idf
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530730 self.db.del_list(self.tokens_collection, {idf: uid})
delacruzramo01b15d32019-07-02 14:37:47 +0200731
732 def delete_user(self, user_id):
733 """
734 Delete user.
735
736 :param user_id: user identifier.
737 :raises AuthconnOperationException: if user deletion failed.
738 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530739 self.db.del_one(self.users_collection, {"_id": user_id})
740 self.db.del_list(self.tokens_collection, {"user_id": user_id})
delacruzramoceb8baf2019-06-21 14:25:38 +0200741 return True
delacruzramo01b15d32019-07-02 14:37:47 +0200742
743 def get_user_list(self, filter_q=None):
744 """
745 Get user list.
746
tierno5ec768a2020-03-31 09:46:44 +0000747 :param filter_q: dictionary to filter user list by:
748 name (username is also admitted). If a user id is equal to the filter name, it is also provided
749 other
delacruzramo01b15d32019-07-02 14:37:47 +0200750 :return: returns a list of users.
751 """
752 filt = filter_q or {}
tierno5ec768a2020-03-31 09:46:44 +0000753 if "name" in filt: # backward compatibility
754 filt["username"] = filt.pop("name")
755 if filt.get("username") and is_valid_uuid(filt["username"]):
756 # username cannot be a uuid. If this is the case, change from username to _id
757 filt["_id"] = filt.pop("username")
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530758 users = self.db.get_list(self.users_collection, filt)
tierno1546f2a2019-08-20 15:38:11 +0000759 project_id_name = {}
760 role_id_name = {}
delacruzramo01b15d32019-07-02 14:37:47 +0200761 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000762 prms = user.get("project_role_mappings")
763 projects = user.get("projects")
764 if prms:
765 projects = []
766 # add project_name and role_name. Generate projects for backward compatibility
delacruzramo01b15d32019-07-02 14:37:47 +0200767 for prm in prms:
tierno1546f2a2019-08-20 15:38:11 +0000768 project_id = prm["project"]
769 if project_id not in project_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100770 pr = self.db.get_one(
771 self.projects_collection,
772 {BaseTopic.id_field("projects", project_id): project_id},
773 fail_on_empty=False,
774 )
tierno1546f2a2019-08-20 15:38:11 +0000775 project_id_name[project_id] = pr["name"] if pr else None
776 prm["project_name"] = project_id_name[project_id]
777 if prm["project_name"] not in projects:
778 projects.append(prm["project_name"])
779
780 role_id = prm["role"]
781 if role_id not in role_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100782 role = self.db.get_one(
783 self.roles_collection,
784 {BaseTopic.id_field("roles", role_id): role_id},
785 fail_on_empty=False,
786 )
tierno1546f2a2019-08-20 15:38:11 +0000787 role_id_name[role_id] = role["name"] if role else None
788 prm["role_name"] = role_id_name[role_id]
789 user["projects"] = projects # for backward compatibility
790 elif projects:
791 # user created with an old version. Create a project_role mapping with role project_admin
792 user["project_role_mappings"] = []
garciadeblas4568a372021-03-24 09:19:48 +0100793 role = self.db.get_one(
794 self.roles_collection,
795 {BaseTopic.id_field("roles", "project_admin"): "project_admin"},
796 )
tierno1546f2a2019-08-20 15:38:11 +0000797 for p_id_name in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100798 pr = self.db.get_one(
799 self.projects_collection,
800 {BaseTopic.id_field("projects", p_id_name): p_id_name},
801 )
802 prm = {
803 "project": pr["_id"],
804 "project_name": pr["name"],
805 "role_name": "project_admin",
806 "role": role["_id"],
807 }
tierno1546f2a2019-08-20 15:38:11 +0000808 user["project_role_mappings"].append(prm)
809 else:
810 user["projects"] = []
811 user["project_role_mappings"] = []
812
delacruzramo01b15d32019-07-02 14:37:47 +0200813 return users
814
815 def get_project_list(self, filter_q={}):
816 """
817 Get role list.
818
819 :return: returns the list of projects.
820 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530821 return self.db.get_list(self.projects_collection, filter_q)
delacruzramo01b15d32019-07-02 14:37:47 +0200822
823 def create_project(self, project_info):
824 """
825 Create a project.
826
827 :param project: full project info.
828 :return: the internal id of the created project
829 :raises AuthconnOperationException: if project creation failed.
830 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530831 pid = self.db.create(self.projects_collection, project_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200832 return pid
833
834 def delete_project(self, project_id):
835 """
836 Delete a project.
837
838 :param project_id: project identifier.
839 :raises AuthconnOperationException: if project deletion failed.
840 """
delacruzramoad682a52019-12-10 16:26:34 +0100841 idf = BaseTopic.id_field("projects", project_id)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530842 r = self.db.del_one(self.projects_collection, {idf: project_id})
delacruzramoad682a52019-12-10 16:26:34 +0100843 idf = "project_id" if idf == "_id" else "project_name"
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530844 self.db.del_list(self.tokens_collection, {idf: project_id})
delacruzramo01b15d32019-07-02 14:37:47 +0200845 return r
846
847 def update_project(self, project_id, project_info):
848 """
849 Change the name of a project
850
851 :param project_id: project to be changed
852 :param project_info: full project info
853 :return: None
854 :raises AuthconnOperationException: if project update failed.
855 """
garciadeblas4568a372021-03-24 09:19:48 +0100856 self.db.set_one(
857 self.projects_collection,
858 {BaseTopic.id_field("projects", project_id): project_id},
859 project_info,
860 )