blob: ac157b8e66bdb225e5a5693068aa1791a2a37c16 [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
escaleira9e393822025-04-03 18:53:24 +010044from osm_nbi.password_utils import (
45 hash_password,
46 verify_password,
47 verify_password_sha256,
48)
jeganbe1a3df2024-06-04 12:05:19 +000049from osm_nbi.validation import is_valid_uuid, email_schema
delacruzramoad682a52019-12-10 16:26:34 +010050from time import time, sleep
delacruzramoceb8baf2019-06-21 14:25:38 +020051from http import HTTPStatus
52from uuid import uuid4
delacruzramoceb8baf2019-06-21 14:25:38 +020053from copy import deepcopy
54from random import choice as random_choice
jeganbe1a3df2024-06-04 12:05:19 +000055import smtplib
56from email.message import EmailMessage
57from email.mime.text import MIMEText
58from email.mime.multipart import MIMEMultipart
delacruzramoceb8baf2019-06-21 14:25:38 +020059
60
61class AuthconnInternal(Authconn):
garciadeblas4568a372021-03-24 09:19:48 +010062 token_time_window = 2 # seconds
63 token_delay = 1 # seconds to wait upon second request within time window
delacruzramoceb8baf2019-06-21 14:25:38 +020064
K Sai Kiran7ddb0732020-10-30 11:14:44 +053065 users_collection = "users"
66 roles_collection = "roles"
67 projects_collection = "projects"
68 tokens_collection = "tokens"
69
tierno9e87a7f2020-03-23 09:24:10 +000070 def __init__(self, config, db, role_permissions):
71 Authconn.__init__(self, config, db, role_permissions)
delacruzramoceb8baf2019-06-21 14:25:38 +020072 self.logger = logging.getLogger("nbi.authenticator.internal")
73
delacruzramoceb8baf2019-06-21 14:25:38 +020074 self.db = db
delacruzramoad682a52019-12-10 16:26:34 +010075 # self.msg = msg
76 # self.token_cache = token_cache
delacruzramoceb8baf2019-06-21 14:25:38 +020077
78 # To be Confirmed
delacruzramoceb8baf2019-06-21 14:25:38 +020079 self.sess = None
elumalai7802ff82023-04-24 20:38:32 +053080 self.cef_logger = cef_event_builder(config)
delacruzramoceb8baf2019-06-21 14:25:38 +020081
delacruzramoceb8baf2019-06-21 14:25:38 +020082 def validate_token(self, token):
83 """
84 Check if the token is valid.
85
86 :param token: token to validate
87 :return: dictionary with information associated with the token:
88 "_id": token id
89 "project_id": project id
90 "project_name": project name
91 "user_id": user id
92 "username": user name
93 "roles": list with dict containing {name, id}
94 "expires": expiration date
95 If the token is not valid an exception is raised.
96 """
97
98 try:
99 if not token:
garciadeblas4568a372021-03-24 09:19:48 +0100100 raise AuthException(
101 "Needed a token or Authorization HTTP header",
102 http_code=HTTPStatus.UNAUTHORIZED,
103 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200104
delacruzramoceb8baf2019-06-21 14:25:38 +0200105 now = time()
delacruzramoceb8baf2019-06-21 14:25:38 +0200106
107 # get from database if not in cache
delacruzramoad682a52019-12-10 16:26:34 +0100108 # if not token_info:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530109 token_info = self.db.get_one(self.tokens_collection, {"_id": token})
delacruzramoad682a52019-12-10 16:26:34 +0100110 if token_info["expires"] < now:
garciadeblas4568a372021-03-24 09:19:48 +0100111 raise AuthException(
112 "Expired Token or Authorization HTTP header",
113 http_code=HTTPStatus.UNAUTHORIZED,
114 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200115
tierno701018c2019-06-25 11:13:14 +0000116 return token_info
delacruzramoceb8baf2019-06-21 14:25:38 +0200117
118 except DbException as e:
119 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100120 raise AuthException(
121 "Invalid Token or Authorization HTTP header",
122 http_code=HTTPStatus.UNAUTHORIZED,
123 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200124 else:
125 raise
126 except AuthException:
tiernoe1eb3b22019-08-26 15:59:24 +0000127 raise
delacruzramoceb8baf2019-06-21 14:25:38 +0200128 except Exception:
garciadeblas4568a372021-03-24 09:19:48 +0100129 self.logger.exception(
130 "Error during token validation using internal backend"
131 )
132 raise AuthException(
133 "Error during token validation using internal backend",
134 http_code=HTTPStatus.UNAUTHORIZED,
135 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200136
137 def revoke_token(self, token):
138 """
139 Invalidate a token.
140
141 :param token: token to be revoked
142 """
143 try:
delacruzramoad682a52019-12-10 16:26:34 +0100144 # self.token_cache.pop(token, None)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530145 self.db.del_one(self.tokens_collection, {"_id": token})
delacruzramoceb8baf2019-06-21 14:25:38 +0200146 return True
147 except DbException as e:
148 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100149 raise AuthException(
150 "Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND
151 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200152 else:
153 # raise
delacruzramoad682a52019-12-10 16:26:34 +0100154 exmsg = "Error during token revocation using internal backend"
155 self.logger.exception(exmsg)
156 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoceb8baf2019-06-21 14:25:38 +0200157
jeganbe1a3df2024-06-04 12:05:19 +0000158 def validate_user(self, user, password, otp=None):
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530159 """
160 Validate username and password via appropriate backend.
161 :param user: username of the user.
162 :param password: password to be validated.
163 """
garciadeblas4568a372021-03-24 09:19:48 +0100164 user_rows = self.db.get_list(
165 self.users_collection, {BaseTopic.id_field("users", user): user}
166 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200167 now = time()
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530168 user_content = None
garciadeblas6d83f8f2023-06-19 22:34:49 +0200169 if user:
170 user_rows = self.db.get_list(
171 self.users_collection,
172 {BaseTopic.id_field(self.users_collection, user): user},
173 )
174 if user_rows:
175 user_content = user_rows[0]
176 # Updating user_status for every system_admin id role login
177 mapped_roles = user_content.get("project_role_mappings")
178 for role in mapped_roles:
179 role_id = role.get("role")
180 role_assigned = self.db.get_one(
181 self.roles_collection,
182 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
183 )
184
185 if role_assigned.get("permissions")["admin"]:
186 if role_assigned.get("permissions")["default"]:
187 if self.config.get("user_management"):
188 filt = {}
189 users = self.db.get_list(self.users_collection, filt)
190 for user_info in users:
191 if not user_info.get("username") == "admin":
192 if not user_info.get("_admin").get(
193 "account_expire_time"
194 ):
195 expire = now + 86400 * self.config.get(
196 "account_expire_days"
197 )
198 self.db.set_one(
199 self.users_collection,
200 {"_id": user_info["_id"]},
201 {"_admin.account_expire_time": expire},
202 )
203 else:
204 if now > user_info.get("_admin").get(
205 "account_expire_time"
206 ):
207 self.db.set_one(
208 self.users_collection,
209 {"_id": user_info["_id"]},
210 {"_admin.user_status": "expired"},
211 )
212 break
213
214 # To add "admin" user_status key while upgrading osm setup with feature enabled
215 if user_content.get("username") == "admin":
216 if self.config.get("user_management"):
217 self.db.set_one(
218 self.users_collection,
219 {"_id": user_content["_id"]},
220 {"_admin.user_status": "always-active"},
221 )
222
223 if not user_content.get("username") == "admin":
224 if self.config.get("user_management"):
225 if not user_content.get("_admin").get("account_expire_time"):
226 account_expire_time = now + 86400 * self.config.get(
227 "account_expire_days"
228 )
229 self.db.set_one(
230 self.users_collection,
231 {"_id": user_content["_id"]},
232 {"_admin.account_expire_time": account_expire_time},
233 )
234 else:
235 account_expire_time = user_content.get("_admin").get(
236 "account_expire_time"
237 )
238
239 if now > account_expire_time:
240 self.db.set_one(
241 self.users_collection,
242 {"_id": user_content["_id"]},
243 {"_admin.user_status": "expired"},
244 )
245 raise AuthException(
246 "Account expired", http_code=HTTPStatus.UNAUTHORIZED
247 )
248
249 if user_content.get("_admin").get("user_status") == "locked":
250 raise AuthException(
251 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
252 )
253 elif user_content.get("_admin").get("user_status") == "expired":
254 raise AuthException(
255 "Failed to login as the account is expired"
256 )
jeganbe1a3df2024-06-04 12:05:19 +0000257 if otp:
258 return user_content
escaleira9e393822025-04-03 18:53:24 +0100259 correct_pwd = False
260 if user_content.get("hashing_function") == "bcrypt":
261 correct_pwd = verify_password(
262 password=password, hashed_password_hex=user_content["password"]
263 )
264 else:
265 correct_pwd = verify_password_sha256(
266 password=password,
267 hashed_password_hex=user_content["password"],
268 salt=user_content["_admin"]["salt"],
269 )
270 if not correct_pwd:
garciadeblas6d83f8f2023-06-19 22:34:49 +0200271 count = 1
272 if user_content.get("_admin").get("retry_count") >= 0:
273 count += user_content.get("_admin").get("retry_count")
274 self.db.set_one(
275 self.users_collection,
276 {"_id": user_content["_id"]},
277 {"_admin.retry_count": count},
278 )
279 self.logger.debug(
280 "Failed Authentications count: {}".format(count)
281 )
282
283 if user_content.get("username") == "admin":
284 user_content = None
285 else:
286 if not self.config.get("user_management"):
287 user_content = None
288 else:
289 if (
290 user_content.get("_admin").get("retry_count")
291 >= self.config["max_pwd_attempt"] - 1
292 ):
293 self.db.set_one(
294 self.users_collection,
295 {"_id": user_content["_id"]},
296 {"_admin.user_status": "locked"},
297 )
298 raise AuthException(
299 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
300 )
301 else:
302 user_content = None
escaleira9e393822025-04-03 18:53:24 +0100303 elif correct_pwd and user_content.get("hashing_function") != "bcrypt":
304 # Update the database using a more secure hashing function to store the password
305 user_content["password"] = hash_password(
306 password=password,
307 rounds=self.config.get("password_rounds", 12),
308 )
309 user_content["hashing_function"] = "bcrypt"
310 user_content["_admin"]["password_history_sha256"] = user_content[
311 "_admin"
312 ]["password_history"]
313 user_content["_admin"]["password_history"] = [
314 user_content["password"]
315 ]
316 del user_content["_admin"]["salt"]
317
318 uid = user_content["_id"]
319 idf = BaseTopic.id_field("users", uid)
320 self.db.set_one(self.users_collection, {idf: uid}, user_content)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530321 return user_content
322
tierno6486f742020-02-13 16:30:14 +0000323 def authenticate(self, credentials, token_info=None):
delacruzramoceb8baf2019-06-21 14:25:38 +0200324 """
tierno701018c2019-06-25 11:13:14 +0000325 Authenticate a user using username/password or previous token_info plus project; its creates a new token
delacruzramoceb8baf2019-06-21 14:25:38 +0200326
tierno6486f742020-02-13 16:30:14 +0000327 :param credentials: dictionary that contains:
328 username: name, id or None
329 password: password or None
330 project_id: name, id, or None. If None first found project will be used to get an scope token
331 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000332 :param token_info: previous token_info to obtain authorization
delacruzramoceb8baf2019-06-21 14:25:38 +0200333 :return: the scoped token info or raises an exception. The token is a dictionary with:
334 _id: token string id,
335 username: username,
336 project_id: scoped_token project_id,
337 project_name: scoped_token project_name,
338 expires: epoch time when it expires,
339 """
340
341 now = time()
342 user_content = None
tierno6486f742020-02-13 16:30:14 +0000343 user = credentials.get("username")
344 password = credentials.get("password")
345 project = credentials.get("project_id")
jeganbe1a3df2024-06-04 12:05:19 +0000346 otp_validation = credentials.get("otp")
delacruzramoceb8baf2019-06-21 14:25:38 +0200347
delacruzramo01b15d32019-07-02 14:37:47 +0200348 # Try using username/password
jeganbe1a3df2024-06-04 12:05:19 +0000349 if otp_validation:
350 user_content = self.validate_user(user, password=None, otp=otp_validation)
351 elif user:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530352 user_content = self.validate_user(user, password)
delacruzramo01b15d32019-07-02 14:37:47 +0200353 if not user_content:
elumalai7802ff82023-04-24 20:38:32 +0530354 cef_event(
355 self.cef_logger,
356 {
357 "name": "User login",
358 "sourceUserName": user,
359 "message": "Invalid username/password Project={} Outcome=Failure".format(
360 project
361 ),
362 "severity": "3",
363 },
364 )
365 self.logger.exception("{}".format(self.cef_logger))
garciadeblas4568a372021-03-24 09:19:48 +0100366 raise AuthException(
367 "Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED
368 )
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530369 if not user_content.get("_admin", None):
garciadeblas4568a372021-03-24 09:19:48 +0100370 raise AuthException(
371 "No default project for this user.",
372 http_code=HTTPStatus.UNAUTHORIZED,
373 )
delacruzramo01b15d32019-07-02 14:37:47 +0200374 elif token_info:
garciadeblas4568a372021-03-24 09:19:48 +0100375 user_rows = self.db.get_list(
376 self.users_collection, {"username": token_info["username"]}
377 )
delacruzramo01b15d32019-07-02 14:37:47 +0200378 if user_rows:
379 user_content = user_rows[0]
delacruzramoceb8baf2019-06-21 14:25:38 +0200380 else:
delacruzramo01b15d32019-07-02 14:37:47 +0200381 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
382 else:
garciadeblas4568a372021-03-24 09:19:48 +0100383 raise AuthException(
384 "Provide credentials: username/password or Authorization Bearer token",
385 http_code=HTTPStatus.UNAUTHORIZED,
386 )
delacruzramoad682a52019-12-10 16:26:34 +0100387 # Delay upon second request within time window
garciadeblas4568a372021-03-24 09:19:48 +0100388 if (
389 now - user_content["_admin"].get("last_token_time", 0)
390 < self.token_time_window
391 ):
delacruzramoad682a52019-12-10 16:26:34 +0100392 sleep(self.token_delay)
393 # user_content["_admin"]["last_token_time"] = now
394 # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions
garciadeblas6d83f8f2023-06-19 22:34:49 +0200395 user_data = {
396 "_admin.last_token_time": now,
397 "_admin.retry_count": 0,
398 }
garciadeblas4568a372021-03-24 09:19:48 +0100399 self.db.set_one(
400 self.users_collection,
401 {"_id": user_content["_id"]},
garciadeblas6d83f8f2023-06-19 22:34:49 +0200402 user_data,
garciadeblas4568a372021-03-24 09:19:48 +0100403 )
delacruzramoad682a52019-12-10 16:26:34 +0100404
garciadeblas4568a372021-03-24 09:19:48 +0100405 token_id = "".join(
406 random_choice(
407 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
408 )
409 for _ in range(0, 32)
410 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200411
delacruzramo01b15d32019-07-02 14:37:47 +0200412 # projects = user_content.get("projects", [])
413 prm_list = user_content.get("project_role_mappings", [])
delacruzramoceb8baf2019-06-21 14:25:38 +0200414
delacruzramo01b15d32019-07-02 14:37:47 +0200415 if not project:
416 project = prm_list[0]["project"] if prm_list else None
417 if not project:
garciadeblas4568a372021-03-24 09:19:48 +0100418 raise AuthException(
419 "can't find a default project for this user",
420 http_code=HTTPStatus.UNAUTHORIZED,
421 )
tierno701018c2019-06-25 11:13:14 +0000422
delacruzramo01b15d32019-07-02 14:37:47 +0200423 projects = [prm["project"] for prm in prm_list]
delacruzramoceb8baf2019-06-21 14:25:38 +0200424
garciadeblas4568a372021-03-24 09:19:48 +0100425 proj = self.db.get_one(
426 self.projects_collection, {BaseTopic.id_field("projects", project): project}
427 )
delacruzramo01b15d32019-07-02 14:37:47 +0200428 project_name = proj["name"]
429 project_id = proj["_id"]
430 if project_name not in projects and project_id not in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100431 raise AuthException(
432 "project {} not allowed for this user".format(project),
433 http_code=HTTPStatus.UNAUTHORIZED,
434 )
tierno701018c2019-06-25 11:13:14 +0000435
delacruzramo01b15d32019-07-02 14:37:47 +0200436 # TODO remove admin, this vill be used by roles RBAC
437 if project_name == "admin":
438 token_admin = True
439 else:
440 token_admin = proj.get("admin", False)
delacruzramoceb8baf2019-06-21 14:25:38 +0200441
delacruzramo01b15d32019-07-02 14:37:47 +0200442 # add token roles
443 roles = []
444 roles_list = []
445 for prm in prm_list:
446 if prm["project"] in [project_id, project_name]:
garciadeblas4568a372021-03-24 09:19:48 +0100447 role = self.db.get_one(
448 self.roles_collection,
449 {BaseTopic.id_field("roles", prm["role"]): prm["role"]},
450 )
delacruzramo01b15d32019-07-02 14:37:47 +0200451 rid = role["_id"]
452 if rid not in roles:
453 rnm = role["name"]
454 roles.append(rid)
455 roles_list.append({"name": rnm, "id": rid})
456 if not roles_list:
garciadeblas4568a372021-03-24 09:19:48 +0100457 rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})[
458 "_id"
459 ]
delacruzramo01b15d32019-07-02 14:37:47 +0200460 roles_list = [{"name": "project_admin", "id": rid}]
delacruzramoceb8baf2019-06-21 14:25:38 +0200461
garciadeblas6d83f8f2023-06-19 22:34:49 +0200462 login_count = user_content.get("_admin").get("retry_count")
463 last_token_time = user_content.get("_admin").get("last_token_time")
464
465 admin_show = False
466 user_show = False
467 if self.config.get("user_management"):
468 for role in roles_list:
469 role_id = role.get("id")
470 permission = self.db.get_one(
471 self.roles_collection,
472 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
473 )
474 if permission.get("permissions")["admin"]:
475 if permission.get("permissions")["default"]:
476 admin_show = True
477 break
478 else:
479 user_show = True
garciadeblas4568a372021-03-24 09:19:48 +0100480 new_token = {
481 "issued_at": now,
482 "expires": now + 3600,
483 "_id": token_id,
484 "id": token_id,
485 "project_id": proj["_id"],
486 "project_name": proj["name"],
487 "username": user_content["username"],
488 "user_id": user_content["_id"],
489 "admin": token_admin,
490 "roles": roles_list,
garciadeblas6d83f8f2023-06-19 22:34:49 +0200491 "login_count": login_count,
492 "last_login": last_token_time,
493 "admin_show": admin_show,
494 "user_show": user_show,
garciadeblas4568a372021-03-24 09:19:48 +0100495 }
delacruzramoceb8baf2019-06-21 14:25:38 +0200496
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530497 self.db.create(self.tokens_collection, new_token)
delacruzramo01b15d32019-07-02 14:37:47 +0200498 return deepcopy(new_token)
499
500 def get_role_list(self, filter_q={}):
delacruzramoceb8baf2019-06-21 14:25:38 +0200501 """
502 Get role list.
503
504 :return: returns the list of roles.
505 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530506 return self.db.get_list(self.roles_collection, filter_q)
delacruzramoceb8baf2019-06-21 14:25:38 +0200507
delacruzramo01b15d32019-07-02 14:37:47 +0200508 def create_role(self, role_info):
delacruzramoceb8baf2019-06-21 14:25:38 +0200509 """
510 Create a role.
511
delacruzramo01b15d32019-07-02 14:37:47 +0200512 :param role_info: full role info.
513 :return: returns the role id.
delacruzramoceb8baf2019-06-21 14:25:38 +0200514 :raises AuthconnOperationException: if role creation failed.
515 """
delacruzramoceb8baf2019-06-21 14:25:38 +0200516 # TODO: Check that role name does not exist ?
delacruzramo01b15d32019-07-02 14:37:47 +0200517 rid = str(uuid4())
518 role_info["_id"] = rid
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530519 rid = self.db.create(self.roles_collection, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200520 return rid
delacruzramoceb8baf2019-06-21 14:25:38 +0200521
522 def delete_role(self, role_id):
523 """
524 Delete a role.
525
526 :param role_id: role identifier.
527 :raises AuthconnOperationException: if role deletion failed.
528 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530529 rc = self.db.del_one(self.roles_collection, {"_id": role_id})
530 self.db.del_list(self.tokens_collection, {"roles.id": role_id})
delacruzramoad682a52019-12-10 16:26:34 +0100531 return rc
delacruzramo01b15d32019-07-02 14:37:47 +0200532
533 def update_role(self, role_info):
534 """
535 Update a role.
536
537 :param role_info: full role info.
538 :return: returns the role name and id.
539 :raises AuthconnOperationException: if user creation failed.
540 """
541 rid = role_info["_id"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530542 self.db.set_one(self.roles_collection, {"_id": rid}, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200543 return {"_id": rid, "name": role_info["name"]}
544
545 def create_user(self, user_info):
546 """
547 Create a user.
548
549 :param user_info: full user info.
550 :return: returns the username and id of the user.
551 """
552 BaseTopic.format_on_new(user_info, make_public=False)
garciadeblas6d83f8f2023-06-19 22:34:49 +0200553 user_info["_admin"]["user_status"] = "active"
selvi.ja9a1fc82022-04-04 06:54:30 +0000554 present = time()
555 if not user_info["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200556 if self.config.get("user_management"):
557 user_info["_admin"]["modified"] = present
558 user_info["_admin"]["password_expire_time"] = present
559 account_expire_time = present + 86400 * self.config.get(
560 "account_expire_days"
561 )
562 user_info["_admin"]["account_expire_time"] = account_expire_time
563
564 user_info["_admin"]["retry_count"] = 0
565 user_info["_admin"]["last_token_time"] = present
delacruzramo01b15d32019-07-02 14:37:47 +0200566 if "password" in user_info:
escaleira9e393822025-04-03 18:53:24 +0100567 user_info["password"] = hash_password(
568 password=user_info["password"],
569 rounds=self.config.get("password_rounds", 12),
570 )
571 user_info["hashing_function"] = "bcrypt"
572 user_info["_admin"]["password_history"] = [user_info["password"]]
delacruzramo01b15d32019-07-02 14:37:47 +0200573 # "projects" are not stored any more
574 if "projects" in user_info:
575 del user_info["projects"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530576 self.db.create(self.users_collection, user_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200577 return {"username": user_info["username"], "_id": user_info["_id"]}
578
579 def update_user(self, user_info):
580 """
581 Change the user name and/or password.
582
583 :param user_info: user info modifications
584 """
585 uid = user_info["_id"]
selvi.ja9a1fc82022-04-04 06:54:30 +0000586 old_pwd = user_info.get("old_password")
garciadeblas6d83f8f2023-06-19 22:34:49 +0200587 unlock = user_info.get("unlock")
588 renew = user_info.get("renew")
589 permission_id = user_info.get("system_admin_id")
Adurti0c9b0102023-11-08 11:16:32 +0000590 now = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200591
garciadeblas4568a372021-03-24 09:19:48 +0100592 user_data = self.db.get_one(
593 self.users_collection, {BaseTopic.id_field("users", uid): uid}
594 )
selvi.ja9a1fc82022-04-04 06:54:30 +0000595 if old_pwd:
escaleira9e393822025-04-03 18:53:24 +0100596 correct_pwd = False
597 if user_data.get("hashing_function") == "bcrypt":
598 correct_pwd = verify_password(
599 password=old_pwd, hashed_password_hex=user_data["password"]
600 )
601 else:
602 correct_pwd = verify_password_sha256(
603 password=old_pwd,
604 hashed_password_hex=user_data["password"],
605 salt=user_data["salt"],
606 )
607 if not correct_pwd:
selvi.ja9a1fc82022-04-04 06:54:30 +0000608 raise AuthconnConflictException(
garciadeblasf2af4a12023-01-24 16:56:54 +0100609 "Incorrect password", http_code=HTTPStatus.CONFLICT
selvi.ja9a1fc82022-04-04 06:54:30 +0000610 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200611 # Unlocking the user
612 if unlock:
613 system_user = None
614 unlock_state = False
615 if not permission_id:
616 raise AuthconnConflictException(
617 "system_admin_id is the required field to unlock the user",
618 http_code=HTTPStatus.CONFLICT,
619 )
620 else:
621 system_user = self.db.get_one(
622 self.users_collection,
623 {
624 BaseTopic.id_field(
625 self.users_collection, permission_id
626 ): permission_id
627 },
628 )
629 mapped_roles = system_user.get("project_role_mappings")
630 for role in mapped_roles:
631 role_id = role.get("role")
632 role_assigned = self.db.get_one(
633 self.roles_collection,
634 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
635 )
636 if role_assigned.get("permissions")["admin"]:
637 if role_assigned.get("permissions")["default"]:
638 user_data["_admin"]["retry_count"] = 0
Adurti0c9b0102023-11-08 11:16:32 +0000639 if now > user_data["_admin"]["account_expire_time"]:
640 user_data["_admin"]["user_status"] = "expired"
641 else:
642 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200643 unlock_state = True
644 break
645 if not unlock_state:
646 raise AuthconnConflictException(
647 "User '{}' does not have the privilege to unlock the user".format(
648 permission_id
649 ),
650 http_code=HTTPStatus.CONFLICT,
651 )
652 # Renewing the user
653 if renew:
654 system_user = None
655 renew_state = False
656 if not permission_id:
657 raise AuthconnConflictException(
658 "system_admin_id is the required field to renew the user",
659 http_code=HTTPStatus.CONFLICT,
660 )
661 else:
662 system_user = self.db.get_one(
663 self.users_collection,
664 {
665 BaseTopic.id_field(
666 self.users_collection, permission_id
667 ): permission_id
668 },
669 )
670 mapped_roles = system_user.get("project_role_mappings")
671 for role in mapped_roles:
672 role_id = role.get("role")
673 role_assigned = self.db.get_one(
674 self.roles_collection,
675 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
676 )
677 if role_assigned.get("permissions")["admin"]:
678 if role_assigned.get("permissions")["default"]:
679 present = time()
680 account_expire = (
681 present + 86400 * self.config["account_expire_days"]
682 )
683 user_data["_admin"]["modified"] = present
684 user_data["_admin"]["account_expire_time"] = account_expire
Adurti0c9b0102023-11-08 11:16:32 +0000685 if (
686 user_data["_admin"]["retry_count"]
687 >= self.config["max_pwd_attempt"]
688 ):
689 user_data["_admin"]["user_status"] = "locked"
690 else:
691 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200692 renew_state = True
693 break
694 if not renew_state:
695 raise AuthconnConflictException(
696 "User '{}' does not have the privilege to renew the user".format(
697 permission_id
698 ),
699 http_code=HTTPStatus.CONFLICT,
700 )
delacruzramo01b15d32019-07-02 14:37:47 +0200701 BaseTopic.format_on_edit(user_data, user_info)
702 # User Name
703 usnm = user_info.get("username")
jeganbe1a3df2024-06-04 12:05:19 +0000704 email_id = user_info.get("email_id")
delacruzramo01b15d32019-07-02 14:37:47 +0200705 if usnm:
706 user_data["username"] = usnm
jeganbe1a3df2024-06-04 12:05:19 +0000707 if email_id:
708 user_data["email_id"] = email_id
delacruzramo01b15d32019-07-02 14:37:47 +0200709 # If password is given and is not already encripted
710 pswd = user_info.get("password")
garciadeblas4568a372021-03-24 09:19:48 +0100711 if pswd and (
712 len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd)
713 ): # TODO: Improve check?
elumalai7802ff82023-04-24 20:38:32 +0530714 cef_event(
715 self.cef_logger,
716 {
717 "name": "Change Password",
718 "sourceUserName": user_data["username"],
garciadeblasf53612b2024-07-12 14:44:37 +0200719 "message": "User {} changing Password for user {}, Outcome=Success".format(
720 user_info.get("session_user"), user_data["username"]
721 ),
elumalai7802ff82023-04-24 20:38:32 +0530722 "severity": "2",
723 },
724 )
725 self.logger.info("{}".format(self.cef_logger))
delacruzramo01b15d32019-07-02 14:37:47 +0200726 if "_admin" not in user_data:
727 user_data["_admin"] = {}
garciadeblas6d83f8f2023-06-19 22:34:49 +0200728 if user_data.get("_admin").get("password_history"):
729 old_pwds = user_data.get("_admin").get("password_history")
730 else:
escaleira9e393822025-04-03 18:53:24 +0100731 old_pwds = []
732 for v in old_pwds:
733 if verify_password(password=pswd, hashed_password_hex=v):
garciadeblas6d83f8f2023-06-19 22:34:49 +0200734 raise AuthconnConflictException(
735 "Password is used before", http_code=HTTPStatus.CONFLICT
736 )
escaleira9e393822025-04-03 18:53:24 +0100737
738 # Backwards compatibility for SHA256 hashed passwords
739 if user_data.get("_admin").get("password_history_sha256"):
740 old_pwds_sha256 = user_data.get("_admin").get("password_history_sha256")
741 else:
742 old_pwds_sha256 = {}
743 for k, v in old_pwds_sha256.items():
744 if verify_password_sha256(password=pswd, hashed_password_hex=v, salt=k):
745 raise AuthconnConflictException(
746 "Password is used before", http_code=HTTPStatus.CONFLICT
747 )
748
749 # Finally, hash the password to be updated
750 user_data["password"] = hash_password(
751 password=pswd, rounds=self.config.get("password_rounds", 12)
752 )
753 user_data["hashing_function"] = "bcrypt"
754
garciadeblas6d83f8f2023-06-19 22:34:49 +0200755 if len(old_pwds) >= 3:
756 old_pwds.pop(list(old_pwds.keys())[0])
escaleira9e393822025-04-03 18:53:24 +0100757 old_pwds.append([user_data["password"]])
garciadeblas6d83f8f2023-06-19 22:34:49 +0200758 user_data["_admin"]["password_history"] = old_pwds
selvi.ja9a1fc82022-04-04 06:54:30 +0000759 if not user_data["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200760 if self.config.get("user_management"):
selvi.ja9a1fc82022-04-04 06:54:30 +0000761 present = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200762 if self.config.get("pwd_expire_days"):
763 expire = present + 86400 * self.config.get("pwd_expire_days")
764 user_data["_admin"]["modified"] = present
765 user_data["_admin"]["password_expire_time"] = expire
delacruzramo01b15d32019-07-02 14:37:47 +0200766 # Project-Role Mappings
767 # TODO: Check that user_info NEVER includes "project_role_mappings"
768 if "project_role_mappings" not in user_data:
769 user_data["project_role_mappings"] = []
770 for prm in user_info.get("add_project_role_mappings", []):
771 user_data["project_role_mappings"].append(prm)
772 for prm in user_info.get("remove_project_role_mappings", []):
773 for pidf in ["project", "project_name"]:
774 for ridf in ["role", "role_name"]:
775 try:
garciadeblas4568a372021-03-24 09:19:48 +0100776 user_data["project_role_mappings"].remove(
777 {"role": prm[ridf], "project": prm[pidf]}
778 )
delacruzramo01b15d32019-07-02 14:37:47 +0200779 except KeyError:
780 pass
781 except ValueError:
782 pass
delacruzramo3d6881c2019-12-04 13:42:26 +0100783 idf = BaseTopic.id_field("users", uid)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530784 self.db.set_one(self.users_collection, {idf: uid}, user_data)
delacruzramo3d6881c2019-12-04 13:42:26 +0100785 if user_info.get("remove_project_role_mappings"):
delacruzramoad682a52019-12-10 16:26:34 +0100786 idf = "user_id" if idf == "_id" else idf
Adurti9f2ac992024-03-25 10:58:29 +0000787 if not user_data.get("project_role_mappings") or user_info.get(
788 "remove_session_project"
789 ):
790 self.db.del_list(self.tokens_collection, {idf: uid})
delacruzramo01b15d32019-07-02 14:37:47 +0200791
792 def delete_user(self, user_id):
793 """
794 Delete user.
795
796 :param user_id: user identifier.
797 :raises AuthconnOperationException: if user deletion failed.
798 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530799 self.db.del_one(self.users_collection, {"_id": user_id})
800 self.db.del_list(self.tokens_collection, {"user_id": user_id})
delacruzramoceb8baf2019-06-21 14:25:38 +0200801 return True
delacruzramo01b15d32019-07-02 14:37:47 +0200802
803 def get_user_list(self, filter_q=None):
804 """
805 Get user list.
806
tierno5ec768a2020-03-31 09:46:44 +0000807 :param filter_q: dictionary to filter user list by:
808 name (username is also admitted). If a user id is equal to the filter name, it is also provided
809 other
delacruzramo01b15d32019-07-02 14:37:47 +0200810 :return: returns a list of users.
811 """
812 filt = filter_q or {}
tierno5ec768a2020-03-31 09:46:44 +0000813 if "name" in filt: # backward compatibility
814 filt["username"] = filt.pop("name")
815 if filt.get("username") and is_valid_uuid(filt["username"]):
816 # username cannot be a uuid. If this is the case, change from username to _id
817 filt["_id"] = filt.pop("username")
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530818 users = self.db.get_list(self.users_collection, filt)
tierno1546f2a2019-08-20 15:38:11 +0000819 project_id_name = {}
820 role_id_name = {}
delacruzramo01b15d32019-07-02 14:37:47 +0200821 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000822 prms = user.get("project_role_mappings")
823 projects = user.get("projects")
824 if prms:
825 projects = []
826 # add project_name and role_name. Generate projects for backward compatibility
delacruzramo01b15d32019-07-02 14:37:47 +0200827 for prm in prms:
tierno1546f2a2019-08-20 15:38:11 +0000828 project_id = prm["project"]
829 if project_id not in project_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100830 pr = self.db.get_one(
831 self.projects_collection,
832 {BaseTopic.id_field("projects", project_id): project_id},
833 fail_on_empty=False,
834 )
tierno1546f2a2019-08-20 15:38:11 +0000835 project_id_name[project_id] = pr["name"] if pr else None
836 prm["project_name"] = project_id_name[project_id]
837 if prm["project_name"] not in projects:
838 projects.append(prm["project_name"])
839
840 role_id = prm["role"]
841 if role_id not in role_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100842 role = self.db.get_one(
843 self.roles_collection,
844 {BaseTopic.id_field("roles", role_id): role_id},
845 fail_on_empty=False,
846 )
tierno1546f2a2019-08-20 15:38:11 +0000847 role_id_name[role_id] = role["name"] if role else None
848 prm["role_name"] = role_id_name[role_id]
849 user["projects"] = projects # for backward compatibility
850 elif projects:
851 # user created with an old version. Create a project_role mapping with role project_admin
852 user["project_role_mappings"] = []
garciadeblas4568a372021-03-24 09:19:48 +0100853 role = self.db.get_one(
854 self.roles_collection,
855 {BaseTopic.id_field("roles", "project_admin"): "project_admin"},
856 )
tierno1546f2a2019-08-20 15:38:11 +0000857 for p_id_name in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100858 pr = self.db.get_one(
859 self.projects_collection,
860 {BaseTopic.id_field("projects", p_id_name): p_id_name},
861 )
862 prm = {
863 "project": pr["_id"],
864 "project_name": pr["name"],
865 "role_name": "project_admin",
866 "role": role["_id"],
867 }
tierno1546f2a2019-08-20 15:38:11 +0000868 user["project_role_mappings"].append(prm)
869 else:
870 user["projects"] = []
871 user["project_role_mappings"] = []
872
delacruzramo01b15d32019-07-02 14:37:47 +0200873 return users
874
875 def get_project_list(self, filter_q={}):
876 """
877 Get role list.
878
879 :return: returns the list of projects.
880 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530881 return self.db.get_list(self.projects_collection, filter_q)
delacruzramo01b15d32019-07-02 14:37:47 +0200882
883 def create_project(self, project_info):
884 """
885 Create a project.
886
887 :param project: full project info.
888 :return: the internal id of the created project
889 :raises AuthconnOperationException: if project creation failed.
890 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530891 pid = self.db.create(self.projects_collection, project_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200892 return pid
893
894 def delete_project(self, project_id):
895 """
896 Delete a project.
897
898 :param project_id: project identifier.
899 :raises AuthconnOperationException: if project deletion failed.
900 """
delacruzramoad682a52019-12-10 16:26:34 +0100901 idf = BaseTopic.id_field("projects", project_id)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530902 r = self.db.del_one(self.projects_collection, {idf: project_id})
delacruzramoad682a52019-12-10 16:26:34 +0100903 idf = "project_id" if idf == "_id" else "project_name"
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530904 self.db.del_list(self.tokens_collection, {idf: project_id})
delacruzramo01b15d32019-07-02 14:37:47 +0200905 return r
906
907 def update_project(self, project_id, project_info):
908 """
909 Change the name of a project
910
911 :param project_id: project to be changed
912 :param project_info: full project info
913 :return: None
914 :raises AuthconnOperationException: if project update failed.
915 """
garciadeblas4568a372021-03-24 09:19:48 +0100916 self.db.set_one(
917 self.projects_collection,
918 {BaseTopic.id_field("projects", project_id): project_id},
919 project_info,
920 )
jeganbe1a3df2024-06-04 12:05:19 +0000921
922 def generate_otp(self):
923 otp = "".join(random_choice("0123456789") for i in range(0, 4))
924 return otp
925
926 def send_email(self, indata):
927 user = indata.get("username")
928 user_rows = self.db.get_list(self.users_collection, {"username": user})
929 sender_password = None
930 otp_expiry_time = self.config.get("otp_expiry_time", 300)
931 if not re.match(email_schema["pattern"], indata.get("email_id")):
932 raise AuthException(
933 "Invalid email-id",
934 http_code=HTTPStatus.BAD_REQUEST,
935 )
936 if self.config.get("sender_email"):
937 sender_email = self.config["sender_email"]
938 else:
939 raise AuthException(
940 "sender_email not found",
941 http_code=HTTPStatus.NOT_FOUND,
942 )
943 if self.config.get("smtp_server"):
944 smtp_server = self.config["smtp_server"]
945 else:
946 raise AuthException(
947 "smtp server not found",
948 http_code=HTTPStatus.NOT_FOUND,
949 )
950 if self.config.get("smtp_port"):
951 smtp_port = self.config["smtp_port"]
952 else:
953 raise AuthException(
954 "smtp port not found",
955 http_code=HTTPStatus.NOT_FOUND,
956 )
957 sender_password = self.config.get("sender_password") or None
958 if user_rows:
959 user_data = user_rows[0]
960 user_status = user_data["_admin"]["user_status"]
961 if not user_data.get("project_role_mappings", None):
962 raise AuthException(
963 "can't find a default project for this user",
964 http_code=HTTPStatus.UNAUTHORIZED,
965 )
966 if user_status != "active" and user_status != "always-active":
967 raise AuthException(
968 f"User account is {user_status}.Please contact the system administrator.",
969 http_code=HTTPStatus.UNAUTHORIZED,
970 )
971 if user_data.get("email_id"):
972 if user_data["email_id"] == indata.get("email_id"):
973 otp = self.generate_otp()
escaleira9e393822025-04-03 18:53:24 +0100974 encode_otp = hash_password(
975 password=otp, rounds=self.config.get("password_rounds", 12)
976 )
jeganbe1a3df2024-06-04 12:05:19 +0000977 otp_field = {encode_otp: time() + otp_expiry_time, "retries": 0}
978 user_data["OTP"] = otp_field
979 uid = user_data["_id"]
980 idf = BaseTopic.id_field("users", uid)
981 reciever_email = user_data["email_id"]
982 email_template_path = self.config.get("email_template")
983 with open(email_template_path, "r") as et:
984 email_template = et.read()
985 msg = EmailMessage()
986 msg = MIMEMultipart("alternative")
987 html_content = email_template.format(
988 username=user_data["username"],
989 otp=otp,
990 validity=otp_expiry_time // 60,
991 )
992 html = MIMEText(html_content, "html")
993 msg["Subject"] = "OSM password reset request"
994 msg.attach(html)
995 with smtplib.SMTP(smtp_server, smtp_port) as smtp:
996 smtp.starttls()
997 if sender_password:
998 smtp.login(sender_email, sender_password)
999 smtp.sendmail(sender_email, reciever_email, msg.as_string())
1000 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1001 return {"email": "sent"}
1002 else:
1003 raise AuthException(
1004 "No email id is registered for this user.Please contact the system administrator.",
1005 http_code=HTTPStatus.NOT_FOUND,
1006 )
1007 else:
1008 raise AuthException(
1009 "user not found",
1010 http_code=HTTPStatus.NOT_FOUND,
1011 )
1012
1013 def validate_otp(self, indata):
1014 otp = indata.get("otp")
1015 user = indata.get("username")
1016 user_rows = self.db.get_list(self.users_collection, {"username": user})
1017 user_data = user_rows[0]
1018 uid = user_data["_id"]
1019 idf = BaseTopic.id_field("users", uid)
1020 retry_count = self.config.get("retry_count", 3)
1021 if user_data:
jeganbe1a3df2024-06-04 12:05:19 +00001022 if not user_data.get("OTP"):
1023 otp_field = {"retries": 1}
1024 user_data["OTP"] = otp_field
1025 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1026 return {"retries": user_data["OTP"]["retries"]}
1027 for key, value in user_data["OTP"].items():
1028 curr_time = time()
escaleira9e393822025-04-03 18:53:24 +01001029 if (
1030 verify_password(password=otp, hashed_password_hex=key)
1031 and curr_time < value
1032 ):
jeganbe1a3df2024-06-04 12:05:19 +00001033 user_data["OTP"] = {}
1034 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1035 return {"valid": "True", "password_change": "True"}
1036 else:
1037 user_data["OTP"]["retries"] += 1
1038 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1039 if user_data["OTP"].get("retries") >= retry_count:
1040 raise AuthException(
1041 "Invalid OTP. Maximum retries exceeded",
1042 http_code=HTTPStatus.TOO_MANY_REQUESTS,
1043 )
1044 return {"retry_count": user_data["OTP"]["retries"]}