blob: fe86f16185f9b9c26a7d86854fb9d53c7ee5f32b [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
garciadeblasd8c3d8e2025-06-25 17:18:05 +020035import secrets
tierno5ec768a2020-03-31 09:46:44 +000036
garciadeblasf2af4a12023-01-24 16:56:54 +010037from osm_nbi.authconn import (
38 Authconn,
39 AuthException,
40 AuthconnConflictException,
41) # , AuthconnOperationException
delacruzramoceb8baf2019-06-21 14:25:38 +020042from osm_common.dbbase import DbException
tierno23acf402019-08-28 13:36:34 +000043from osm_nbi.base_topic import BaseTopic
elumalai7802ff82023-04-24 20:38:32 +053044from osm_nbi.utils import cef_event, cef_event_builder
escaleira9e393822025-04-03 18:53:24 +010045from osm_nbi.password_utils import (
46 hash_password,
47 verify_password,
48 verify_password_sha256,
49)
jeganbe1a3df2024-06-04 12:05:19 +000050from osm_nbi.validation import is_valid_uuid, email_schema
delacruzramoad682a52019-12-10 16:26:34 +010051from time import time, sleep
delacruzramoceb8baf2019-06-21 14:25:38 +020052from http import HTTPStatus
53from uuid import uuid4
delacruzramoceb8baf2019-06-21 14:25:38 +020054from copy import deepcopy
55from random import choice as random_choice
jeganbe1a3df2024-06-04 12:05:19 +000056import smtplib
57from email.message import EmailMessage
58from email.mime.text import MIMEText
59from email.mime.multipart import MIMEMultipart
delacruzramoceb8baf2019-06-21 14:25:38 +020060
61
62class AuthconnInternal(Authconn):
garciadeblas4568a372021-03-24 09:19:48 +010063 token_time_window = 2 # seconds
64 token_delay = 1 # seconds to wait upon second request within time window
delacruzramoceb8baf2019-06-21 14:25:38 +020065
K Sai Kiran7ddb0732020-10-30 11:14:44 +053066 users_collection = "users"
67 roles_collection = "roles"
68 projects_collection = "projects"
69 tokens_collection = "tokens"
70
tierno9e87a7f2020-03-23 09:24:10 +000071 def __init__(self, config, db, role_permissions):
72 Authconn.__init__(self, config, db, role_permissions)
delacruzramoceb8baf2019-06-21 14:25:38 +020073 self.logger = logging.getLogger("nbi.authenticator.internal")
74
delacruzramoceb8baf2019-06-21 14:25:38 +020075 self.db = db
delacruzramoad682a52019-12-10 16:26:34 +010076 # self.msg = msg
77 # self.token_cache = token_cache
delacruzramoceb8baf2019-06-21 14:25:38 +020078
79 # To be Confirmed
delacruzramoceb8baf2019-06-21 14:25:38 +020080 self.sess = None
elumalai7802ff82023-04-24 20:38:32 +053081 self.cef_logger = cef_event_builder(config)
delacruzramoceb8baf2019-06-21 14:25:38 +020082
delacruzramoceb8baf2019-06-21 14:25:38 +020083 def validate_token(self, token):
84 """
85 Check if the token is valid.
86
87 :param token: token to validate
88 :return: dictionary with information associated with the token:
89 "_id": token id
90 "project_id": project id
91 "project_name": project name
92 "user_id": user id
93 "username": user name
94 "roles": list with dict containing {name, id}
95 "expires": expiration date
96 If the token is not valid an exception is raised.
97 """
98
99 try:
100 if not token:
garciadeblas4568a372021-03-24 09:19:48 +0100101 raise AuthException(
102 "Needed a token or Authorization HTTP header",
103 http_code=HTTPStatus.UNAUTHORIZED,
104 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200105
delacruzramoceb8baf2019-06-21 14:25:38 +0200106 now = time()
delacruzramoceb8baf2019-06-21 14:25:38 +0200107
108 # get from database if not in cache
delacruzramoad682a52019-12-10 16:26:34 +0100109 # if not token_info:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530110 token_info = self.db.get_one(self.tokens_collection, {"_id": token})
delacruzramoad682a52019-12-10 16:26:34 +0100111 if token_info["expires"] < now:
garciadeblas4568a372021-03-24 09:19:48 +0100112 raise AuthException(
113 "Expired Token or Authorization HTTP header",
114 http_code=HTTPStatus.UNAUTHORIZED,
115 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200116
tierno701018c2019-06-25 11:13:14 +0000117 return token_info
delacruzramoceb8baf2019-06-21 14:25:38 +0200118
119 except DbException as e:
120 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100121 raise AuthException(
122 "Invalid Token or Authorization HTTP header",
123 http_code=HTTPStatus.UNAUTHORIZED,
124 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200125 else:
126 raise
127 except AuthException:
tiernoe1eb3b22019-08-26 15:59:24 +0000128 raise
delacruzramoceb8baf2019-06-21 14:25:38 +0200129 except Exception:
garciadeblas4568a372021-03-24 09:19:48 +0100130 self.logger.exception(
131 "Error during token validation using internal backend"
132 )
133 raise AuthException(
134 "Error during token validation using internal backend",
135 http_code=HTTPStatus.UNAUTHORIZED,
136 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200137
138 def revoke_token(self, token):
139 """
140 Invalidate a token.
141
142 :param token: token to be revoked
143 """
144 try:
delacruzramoad682a52019-12-10 16:26:34 +0100145 # self.token_cache.pop(token, None)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530146 self.db.del_one(self.tokens_collection, {"_id": token})
delacruzramoceb8baf2019-06-21 14:25:38 +0200147 return True
148 except DbException as e:
149 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100150 raise AuthException(
151 "Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND
152 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200153 else:
154 # raise
delacruzramoad682a52019-12-10 16:26:34 +0100155 exmsg = "Error during token revocation using internal backend"
156 self.logger.exception(exmsg)
157 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoceb8baf2019-06-21 14:25:38 +0200158
jeganbe1a3df2024-06-04 12:05:19 +0000159 def validate_user(self, user, password, otp=None):
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530160 """
161 Validate username and password via appropriate backend.
162 :param user: username of the user.
163 :param password: password to be validated.
164 """
garciadeblas4568a372021-03-24 09:19:48 +0100165 user_rows = self.db.get_list(
166 self.users_collection, {BaseTopic.id_field("users", user): user}
167 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200168 now = time()
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530169 user_content = None
garciadeblas6d83f8f2023-06-19 22:34:49 +0200170 if user:
171 user_rows = self.db.get_list(
172 self.users_collection,
173 {BaseTopic.id_field(self.users_collection, user): user},
174 )
175 if user_rows:
176 user_content = user_rows[0]
177 # Updating user_status for every system_admin id role login
178 mapped_roles = user_content.get("project_role_mappings")
179 for role in mapped_roles:
180 role_id = role.get("role")
181 role_assigned = self.db.get_one(
182 self.roles_collection,
183 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
184 )
185
186 if role_assigned.get("permissions")["admin"]:
187 if role_assigned.get("permissions")["default"]:
188 if self.config.get("user_management"):
189 filt = {}
190 users = self.db.get_list(self.users_collection, filt)
191 for user_info in users:
192 if not user_info.get("username") == "admin":
193 if not user_info.get("_admin").get(
194 "account_expire_time"
195 ):
196 expire = now + 86400 * self.config.get(
197 "account_expire_days"
198 )
199 self.db.set_one(
200 self.users_collection,
201 {"_id": user_info["_id"]},
202 {"_admin.account_expire_time": expire},
203 )
204 else:
205 if now > user_info.get("_admin").get(
206 "account_expire_time"
207 ):
208 self.db.set_one(
209 self.users_collection,
210 {"_id": user_info["_id"]},
211 {"_admin.user_status": "expired"},
212 )
213 break
214
215 # To add "admin" user_status key while upgrading osm setup with feature enabled
216 if user_content.get("username") == "admin":
217 if self.config.get("user_management"):
218 self.db.set_one(
219 self.users_collection,
220 {"_id": user_content["_id"]},
221 {"_admin.user_status": "always-active"},
222 )
223
224 if not user_content.get("username") == "admin":
225 if self.config.get("user_management"):
226 if not user_content.get("_admin").get("account_expire_time"):
227 account_expire_time = now + 86400 * self.config.get(
228 "account_expire_days"
229 )
230 self.db.set_one(
231 self.users_collection,
232 {"_id": user_content["_id"]},
233 {"_admin.account_expire_time": account_expire_time},
234 )
235 else:
236 account_expire_time = user_content.get("_admin").get(
237 "account_expire_time"
238 )
239
240 if now > account_expire_time:
241 self.db.set_one(
242 self.users_collection,
243 {"_id": user_content["_id"]},
244 {"_admin.user_status": "expired"},
245 )
246 raise AuthException(
247 "Account expired", http_code=HTTPStatus.UNAUTHORIZED
248 )
249
250 if user_content.get("_admin").get("user_status") == "locked":
251 raise AuthException(
252 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
253 )
254 elif user_content.get("_admin").get("user_status") == "expired":
255 raise AuthException(
256 "Failed to login as the account is expired"
257 )
jeganbe1a3df2024-06-04 12:05:19 +0000258 if otp:
259 return user_content
escaleira9e393822025-04-03 18:53:24 +0100260 correct_pwd = False
261 if user_content.get("hashing_function") == "bcrypt":
262 correct_pwd = verify_password(
263 password=password, hashed_password_hex=user_content["password"]
264 )
265 else:
266 correct_pwd = verify_password_sha256(
267 password=password,
268 hashed_password_hex=user_content["password"],
269 salt=user_content["_admin"]["salt"],
270 )
271 if not correct_pwd:
garciadeblas6d83f8f2023-06-19 22:34:49 +0200272 count = 1
273 if user_content.get("_admin").get("retry_count") >= 0:
274 count += user_content.get("_admin").get("retry_count")
275 self.db.set_one(
276 self.users_collection,
277 {"_id": user_content["_id"]},
278 {"_admin.retry_count": count},
279 )
280 self.logger.debug(
281 "Failed Authentications count: {}".format(count)
282 )
283
284 if user_content.get("username") == "admin":
285 user_content = None
286 else:
287 if not self.config.get("user_management"):
288 user_content = None
289 else:
290 if (
291 user_content.get("_admin").get("retry_count")
292 >= self.config["max_pwd_attempt"] - 1
293 ):
294 self.db.set_one(
295 self.users_collection,
296 {"_id": user_content["_id"]},
297 {"_admin.user_status": "locked"},
298 )
299 raise AuthException(
300 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
301 )
302 else:
303 user_content = None
escaleira9e393822025-04-03 18:53:24 +0100304 elif correct_pwd and user_content.get("hashing_function") != "bcrypt":
305 # Update the database using a more secure hashing function to store the password
306 user_content["password"] = hash_password(
307 password=password,
308 rounds=self.config.get("password_rounds", 12),
309 )
310 user_content["hashing_function"] = "bcrypt"
311 user_content["_admin"]["password_history_sha256"] = user_content[
312 "_admin"
313 ]["password_history"]
314 user_content["_admin"]["password_history"] = [
315 user_content["password"]
316 ]
317 del user_content["_admin"]["salt"]
318
319 uid = user_content["_id"]
320 idf = BaseTopic.id_field("users", uid)
321 self.db.set_one(self.users_collection, {idf: uid}, user_content)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530322 return user_content
323
tierno6486f742020-02-13 16:30:14 +0000324 def authenticate(self, credentials, token_info=None):
delacruzramoceb8baf2019-06-21 14:25:38 +0200325 """
tierno701018c2019-06-25 11:13:14 +0000326 Authenticate a user using username/password or previous token_info plus project; its creates a new token
delacruzramoceb8baf2019-06-21 14:25:38 +0200327
tierno6486f742020-02-13 16:30:14 +0000328 :param credentials: dictionary that contains:
329 username: name, id or None
330 password: password or None
331 project_id: name, id, or None. If None first found project will be used to get an scope token
332 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000333 :param token_info: previous token_info to obtain authorization
delacruzramoceb8baf2019-06-21 14:25:38 +0200334 :return: the scoped token info or raises an exception. The token is a dictionary with:
335 _id: token string id,
336 username: username,
337 project_id: scoped_token project_id,
338 project_name: scoped_token project_name,
339 expires: epoch time when it expires,
340 """
341
342 now = time()
343 user_content = None
tierno6486f742020-02-13 16:30:14 +0000344 user = credentials.get("username")
345 password = credentials.get("password")
346 project = credentials.get("project_id")
jeganbe1a3df2024-06-04 12:05:19 +0000347 otp_validation = credentials.get("otp")
delacruzramoceb8baf2019-06-21 14:25:38 +0200348
delacruzramo01b15d32019-07-02 14:37:47 +0200349 # Try using username/password
jeganbe1a3df2024-06-04 12:05:19 +0000350 if otp_validation:
351 user_content = self.validate_user(user, password=None, otp=otp_validation)
352 elif user:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530353 user_content = self.validate_user(user, password)
delacruzramo01b15d32019-07-02 14:37:47 +0200354 if not user_content:
elumalai7802ff82023-04-24 20:38:32 +0530355 cef_event(
356 self.cef_logger,
357 {
358 "name": "User login",
359 "sourceUserName": user,
360 "message": "Invalid username/password Project={} Outcome=Failure".format(
361 project
362 ),
363 "severity": "3",
364 },
365 )
366 self.logger.exception("{}".format(self.cef_logger))
garciadeblas4568a372021-03-24 09:19:48 +0100367 raise AuthException(
368 "Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED
369 )
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530370 if not user_content.get("_admin", None):
garciadeblas4568a372021-03-24 09:19:48 +0100371 raise AuthException(
372 "No default project for this user.",
373 http_code=HTTPStatus.UNAUTHORIZED,
374 )
delacruzramo01b15d32019-07-02 14:37:47 +0200375 elif token_info:
garciadeblas4568a372021-03-24 09:19:48 +0100376 user_rows = self.db.get_list(
377 self.users_collection, {"username": token_info["username"]}
378 )
delacruzramo01b15d32019-07-02 14:37:47 +0200379 if user_rows:
380 user_content = user_rows[0]
delacruzramoceb8baf2019-06-21 14:25:38 +0200381 else:
delacruzramo01b15d32019-07-02 14:37:47 +0200382 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
383 else:
garciadeblas4568a372021-03-24 09:19:48 +0100384 raise AuthException(
385 "Provide credentials: username/password or Authorization Bearer token",
386 http_code=HTTPStatus.UNAUTHORIZED,
387 )
delacruzramoad682a52019-12-10 16:26:34 +0100388 # Delay upon second request within time window
garciadeblas4568a372021-03-24 09:19:48 +0100389 if (
390 now - user_content["_admin"].get("last_token_time", 0)
391 < self.token_time_window
392 ):
delacruzramoad682a52019-12-10 16:26:34 +0100393 sleep(self.token_delay)
394 # user_content["_admin"]["last_token_time"] = now
395 # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions
garciadeblas6d83f8f2023-06-19 22:34:49 +0200396 user_data = {
397 "_admin.last_token_time": now,
398 "_admin.retry_count": 0,
399 }
garciadeblas4568a372021-03-24 09:19:48 +0100400 self.db.set_one(
401 self.users_collection,
402 {"_id": user_content["_id"]},
garciadeblas6d83f8f2023-06-19 22:34:49 +0200403 user_data,
garciadeblas4568a372021-03-24 09:19:48 +0100404 )
delacruzramoad682a52019-12-10 16:26:34 +0100405
garciadeblasd8c3d8e2025-06-25 17:18:05 +0200406 # Generate a secure random 32 byte array base64 encoded for use in URLs
407 token_id = secrets.token_urlsafe(32)
delacruzramoceb8baf2019-06-21 14:25:38 +0200408
delacruzramo01b15d32019-07-02 14:37:47 +0200409 # projects = user_content.get("projects", [])
410 prm_list = user_content.get("project_role_mappings", [])
delacruzramoceb8baf2019-06-21 14:25:38 +0200411
delacruzramo01b15d32019-07-02 14:37:47 +0200412 if not project:
413 project = prm_list[0]["project"] if prm_list else None
414 if not project:
garciadeblas4568a372021-03-24 09:19:48 +0100415 raise AuthException(
416 "can't find a default project for this user",
417 http_code=HTTPStatus.UNAUTHORIZED,
418 )
tierno701018c2019-06-25 11:13:14 +0000419
delacruzramo01b15d32019-07-02 14:37:47 +0200420 projects = [prm["project"] for prm in prm_list]
delacruzramoceb8baf2019-06-21 14:25:38 +0200421
garciadeblas4568a372021-03-24 09:19:48 +0100422 proj = self.db.get_one(
423 self.projects_collection, {BaseTopic.id_field("projects", project): project}
424 )
delacruzramo01b15d32019-07-02 14:37:47 +0200425 project_name = proj["name"]
426 project_id = proj["_id"]
427 if project_name not in projects and project_id not in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100428 raise AuthException(
429 "project {} not allowed for this user".format(project),
430 http_code=HTTPStatus.UNAUTHORIZED,
431 )
tierno701018c2019-06-25 11:13:14 +0000432
delacruzramo01b15d32019-07-02 14:37:47 +0200433 # TODO remove admin, this vill be used by roles RBAC
434 if project_name == "admin":
435 token_admin = True
436 else:
437 token_admin = proj.get("admin", False)
delacruzramoceb8baf2019-06-21 14:25:38 +0200438
delacruzramo01b15d32019-07-02 14:37:47 +0200439 # add token roles
440 roles = []
441 roles_list = []
442 for prm in prm_list:
443 if prm["project"] in [project_id, project_name]:
garciadeblas4568a372021-03-24 09:19:48 +0100444 role = self.db.get_one(
445 self.roles_collection,
446 {BaseTopic.id_field("roles", prm["role"]): prm["role"]},
447 )
delacruzramo01b15d32019-07-02 14:37:47 +0200448 rid = role["_id"]
449 if rid not in roles:
450 rnm = role["name"]
451 roles.append(rid)
452 roles_list.append({"name": rnm, "id": rid})
453 if not roles_list:
garciadeblas4568a372021-03-24 09:19:48 +0100454 rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})[
455 "_id"
456 ]
delacruzramo01b15d32019-07-02 14:37:47 +0200457 roles_list = [{"name": "project_admin", "id": rid}]
delacruzramoceb8baf2019-06-21 14:25:38 +0200458
garciadeblas6d83f8f2023-06-19 22:34:49 +0200459 login_count = user_content.get("_admin").get("retry_count")
460 last_token_time = user_content.get("_admin").get("last_token_time")
461
462 admin_show = False
463 user_show = False
464 if self.config.get("user_management"):
465 for role in roles_list:
466 role_id = role.get("id")
467 permission = self.db.get_one(
468 self.roles_collection,
469 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
470 )
471 if permission.get("permissions")["admin"]:
472 if permission.get("permissions")["default"]:
473 admin_show = True
474 break
475 else:
476 user_show = True
garciadeblas4568a372021-03-24 09:19:48 +0100477 new_token = {
478 "issued_at": now,
479 "expires": now + 3600,
480 "_id": token_id,
481 "id": token_id,
482 "project_id": proj["_id"],
483 "project_name": proj["name"],
484 "username": user_content["username"],
485 "user_id": user_content["_id"],
486 "admin": token_admin,
487 "roles": roles_list,
garciadeblas6d83f8f2023-06-19 22:34:49 +0200488 "login_count": login_count,
489 "last_login": last_token_time,
490 "admin_show": admin_show,
491 "user_show": user_show,
garciadeblas4568a372021-03-24 09:19:48 +0100492 }
delacruzramoceb8baf2019-06-21 14:25:38 +0200493
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530494 self.db.create(self.tokens_collection, new_token)
delacruzramo01b15d32019-07-02 14:37:47 +0200495 return deepcopy(new_token)
496
497 def get_role_list(self, filter_q={}):
delacruzramoceb8baf2019-06-21 14:25:38 +0200498 """
499 Get role list.
500
501 :return: returns the list of roles.
502 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530503 return self.db.get_list(self.roles_collection, filter_q)
delacruzramoceb8baf2019-06-21 14:25:38 +0200504
delacruzramo01b15d32019-07-02 14:37:47 +0200505 def create_role(self, role_info):
delacruzramoceb8baf2019-06-21 14:25:38 +0200506 """
507 Create a role.
508
delacruzramo01b15d32019-07-02 14:37:47 +0200509 :param role_info: full role info.
510 :return: returns the role id.
delacruzramoceb8baf2019-06-21 14:25:38 +0200511 :raises AuthconnOperationException: if role creation failed.
512 """
delacruzramoceb8baf2019-06-21 14:25:38 +0200513 # TODO: Check that role name does not exist ?
delacruzramo01b15d32019-07-02 14:37:47 +0200514 rid = str(uuid4())
515 role_info["_id"] = rid
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530516 rid = self.db.create(self.roles_collection, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200517 return rid
delacruzramoceb8baf2019-06-21 14:25:38 +0200518
519 def delete_role(self, role_id):
520 """
521 Delete a role.
522
523 :param role_id: role identifier.
524 :raises AuthconnOperationException: if role deletion failed.
525 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530526 rc = self.db.del_one(self.roles_collection, {"_id": role_id})
527 self.db.del_list(self.tokens_collection, {"roles.id": role_id})
delacruzramoad682a52019-12-10 16:26:34 +0100528 return rc
delacruzramo01b15d32019-07-02 14:37:47 +0200529
530 def update_role(self, role_info):
531 """
532 Update a role.
533
534 :param role_info: full role info.
535 :return: returns the role name and id.
536 :raises AuthconnOperationException: if user creation failed.
537 """
538 rid = role_info["_id"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530539 self.db.set_one(self.roles_collection, {"_id": rid}, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200540 return {"_id": rid, "name": role_info["name"]}
541
542 def create_user(self, user_info):
543 """
544 Create a user.
545
546 :param user_info: full user info.
547 :return: returns the username and id of the user.
548 """
549 BaseTopic.format_on_new(user_info, make_public=False)
garciadeblas6d83f8f2023-06-19 22:34:49 +0200550 user_info["_admin"]["user_status"] = "active"
selvi.ja9a1fc82022-04-04 06:54:30 +0000551 present = time()
552 if not user_info["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200553 if self.config.get("user_management"):
554 user_info["_admin"]["modified"] = present
555 user_info["_admin"]["password_expire_time"] = present
556 account_expire_time = present + 86400 * self.config.get(
557 "account_expire_days"
558 )
559 user_info["_admin"]["account_expire_time"] = account_expire_time
560
561 user_info["_admin"]["retry_count"] = 0
562 user_info["_admin"]["last_token_time"] = present
delacruzramo01b15d32019-07-02 14:37:47 +0200563 if "password" in user_info:
escaleira9e393822025-04-03 18:53:24 +0100564 user_info["password"] = hash_password(
565 password=user_info["password"],
566 rounds=self.config.get("password_rounds", 12),
567 )
568 user_info["hashing_function"] = "bcrypt"
569 user_info["_admin"]["password_history"] = [user_info["password"]]
delacruzramo01b15d32019-07-02 14:37:47 +0200570 # "projects" are not stored any more
571 if "projects" in user_info:
572 del user_info["projects"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530573 self.db.create(self.users_collection, user_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200574 return {"username": user_info["username"], "_id": user_info["_id"]}
575
576 def update_user(self, user_info):
577 """
578 Change the user name and/or password.
579
580 :param user_info: user info modifications
581 """
582 uid = user_info["_id"]
selvi.ja9a1fc82022-04-04 06:54:30 +0000583 old_pwd = user_info.get("old_password")
garciadeblas6d83f8f2023-06-19 22:34:49 +0200584 unlock = user_info.get("unlock")
585 renew = user_info.get("renew")
586 permission_id = user_info.get("system_admin_id")
Adurti0c9b0102023-11-08 11:16:32 +0000587 now = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200588
garciadeblas4568a372021-03-24 09:19:48 +0100589 user_data = self.db.get_one(
590 self.users_collection, {BaseTopic.id_field("users", uid): uid}
591 )
selvi.ja9a1fc82022-04-04 06:54:30 +0000592 if old_pwd:
escaleira9e393822025-04-03 18:53:24 +0100593 correct_pwd = False
594 if user_data.get("hashing_function") == "bcrypt":
595 correct_pwd = verify_password(
596 password=old_pwd, hashed_password_hex=user_data["password"]
597 )
598 else:
599 correct_pwd = verify_password_sha256(
600 password=old_pwd,
601 hashed_password_hex=user_data["password"],
602 salt=user_data["salt"],
603 )
604 if not correct_pwd:
selvi.ja9a1fc82022-04-04 06:54:30 +0000605 raise AuthconnConflictException(
garciadeblasf2af4a12023-01-24 16:56:54 +0100606 "Incorrect password", http_code=HTTPStatus.CONFLICT
selvi.ja9a1fc82022-04-04 06:54:30 +0000607 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200608 # Unlocking the user
609 if unlock:
610 system_user = None
611 unlock_state = False
612 if not permission_id:
613 raise AuthconnConflictException(
614 "system_admin_id is the required field to unlock 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 user_data["_admin"]["retry_count"] = 0
Adurti0c9b0102023-11-08 11:16:32 +0000636 if now > user_data["_admin"]["account_expire_time"]:
637 user_data["_admin"]["user_status"] = "expired"
638 else:
639 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200640 unlock_state = True
641 break
642 if not unlock_state:
643 raise AuthconnConflictException(
644 "User '{}' does not have the privilege to unlock the user".format(
645 permission_id
646 ),
647 http_code=HTTPStatus.CONFLICT,
648 )
649 # Renewing the user
650 if renew:
651 system_user = None
652 renew_state = False
653 if not permission_id:
654 raise AuthconnConflictException(
655 "system_admin_id is the required field to renew the user",
656 http_code=HTTPStatus.CONFLICT,
657 )
658 else:
659 system_user = self.db.get_one(
660 self.users_collection,
661 {
662 BaseTopic.id_field(
663 self.users_collection, permission_id
664 ): permission_id
665 },
666 )
667 mapped_roles = system_user.get("project_role_mappings")
668 for role in mapped_roles:
669 role_id = role.get("role")
670 role_assigned = self.db.get_one(
671 self.roles_collection,
672 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
673 )
674 if role_assigned.get("permissions")["admin"]:
675 if role_assigned.get("permissions")["default"]:
676 present = time()
677 account_expire = (
678 present + 86400 * self.config["account_expire_days"]
679 )
680 user_data["_admin"]["modified"] = present
681 user_data["_admin"]["account_expire_time"] = account_expire
Adurti0c9b0102023-11-08 11:16:32 +0000682 if (
683 user_data["_admin"]["retry_count"]
684 >= self.config["max_pwd_attempt"]
685 ):
686 user_data["_admin"]["user_status"] = "locked"
687 else:
688 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200689 renew_state = True
690 break
691 if not renew_state:
692 raise AuthconnConflictException(
693 "User '{}' does not have the privilege to renew the user".format(
694 permission_id
695 ),
696 http_code=HTTPStatus.CONFLICT,
697 )
delacruzramo01b15d32019-07-02 14:37:47 +0200698 BaseTopic.format_on_edit(user_data, user_info)
699 # User Name
700 usnm = user_info.get("username")
jeganbe1a3df2024-06-04 12:05:19 +0000701 email_id = user_info.get("email_id")
delacruzramo01b15d32019-07-02 14:37:47 +0200702 if usnm:
703 user_data["username"] = usnm
jeganbe1a3df2024-06-04 12:05:19 +0000704 if email_id:
705 user_data["email_id"] = email_id
delacruzramo01b15d32019-07-02 14:37:47 +0200706 # If password is given and is not already encripted
707 pswd = user_info.get("password")
garciadeblas4568a372021-03-24 09:19:48 +0100708 if pswd and (
709 len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd)
710 ): # TODO: Improve check?
elumalai7802ff82023-04-24 20:38:32 +0530711 cef_event(
712 self.cef_logger,
713 {
714 "name": "Change Password",
715 "sourceUserName": user_data["username"],
garciadeblasf53612b2024-07-12 14:44:37 +0200716 "message": "User {} changing Password for user {}, Outcome=Success".format(
717 user_info.get("session_user"), user_data["username"]
718 ),
elumalai7802ff82023-04-24 20:38:32 +0530719 "severity": "2",
720 },
721 )
722 self.logger.info("{}".format(self.cef_logger))
delacruzramo01b15d32019-07-02 14:37:47 +0200723 if "_admin" not in user_data:
724 user_data["_admin"] = {}
garciadeblas6d83f8f2023-06-19 22:34:49 +0200725 if user_data.get("_admin").get("password_history"):
726 old_pwds = user_data.get("_admin").get("password_history")
727 else:
escaleira9e393822025-04-03 18:53:24 +0100728 old_pwds = []
729 for v in old_pwds:
730 if verify_password(password=pswd, hashed_password_hex=v):
garciadeblas6d83f8f2023-06-19 22:34:49 +0200731 raise AuthconnConflictException(
732 "Password is used before", http_code=HTTPStatus.CONFLICT
733 )
escaleira9e393822025-04-03 18:53:24 +0100734
735 # Backwards compatibility for SHA256 hashed passwords
736 if user_data.get("_admin").get("password_history_sha256"):
737 old_pwds_sha256 = user_data.get("_admin").get("password_history_sha256")
738 else:
739 old_pwds_sha256 = {}
740 for k, v in old_pwds_sha256.items():
741 if verify_password_sha256(password=pswd, hashed_password_hex=v, salt=k):
742 raise AuthconnConflictException(
743 "Password is used before", http_code=HTTPStatus.CONFLICT
744 )
745
746 # Finally, hash the password to be updated
747 user_data["password"] = hash_password(
748 password=pswd, rounds=self.config.get("password_rounds", 12)
749 )
750 user_data["hashing_function"] = "bcrypt"
751
garciadeblas6d83f8f2023-06-19 22:34:49 +0200752 if len(old_pwds) >= 3:
753 old_pwds.pop(list(old_pwds.keys())[0])
escaleira9e393822025-04-03 18:53:24 +0100754 old_pwds.append([user_data["password"]])
garciadeblas6d83f8f2023-06-19 22:34:49 +0200755 user_data["_admin"]["password_history"] = old_pwds
selvi.ja9a1fc82022-04-04 06:54:30 +0000756 if not user_data["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200757 if self.config.get("user_management"):
selvi.ja9a1fc82022-04-04 06:54:30 +0000758 present = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200759 if self.config.get("pwd_expire_days"):
760 expire = present + 86400 * self.config.get("pwd_expire_days")
761 user_data["_admin"]["modified"] = present
762 user_data["_admin"]["password_expire_time"] = expire
delacruzramo01b15d32019-07-02 14:37:47 +0200763 # Project-Role Mappings
764 # TODO: Check that user_info NEVER includes "project_role_mappings"
765 if "project_role_mappings" not in user_data:
766 user_data["project_role_mappings"] = []
767 for prm in user_info.get("add_project_role_mappings", []):
768 user_data["project_role_mappings"].append(prm)
769 for prm in user_info.get("remove_project_role_mappings", []):
770 for pidf in ["project", "project_name"]:
771 for ridf in ["role", "role_name"]:
772 try:
garciadeblas4568a372021-03-24 09:19:48 +0100773 user_data["project_role_mappings"].remove(
774 {"role": prm[ridf], "project": prm[pidf]}
775 )
delacruzramo01b15d32019-07-02 14:37:47 +0200776 except KeyError:
777 pass
778 except ValueError:
779 pass
delacruzramo3d6881c2019-12-04 13:42:26 +0100780 idf = BaseTopic.id_field("users", uid)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530781 self.db.set_one(self.users_collection, {idf: uid}, user_data)
delacruzramo3d6881c2019-12-04 13:42:26 +0100782 if user_info.get("remove_project_role_mappings"):
delacruzramoad682a52019-12-10 16:26:34 +0100783 idf = "user_id" if idf == "_id" else idf
Adurti9f2ac992024-03-25 10:58:29 +0000784 if not user_data.get("project_role_mappings") or user_info.get(
785 "remove_session_project"
786 ):
787 self.db.del_list(self.tokens_collection, {idf: uid})
delacruzramo01b15d32019-07-02 14:37:47 +0200788
789 def delete_user(self, user_id):
790 """
791 Delete user.
792
793 :param user_id: user identifier.
794 :raises AuthconnOperationException: if user deletion failed.
795 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530796 self.db.del_one(self.users_collection, {"_id": user_id})
797 self.db.del_list(self.tokens_collection, {"user_id": user_id})
delacruzramoceb8baf2019-06-21 14:25:38 +0200798 return True
delacruzramo01b15d32019-07-02 14:37:47 +0200799
800 def get_user_list(self, filter_q=None):
801 """
802 Get user list.
803
tierno5ec768a2020-03-31 09:46:44 +0000804 :param filter_q: dictionary to filter user list by:
805 name (username is also admitted). If a user id is equal to the filter name, it is also provided
806 other
delacruzramo01b15d32019-07-02 14:37:47 +0200807 :return: returns a list of users.
808 """
809 filt = filter_q or {}
tierno5ec768a2020-03-31 09:46:44 +0000810 if "name" in filt: # backward compatibility
811 filt["username"] = filt.pop("name")
812 if filt.get("username") and is_valid_uuid(filt["username"]):
813 # username cannot be a uuid. If this is the case, change from username to _id
814 filt["_id"] = filt.pop("username")
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530815 users = self.db.get_list(self.users_collection, filt)
tierno1546f2a2019-08-20 15:38:11 +0000816 project_id_name = {}
817 role_id_name = {}
delacruzramo01b15d32019-07-02 14:37:47 +0200818 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000819 prms = user.get("project_role_mappings")
820 projects = user.get("projects")
821 if prms:
822 projects = []
823 # add project_name and role_name. Generate projects for backward compatibility
delacruzramo01b15d32019-07-02 14:37:47 +0200824 for prm in prms:
tierno1546f2a2019-08-20 15:38:11 +0000825 project_id = prm["project"]
826 if project_id not in project_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100827 pr = self.db.get_one(
828 self.projects_collection,
829 {BaseTopic.id_field("projects", project_id): project_id},
830 fail_on_empty=False,
831 )
tierno1546f2a2019-08-20 15:38:11 +0000832 project_id_name[project_id] = pr["name"] if pr else None
833 prm["project_name"] = project_id_name[project_id]
834 if prm["project_name"] not in projects:
835 projects.append(prm["project_name"])
836
837 role_id = prm["role"]
838 if role_id not in role_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100839 role = self.db.get_one(
840 self.roles_collection,
841 {BaseTopic.id_field("roles", role_id): role_id},
842 fail_on_empty=False,
843 )
tierno1546f2a2019-08-20 15:38:11 +0000844 role_id_name[role_id] = role["name"] if role else None
845 prm["role_name"] = role_id_name[role_id]
846 user["projects"] = projects # for backward compatibility
847 elif projects:
848 # user created with an old version. Create a project_role mapping with role project_admin
849 user["project_role_mappings"] = []
garciadeblas4568a372021-03-24 09:19:48 +0100850 role = self.db.get_one(
851 self.roles_collection,
852 {BaseTopic.id_field("roles", "project_admin"): "project_admin"},
853 )
tierno1546f2a2019-08-20 15:38:11 +0000854 for p_id_name in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100855 pr = self.db.get_one(
856 self.projects_collection,
857 {BaseTopic.id_field("projects", p_id_name): p_id_name},
858 )
859 prm = {
860 "project": pr["_id"],
861 "project_name": pr["name"],
862 "role_name": "project_admin",
863 "role": role["_id"],
864 }
tierno1546f2a2019-08-20 15:38:11 +0000865 user["project_role_mappings"].append(prm)
866 else:
867 user["projects"] = []
868 user["project_role_mappings"] = []
869
delacruzramo01b15d32019-07-02 14:37:47 +0200870 return users
871
872 def get_project_list(self, filter_q={}):
873 """
874 Get role list.
875
876 :return: returns the list of projects.
877 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530878 return self.db.get_list(self.projects_collection, filter_q)
delacruzramo01b15d32019-07-02 14:37:47 +0200879
880 def create_project(self, project_info):
881 """
882 Create a project.
883
884 :param project: full project info.
885 :return: the internal id of the created project
886 :raises AuthconnOperationException: if project creation failed.
887 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530888 pid = self.db.create(self.projects_collection, project_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200889 return pid
890
891 def delete_project(self, project_id):
892 """
893 Delete a project.
894
895 :param project_id: project identifier.
896 :raises AuthconnOperationException: if project deletion failed.
897 """
delacruzramoad682a52019-12-10 16:26:34 +0100898 idf = BaseTopic.id_field("projects", project_id)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530899 r = self.db.del_one(self.projects_collection, {idf: project_id})
delacruzramoad682a52019-12-10 16:26:34 +0100900 idf = "project_id" if idf == "_id" else "project_name"
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530901 self.db.del_list(self.tokens_collection, {idf: project_id})
delacruzramo01b15d32019-07-02 14:37:47 +0200902 return r
903
904 def update_project(self, project_id, project_info):
905 """
906 Change the name of a project
907
908 :param project_id: project to be changed
909 :param project_info: full project info
910 :return: None
911 :raises AuthconnOperationException: if project update failed.
912 """
garciadeblas4568a372021-03-24 09:19:48 +0100913 self.db.set_one(
914 self.projects_collection,
915 {BaseTopic.id_field("projects", project_id): project_id},
916 project_info,
917 )
jeganbe1a3df2024-06-04 12:05:19 +0000918
919 def generate_otp(self):
920 otp = "".join(random_choice("0123456789") for i in range(0, 4))
921 return otp
922
923 def send_email(self, indata):
924 user = indata.get("username")
925 user_rows = self.db.get_list(self.users_collection, {"username": user})
926 sender_password = None
927 otp_expiry_time = self.config.get("otp_expiry_time", 300)
928 if not re.match(email_schema["pattern"], indata.get("email_id")):
929 raise AuthException(
930 "Invalid email-id",
931 http_code=HTTPStatus.BAD_REQUEST,
932 )
933 if self.config.get("sender_email"):
934 sender_email = self.config["sender_email"]
935 else:
936 raise AuthException(
937 "sender_email not found",
938 http_code=HTTPStatus.NOT_FOUND,
939 )
940 if self.config.get("smtp_server"):
941 smtp_server = self.config["smtp_server"]
942 else:
943 raise AuthException(
944 "smtp server not found",
945 http_code=HTTPStatus.NOT_FOUND,
946 )
947 if self.config.get("smtp_port"):
948 smtp_port = self.config["smtp_port"]
949 else:
950 raise AuthException(
951 "smtp port not found",
952 http_code=HTTPStatus.NOT_FOUND,
953 )
954 sender_password = self.config.get("sender_password") or None
955 if user_rows:
956 user_data = user_rows[0]
957 user_status = user_data["_admin"]["user_status"]
958 if not user_data.get("project_role_mappings", None):
959 raise AuthException(
960 "can't find a default project for this user",
961 http_code=HTTPStatus.UNAUTHORIZED,
962 )
963 if user_status != "active" and user_status != "always-active":
964 raise AuthException(
965 f"User account is {user_status}.Please contact the system administrator.",
966 http_code=HTTPStatus.UNAUTHORIZED,
967 )
968 if user_data.get("email_id"):
969 if user_data["email_id"] == indata.get("email_id"):
970 otp = self.generate_otp()
escaleira9e393822025-04-03 18:53:24 +0100971 encode_otp = hash_password(
972 password=otp, rounds=self.config.get("password_rounds", 12)
973 )
jeganbe1a3df2024-06-04 12:05:19 +0000974 otp_field = {encode_otp: time() + otp_expiry_time, "retries": 0}
975 user_data["OTP"] = otp_field
976 uid = user_data["_id"]
977 idf = BaseTopic.id_field("users", uid)
978 reciever_email = user_data["email_id"]
979 email_template_path = self.config.get("email_template")
980 with open(email_template_path, "r") as et:
981 email_template = et.read()
982 msg = EmailMessage()
983 msg = MIMEMultipart("alternative")
984 html_content = email_template.format(
985 username=user_data["username"],
986 otp=otp,
987 validity=otp_expiry_time // 60,
988 )
989 html = MIMEText(html_content, "html")
990 msg["Subject"] = "OSM password reset request"
991 msg.attach(html)
992 with smtplib.SMTP(smtp_server, smtp_port) as smtp:
993 smtp.starttls()
994 if sender_password:
995 smtp.login(sender_email, sender_password)
996 smtp.sendmail(sender_email, reciever_email, msg.as_string())
997 self.db.set_one(self.users_collection, {idf: uid}, user_data)
998 return {"email": "sent"}
999 else:
1000 raise AuthException(
1001 "No email id is registered for this user.Please contact the system administrator.",
1002 http_code=HTTPStatus.NOT_FOUND,
1003 )
1004 else:
1005 raise AuthException(
1006 "user not found",
1007 http_code=HTTPStatus.NOT_FOUND,
1008 )
1009
1010 def validate_otp(self, indata):
1011 otp = indata.get("otp")
1012 user = indata.get("username")
1013 user_rows = self.db.get_list(self.users_collection, {"username": user})
1014 user_data = user_rows[0]
1015 uid = user_data["_id"]
1016 idf = BaseTopic.id_field("users", uid)
1017 retry_count = self.config.get("retry_count", 3)
1018 if user_data:
jeganbe1a3df2024-06-04 12:05:19 +00001019 if not user_data.get("OTP"):
1020 otp_field = {"retries": 1}
1021 user_data["OTP"] = otp_field
1022 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1023 return {"retries": user_data["OTP"]["retries"]}
1024 for key, value in user_data["OTP"].items():
1025 curr_time = time()
escaleira9e393822025-04-03 18:53:24 +01001026 if (
1027 verify_password(password=otp, hashed_password_hex=key)
1028 and curr_time < value
1029 ):
jeganbe1a3df2024-06-04 12:05:19 +00001030 user_data["OTP"] = {}
1031 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1032 return {"valid": "True", "password_change": "True"}
1033 else:
1034 user_data["OTP"]["retries"] += 1
1035 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1036 if user_data["OTP"].get("retries") >= retry_count:
1037 raise AuthException(
1038 "Invalid OTP. Maximum retries exceeded",
1039 http_code=HTTPStatus.TOO_MANY_REQUESTS,
1040 )
1041 return {"retry_count": user_data["OTP"]["retries"]}