blob: 9daa3fd60411c9bdba1c18f15f9f26a88437d1d6 [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
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
jeganbe1a3df2024-06-04 12:05:19 +000054import smtplib
55from email.message import EmailMessage
56from email.mime.text import MIMEText
57from email.mime.multipart import MIMEMultipart
delacruzramoceb8baf2019-06-21 14:25:38 +020058
59
60class AuthconnInternal(Authconn):
garciadeblas4568a372021-03-24 09:19:48 +010061 token_time_window = 2 # seconds
62 token_delay = 1 # seconds to wait upon second request within time window
delacruzramoceb8baf2019-06-21 14:25:38 +020063
K Sai Kiran7ddb0732020-10-30 11:14:44 +053064 users_collection = "users"
65 roles_collection = "roles"
66 projects_collection = "projects"
67 tokens_collection = "tokens"
68
tierno9e87a7f2020-03-23 09:24:10 +000069 def __init__(self, config, db, role_permissions):
70 Authconn.__init__(self, config, db, role_permissions)
delacruzramoceb8baf2019-06-21 14:25:38 +020071 self.logger = logging.getLogger("nbi.authenticator.internal")
72
delacruzramoceb8baf2019-06-21 14:25:38 +020073 self.db = db
delacruzramoad682a52019-12-10 16:26:34 +010074 # self.msg = msg
75 # self.token_cache = token_cache
delacruzramoceb8baf2019-06-21 14:25:38 +020076
77 # To be Confirmed
delacruzramoceb8baf2019-06-21 14:25:38 +020078 self.sess = None
elumalai7802ff82023-04-24 20:38:32 +053079 self.cef_logger = cef_event_builder(config)
delacruzramoceb8baf2019-06-21 14:25:38 +020080
delacruzramoceb8baf2019-06-21 14:25:38 +020081 def validate_token(self, token):
82 """
83 Check if the token is valid.
84
85 :param token: token to validate
86 :return: dictionary with information associated with the token:
87 "_id": token id
88 "project_id": project id
89 "project_name": project name
90 "user_id": user id
91 "username": user name
92 "roles": list with dict containing {name, id}
93 "expires": expiration date
94 If the token is not valid an exception is raised.
95 """
96
97 try:
98 if not token:
garciadeblas4568a372021-03-24 09:19:48 +010099 raise AuthException(
100 "Needed a token or Authorization HTTP header",
101 http_code=HTTPStatus.UNAUTHORIZED,
102 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200103
delacruzramoceb8baf2019-06-21 14:25:38 +0200104 now = time()
delacruzramoceb8baf2019-06-21 14:25:38 +0200105
106 # get from database if not in cache
delacruzramoad682a52019-12-10 16:26:34 +0100107 # if not token_info:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530108 token_info = self.db.get_one(self.tokens_collection, {"_id": token})
delacruzramoad682a52019-12-10 16:26:34 +0100109 if token_info["expires"] < now:
garciadeblas4568a372021-03-24 09:19:48 +0100110 raise AuthException(
111 "Expired Token or Authorization HTTP header",
112 http_code=HTTPStatus.UNAUTHORIZED,
113 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200114
tierno701018c2019-06-25 11:13:14 +0000115 return token_info
delacruzramoceb8baf2019-06-21 14:25:38 +0200116
117 except DbException as e:
118 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100119 raise AuthException(
120 "Invalid Token or Authorization HTTP header",
121 http_code=HTTPStatus.UNAUTHORIZED,
122 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200123 else:
124 raise
125 except AuthException:
tiernoe1eb3b22019-08-26 15:59:24 +0000126 raise
delacruzramoceb8baf2019-06-21 14:25:38 +0200127 except Exception:
garciadeblas4568a372021-03-24 09:19:48 +0100128 self.logger.exception(
129 "Error during token validation using internal backend"
130 )
131 raise AuthException(
132 "Error during token validation using internal backend",
133 http_code=HTTPStatus.UNAUTHORIZED,
134 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200135
136 def revoke_token(self, token):
137 """
138 Invalidate a token.
139
140 :param token: token to be revoked
141 """
142 try:
delacruzramoad682a52019-12-10 16:26:34 +0100143 # self.token_cache.pop(token, None)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530144 self.db.del_one(self.tokens_collection, {"_id": token})
delacruzramoceb8baf2019-06-21 14:25:38 +0200145 return True
146 except DbException as e:
147 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100148 raise AuthException(
149 "Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND
150 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200151 else:
152 # raise
delacruzramoad682a52019-12-10 16:26:34 +0100153 exmsg = "Error during token revocation using internal backend"
154 self.logger.exception(exmsg)
155 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoceb8baf2019-06-21 14:25:38 +0200156
jeganbe1a3df2024-06-04 12:05:19 +0000157 def validate_user(self, user, password, otp=None):
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530158 """
159 Validate username and password via appropriate backend.
160 :param user: username of the user.
161 :param password: password to be validated.
162 """
garciadeblas4568a372021-03-24 09:19:48 +0100163 user_rows = self.db.get_list(
164 self.users_collection, {BaseTopic.id_field("users", user): user}
165 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200166 now = time()
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530167 user_content = None
garciadeblas6d83f8f2023-06-19 22:34:49 +0200168 if user:
169 user_rows = self.db.get_list(
170 self.users_collection,
171 {BaseTopic.id_field(self.users_collection, user): user},
172 )
173 if user_rows:
174 user_content = user_rows[0]
175 # Updating user_status for every system_admin id role login
176 mapped_roles = user_content.get("project_role_mappings")
177 for role in mapped_roles:
178 role_id = role.get("role")
179 role_assigned = self.db.get_one(
180 self.roles_collection,
181 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
182 )
183
184 if role_assigned.get("permissions")["admin"]:
185 if role_assigned.get("permissions")["default"]:
186 if self.config.get("user_management"):
187 filt = {}
188 users = self.db.get_list(self.users_collection, filt)
189 for user_info in users:
190 if not user_info.get("username") == "admin":
191 if not user_info.get("_admin").get(
192 "account_expire_time"
193 ):
194 expire = now + 86400 * self.config.get(
195 "account_expire_days"
196 )
197 self.db.set_one(
198 self.users_collection,
199 {"_id": user_info["_id"]},
200 {"_admin.account_expire_time": expire},
201 )
202 else:
203 if now > user_info.get("_admin").get(
204 "account_expire_time"
205 ):
206 self.db.set_one(
207 self.users_collection,
208 {"_id": user_info["_id"]},
209 {"_admin.user_status": "expired"},
210 )
211 break
212
213 # To add "admin" user_status key while upgrading osm setup with feature enabled
214 if user_content.get("username") == "admin":
215 if self.config.get("user_management"):
216 self.db.set_one(
217 self.users_collection,
218 {"_id": user_content["_id"]},
219 {"_admin.user_status": "always-active"},
220 )
221
222 if not user_content.get("username") == "admin":
223 if self.config.get("user_management"):
224 if not user_content.get("_admin").get("account_expire_time"):
225 account_expire_time = now + 86400 * self.config.get(
226 "account_expire_days"
227 )
228 self.db.set_one(
229 self.users_collection,
230 {"_id": user_content["_id"]},
231 {"_admin.account_expire_time": account_expire_time},
232 )
233 else:
234 account_expire_time = user_content.get("_admin").get(
235 "account_expire_time"
236 )
237
238 if now > account_expire_time:
239 self.db.set_one(
240 self.users_collection,
241 {"_id": user_content["_id"]},
242 {"_admin.user_status": "expired"},
243 )
244 raise AuthException(
245 "Account expired", http_code=HTTPStatus.UNAUTHORIZED
246 )
247
248 if user_content.get("_admin").get("user_status") == "locked":
249 raise AuthException(
250 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
251 )
252 elif user_content.get("_admin").get("user_status") == "expired":
253 raise AuthException(
254 "Failed to login as the account is expired"
255 )
jeganbe1a3df2024-06-04 12:05:19 +0000256 if otp:
257 return user_content
escaleira9e393822025-04-03 18:53:24 +0100258 correct_pwd = False
259 if user_content.get("hashing_function") == "bcrypt":
260 correct_pwd = verify_password(
261 password=password, hashed_password_hex=user_content["password"]
262 )
263 else:
264 correct_pwd = verify_password_sha256(
265 password=password,
266 hashed_password_hex=user_content["password"],
267 salt=user_content["_admin"]["salt"],
268 )
269 if not correct_pwd:
garciadeblas6d83f8f2023-06-19 22:34:49 +0200270 count = 1
271 if user_content.get("_admin").get("retry_count") >= 0:
272 count += user_content.get("_admin").get("retry_count")
273 self.db.set_one(
274 self.users_collection,
275 {"_id": user_content["_id"]},
276 {"_admin.retry_count": count},
277 )
278 self.logger.debug(
279 "Failed Authentications count: {}".format(count)
280 )
281
282 if user_content.get("username") == "admin":
283 user_content = None
284 else:
285 if not self.config.get("user_management"):
286 user_content = None
287 else:
288 if (
289 user_content.get("_admin").get("retry_count")
290 >= self.config["max_pwd_attempt"] - 1
291 ):
292 self.db.set_one(
293 self.users_collection,
294 {"_id": user_content["_id"]},
295 {"_admin.user_status": "locked"},
296 )
297 raise AuthException(
298 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
299 )
300 else:
301 user_content = None
escaleira9e393822025-04-03 18:53:24 +0100302 elif correct_pwd and user_content.get("hashing_function") != "bcrypt":
303 # Update the database using a more secure hashing function to store the password
304 user_content["password"] = hash_password(
305 password=password,
306 rounds=self.config.get("password_rounds", 12),
307 )
308 user_content["hashing_function"] = "bcrypt"
309 user_content["_admin"]["password_history_sha256"] = user_content[
310 "_admin"
311 ]["password_history"]
312 user_content["_admin"]["password_history"] = [
313 user_content["password"]
314 ]
315 del user_content["_admin"]["salt"]
316
317 uid = user_content["_id"]
318 idf = BaseTopic.id_field("users", uid)
319 self.db.set_one(self.users_collection, {idf: uid}, user_content)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530320 return user_content
321
tierno6486f742020-02-13 16:30:14 +0000322 def authenticate(self, credentials, token_info=None):
delacruzramoceb8baf2019-06-21 14:25:38 +0200323 """
tierno701018c2019-06-25 11:13:14 +0000324 Authenticate a user using username/password or previous token_info plus project; its creates a new token
delacruzramoceb8baf2019-06-21 14:25:38 +0200325
tierno6486f742020-02-13 16:30:14 +0000326 :param credentials: dictionary that contains:
327 username: name, id or None
328 password: password or None
329 project_id: name, id, or None. If None first found project will be used to get an scope token
330 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000331 :param token_info: previous token_info to obtain authorization
delacruzramoceb8baf2019-06-21 14:25:38 +0200332 :return: the scoped token info or raises an exception. The token is a dictionary with:
333 _id: token string id,
334 username: username,
335 project_id: scoped_token project_id,
336 project_name: scoped_token project_name,
337 expires: epoch time when it expires,
338 """
339
340 now = time()
341 user_content = None
tierno6486f742020-02-13 16:30:14 +0000342 user = credentials.get("username")
343 password = credentials.get("password")
344 project = credentials.get("project_id")
jeganbe1a3df2024-06-04 12:05:19 +0000345 otp_validation = credentials.get("otp")
delacruzramoceb8baf2019-06-21 14:25:38 +0200346
delacruzramo01b15d32019-07-02 14:37:47 +0200347 # Try using username/password
jeganbe1a3df2024-06-04 12:05:19 +0000348 if otp_validation:
349 user_content = self.validate_user(user, password=None, otp=otp_validation)
350 elif user:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530351 user_content = self.validate_user(user, password)
delacruzramo01b15d32019-07-02 14:37:47 +0200352 if not user_content:
elumalai7802ff82023-04-24 20:38:32 +0530353 cef_event(
354 self.cef_logger,
355 {
356 "name": "User login",
357 "sourceUserName": user,
358 "message": "Invalid username/password Project={} Outcome=Failure".format(
359 project
360 ),
361 "severity": "3",
362 },
363 )
364 self.logger.exception("{}".format(self.cef_logger))
garciadeblas4568a372021-03-24 09:19:48 +0100365 raise AuthException(
366 "Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED
367 )
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530368 if not user_content.get("_admin", None):
garciadeblas4568a372021-03-24 09:19:48 +0100369 raise AuthException(
370 "No default project for this user.",
371 http_code=HTTPStatus.UNAUTHORIZED,
372 )
delacruzramo01b15d32019-07-02 14:37:47 +0200373 elif token_info:
garciadeblas4568a372021-03-24 09:19:48 +0100374 user_rows = self.db.get_list(
375 self.users_collection, {"username": token_info["username"]}
376 )
delacruzramo01b15d32019-07-02 14:37:47 +0200377 if user_rows:
378 user_content = user_rows[0]
delacruzramoceb8baf2019-06-21 14:25:38 +0200379 else:
delacruzramo01b15d32019-07-02 14:37:47 +0200380 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
381 else:
garciadeblas4568a372021-03-24 09:19:48 +0100382 raise AuthException(
383 "Provide credentials: username/password or Authorization Bearer token",
384 http_code=HTTPStatus.UNAUTHORIZED,
385 )
delacruzramoad682a52019-12-10 16:26:34 +0100386 # Delay upon second request within time window
garciadeblas4568a372021-03-24 09:19:48 +0100387 if (
388 now - user_content["_admin"].get("last_token_time", 0)
389 < self.token_time_window
390 ):
delacruzramoad682a52019-12-10 16:26:34 +0100391 sleep(self.token_delay)
392 # user_content["_admin"]["last_token_time"] = now
393 # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions
garciadeblas6d83f8f2023-06-19 22:34:49 +0200394 user_data = {
395 "_admin.last_token_time": now,
396 "_admin.retry_count": 0,
397 }
garciadeblas4568a372021-03-24 09:19:48 +0100398 self.db.set_one(
399 self.users_collection,
400 {"_id": user_content["_id"]},
garciadeblas6d83f8f2023-06-19 22:34:49 +0200401 user_data,
garciadeblas4568a372021-03-24 09:19:48 +0100402 )
delacruzramoad682a52019-12-10 16:26:34 +0100403
garciadeblasd8c3d8e2025-06-25 17:18:05 +0200404 # Generate a secure random 32 byte array base64 encoded for use in URLs
405 token_id = secrets.token_urlsafe(32)
delacruzramoceb8baf2019-06-21 14:25:38 +0200406
delacruzramo01b15d32019-07-02 14:37:47 +0200407 # projects = user_content.get("projects", [])
408 prm_list = user_content.get("project_role_mappings", [])
delacruzramoceb8baf2019-06-21 14:25:38 +0200409
delacruzramo01b15d32019-07-02 14:37:47 +0200410 if not project:
411 project = prm_list[0]["project"] if prm_list else None
412 if not project:
garciadeblas4568a372021-03-24 09:19:48 +0100413 raise AuthException(
414 "can't find a default project for this user",
415 http_code=HTTPStatus.UNAUTHORIZED,
416 )
tierno701018c2019-06-25 11:13:14 +0000417
delacruzramo01b15d32019-07-02 14:37:47 +0200418 projects = [prm["project"] for prm in prm_list]
delacruzramoceb8baf2019-06-21 14:25:38 +0200419
garciadeblas4568a372021-03-24 09:19:48 +0100420 proj = self.db.get_one(
421 self.projects_collection, {BaseTopic.id_field("projects", project): project}
422 )
delacruzramo01b15d32019-07-02 14:37:47 +0200423 project_name = proj["name"]
424 project_id = proj["_id"]
425 if project_name not in projects and project_id not in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100426 raise AuthException(
427 "project {} not allowed for this user".format(project),
428 http_code=HTTPStatus.UNAUTHORIZED,
429 )
tierno701018c2019-06-25 11:13:14 +0000430
delacruzramo01b15d32019-07-02 14:37:47 +0200431 # TODO remove admin, this vill be used by roles RBAC
432 if project_name == "admin":
433 token_admin = True
434 else:
435 token_admin = proj.get("admin", False)
delacruzramoceb8baf2019-06-21 14:25:38 +0200436
delacruzramo01b15d32019-07-02 14:37:47 +0200437 # add token roles
438 roles = []
439 roles_list = []
440 for prm in prm_list:
441 if prm["project"] in [project_id, project_name]:
garciadeblas4568a372021-03-24 09:19:48 +0100442 role = self.db.get_one(
443 self.roles_collection,
444 {BaseTopic.id_field("roles", prm["role"]): prm["role"]},
445 )
delacruzramo01b15d32019-07-02 14:37:47 +0200446 rid = role["_id"]
447 if rid not in roles:
448 rnm = role["name"]
449 roles.append(rid)
450 roles_list.append({"name": rnm, "id": rid})
451 if not roles_list:
garciadeblas4568a372021-03-24 09:19:48 +0100452 rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})[
453 "_id"
454 ]
delacruzramo01b15d32019-07-02 14:37:47 +0200455 roles_list = [{"name": "project_admin", "id": rid}]
delacruzramoceb8baf2019-06-21 14:25:38 +0200456
garciadeblas6d83f8f2023-06-19 22:34:49 +0200457 login_count = user_content.get("_admin").get("retry_count")
458 last_token_time = user_content.get("_admin").get("last_token_time")
459
460 admin_show = False
461 user_show = False
462 if self.config.get("user_management"):
463 for role in roles_list:
464 role_id = role.get("id")
465 permission = self.db.get_one(
466 self.roles_collection,
467 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
468 )
469 if permission.get("permissions")["admin"]:
470 if permission.get("permissions")["default"]:
471 admin_show = True
472 break
473 else:
474 user_show = True
garciadeblas4568a372021-03-24 09:19:48 +0100475 new_token = {
476 "issued_at": now,
477 "expires": now + 3600,
478 "_id": token_id,
479 "id": token_id,
480 "project_id": proj["_id"],
481 "project_name": proj["name"],
482 "username": user_content["username"],
483 "user_id": user_content["_id"],
484 "admin": token_admin,
485 "roles": roles_list,
garciadeblas6d83f8f2023-06-19 22:34:49 +0200486 "login_count": login_count,
487 "last_login": last_token_time,
488 "admin_show": admin_show,
489 "user_show": user_show,
garciadeblas4568a372021-03-24 09:19:48 +0100490 }
delacruzramoceb8baf2019-06-21 14:25:38 +0200491
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530492 self.db.create(self.tokens_collection, new_token)
delacruzramo01b15d32019-07-02 14:37:47 +0200493 return deepcopy(new_token)
494
495 def get_role_list(self, filter_q={}):
delacruzramoceb8baf2019-06-21 14:25:38 +0200496 """
497 Get role list.
498
499 :return: returns the list of roles.
500 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530501 return self.db.get_list(self.roles_collection, filter_q)
delacruzramoceb8baf2019-06-21 14:25:38 +0200502
delacruzramo01b15d32019-07-02 14:37:47 +0200503 def create_role(self, role_info):
delacruzramoceb8baf2019-06-21 14:25:38 +0200504 """
505 Create a role.
506
delacruzramo01b15d32019-07-02 14:37:47 +0200507 :param role_info: full role info.
508 :return: returns the role id.
delacruzramoceb8baf2019-06-21 14:25:38 +0200509 :raises AuthconnOperationException: if role creation failed.
510 """
delacruzramoceb8baf2019-06-21 14:25:38 +0200511 # TODO: Check that role name does not exist ?
delacruzramo01b15d32019-07-02 14:37:47 +0200512 rid = str(uuid4())
513 role_info["_id"] = rid
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530514 rid = self.db.create(self.roles_collection, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200515 return rid
delacruzramoceb8baf2019-06-21 14:25:38 +0200516
517 def delete_role(self, role_id):
518 """
519 Delete a role.
520
521 :param role_id: role identifier.
522 :raises AuthconnOperationException: if role deletion failed.
523 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530524 rc = self.db.del_one(self.roles_collection, {"_id": role_id})
525 self.db.del_list(self.tokens_collection, {"roles.id": role_id})
delacruzramoad682a52019-12-10 16:26:34 +0100526 return rc
delacruzramo01b15d32019-07-02 14:37:47 +0200527
528 def update_role(self, role_info):
529 """
530 Update a role.
531
532 :param role_info: full role info.
533 :return: returns the role name and id.
534 :raises AuthconnOperationException: if user creation failed.
535 """
536 rid = role_info["_id"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530537 self.db.set_one(self.roles_collection, {"_id": rid}, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200538 return {"_id": rid, "name": role_info["name"]}
539
540 def create_user(self, user_info):
541 """
542 Create a user.
543
544 :param user_info: full user info.
545 :return: returns the username and id of the user.
546 """
547 BaseTopic.format_on_new(user_info, make_public=False)
garciadeblas6d83f8f2023-06-19 22:34:49 +0200548 user_info["_admin"]["user_status"] = "active"
selvi.ja9a1fc82022-04-04 06:54:30 +0000549 present = time()
550 if not user_info["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200551 if self.config.get("user_management"):
552 user_info["_admin"]["modified"] = present
553 user_info["_admin"]["password_expire_time"] = present
554 account_expire_time = present + 86400 * self.config.get(
555 "account_expire_days"
556 )
557 user_info["_admin"]["account_expire_time"] = account_expire_time
558
559 user_info["_admin"]["retry_count"] = 0
560 user_info["_admin"]["last_token_time"] = present
delacruzramo01b15d32019-07-02 14:37:47 +0200561 if "password" in user_info:
escaleira9e393822025-04-03 18:53:24 +0100562 user_info["password"] = hash_password(
563 password=user_info["password"],
564 rounds=self.config.get("password_rounds", 12),
565 )
566 user_info["hashing_function"] = "bcrypt"
567 user_info["_admin"]["password_history"] = [user_info["password"]]
delacruzramo01b15d32019-07-02 14:37:47 +0200568 # "projects" are not stored any more
569 if "projects" in user_info:
570 del user_info["projects"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530571 self.db.create(self.users_collection, user_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200572 return {"username": user_info["username"], "_id": user_info["_id"]}
573
574 def update_user(self, user_info):
575 """
576 Change the user name and/or password.
577
578 :param user_info: user info modifications
579 """
580 uid = user_info["_id"]
selvi.ja9a1fc82022-04-04 06:54:30 +0000581 old_pwd = user_info.get("old_password")
garciadeblas6d83f8f2023-06-19 22:34:49 +0200582 unlock = user_info.get("unlock")
583 renew = user_info.get("renew")
584 permission_id = user_info.get("system_admin_id")
Adurti0c9b0102023-11-08 11:16:32 +0000585 now = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200586
garciadeblas4568a372021-03-24 09:19:48 +0100587 user_data = self.db.get_one(
588 self.users_collection, {BaseTopic.id_field("users", uid): uid}
589 )
selvi.ja9a1fc82022-04-04 06:54:30 +0000590 if old_pwd:
escaleira9e393822025-04-03 18:53:24 +0100591 correct_pwd = False
592 if user_data.get("hashing_function") == "bcrypt":
593 correct_pwd = verify_password(
594 password=old_pwd, hashed_password_hex=user_data["password"]
595 )
596 else:
597 correct_pwd = verify_password_sha256(
598 password=old_pwd,
599 hashed_password_hex=user_data["password"],
600 salt=user_data["salt"],
601 )
602 if not correct_pwd:
selvi.ja9a1fc82022-04-04 06:54:30 +0000603 raise AuthconnConflictException(
garciadeblasf2af4a12023-01-24 16:56:54 +0100604 "Incorrect password", http_code=HTTPStatus.CONFLICT
selvi.ja9a1fc82022-04-04 06:54:30 +0000605 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200606 # Unlocking the user
607 if unlock:
608 system_user = None
609 unlock_state = False
610 if not permission_id:
611 raise AuthconnConflictException(
612 "system_admin_id is the required field to unlock the user",
613 http_code=HTTPStatus.CONFLICT,
614 )
615 else:
616 system_user = self.db.get_one(
617 self.users_collection,
618 {
619 BaseTopic.id_field(
620 self.users_collection, permission_id
621 ): permission_id
622 },
623 )
624 mapped_roles = system_user.get("project_role_mappings")
625 for role in mapped_roles:
626 role_id = role.get("role")
627 role_assigned = self.db.get_one(
628 self.roles_collection,
629 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
630 )
631 if role_assigned.get("permissions")["admin"]:
632 if role_assigned.get("permissions")["default"]:
633 user_data["_admin"]["retry_count"] = 0
Adurti0c9b0102023-11-08 11:16:32 +0000634 if now > user_data["_admin"]["account_expire_time"]:
635 user_data["_admin"]["user_status"] = "expired"
636 else:
637 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200638 unlock_state = True
639 break
640 if not unlock_state:
641 raise AuthconnConflictException(
642 "User '{}' does not have the privilege to unlock the user".format(
643 permission_id
644 ),
645 http_code=HTTPStatus.CONFLICT,
646 )
647 # Renewing the user
648 if renew:
649 system_user = None
650 renew_state = False
651 if not permission_id:
652 raise AuthconnConflictException(
653 "system_admin_id is the required field to renew the user",
654 http_code=HTTPStatus.CONFLICT,
655 )
656 else:
657 system_user = self.db.get_one(
658 self.users_collection,
659 {
660 BaseTopic.id_field(
661 self.users_collection, permission_id
662 ): permission_id
663 },
664 )
665 mapped_roles = system_user.get("project_role_mappings")
666 for role in mapped_roles:
667 role_id = role.get("role")
668 role_assigned = self.db.get_one(
669 self.roles_collection,
670 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
671 )
672 if role_assigned.get("permissions")["admin"]:
673 if role_assigned.get("permissions")["default"]:
674 present = time()
675 account_expire = (
676 present + 86400 * self.config["account_expire_days"]
677 )
678 user_data["_admin"]["modified"] = present
679 user_data["_admin"]["account_expire_time"] = account_expire
Adurti0c9b0102023-11-08 11:16:32 +0000680 if (
681 user_data["_admin"]["retry_count"]
682 >= self.config["max_pwd_attempt"]
683 ):
684 user_data["_admin"]["user_status"] = "locked"
685 else:
686 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200687 renew_state = True
688 break
689 if not renew_state:
690 raise AuthconnConflictException(
691 "User '{}' does not have the privilege to renew the user".format(
692 permission_id
693 ),
694 http_code=HTTPStatus.CONFLICT,
695 )
delacruzramo01b15d32019-07-02 14:37:47 +0200696 BaseTopic.format_on_edit(user_data, user_info)
697 # User Name
698 usnm = user_info.get("username")
jeganbe1a3df2024-06-04 12:05:19 +0000699 email_id = user_info.get("email_id")
delacruzramo01b15d32019-07-02 14:37:47 +0200700 if usnm:
701 user_data["username"] = usnm
jeganbe1a3df2024-06-04 12:05:19 +0000702 if email_id:
703 user_data["email_id"] = email_id
delacruzramo01b15d32019-07-02 14:37:47 +0200704 # If password is given and is not already encripted
705 pswd = user_info.get("password")
garciadeblas4568a372021-03-24 09:19:48 +0100706 if pswd and (
707 len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd)
708 ): # TODO: Improve check?
elumalai7802ff82023-04-24 20:38:32 +0530709 cef_event(
710 self.cef_logger,
711 {
712 "name": "Change Password",
713 "sourceUserName": user_data["username"],
garciadeblasf53612b2024-07-12 14:44:37 +0200714 "message": "User {} changing Password for user {}, Outcome=Success".format(
715 user_info.get("session_user"), user_data["username"]
716 ),
elumalai7802ff82023-04-24 20:38:32 +0530717 "severity": "2",
718 },
719 )
720 self.logger.info("{}".format(self.cef_logger))
delacruzramo01b15d32019-07-02 14:37:47 +0200721 if "_admin" not in user_data:
722 user_data["_admin"] = {}
garciadeblas6d83f8f2023-06-19 22:34:49 +0200723 if user_data.get("_admin").get("password_history"):
724 old_pwds = user_data.get("_admin").get("password_history")
725 else:
escaleira9e393822025-04-03 18:53:24 +0100726 old_pwds = []
727 for v in old_pwds:
728 if verify_password(password=pswd, hashed_password_hex=v):
garciadeblas6d83f8f2023-06-19 22:34:49 +0200729 raise AuthconnConflictException(
730 "Password is used before", http_code=HTTPStatus.CONFLICT
731 )
escaleira9e393822025-04-03 18:53:24 +0100732
733 # Backwards compatibility for SHA256 hashed passwords
734 if user_data.get("_admin").get("password_history_sha256"):
735 old_pwds_sha256 = user_data.get("_admin").get("password_history_sha256")
736 else:
737 old_pwds_sha256 = {}
738 for k, v in old_pwds_sha256.items():
739 if verify_password_sha256(password=pswd, hashed_password_hex=v, salt=k):
740 raise AuthconnConflictException(
741 "Password is used before", http_code=HTTPStatus.CONFLICT
742 )
743
744 # Finally, hash the password to be updated
745 user_data["password"] = hash_password(
746 password=pswd, rounds=self.config.get("password_rounds", 12)
747 )
748 user_data["hashing_function"] = "bcrypt"
749
garciadeblas6d83f8f2023-06-19 22:34:49 +0200750 if len(old_pwds) >= 3:
751 old_pwds.pop(list(old_pwds.keys())[0])
escaleira9e393822025-04-03 18:53:24 +0100752 old_pwds.append([user_data["password"]])
garciadeblas6d83f8f2023-06-19 22:34:49 +0200753 user_data["_admin"]["password_history"] = old_pwds
selvi.ja9a1fc82022-04-04 06:54:30 +0000754 if not user_data["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200755 if self.config.get("user_management"):
selvi.ja9a1fc82022-04-04 06:54:30 +0000756 present = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200757 if self.config.get("pwd_expire_days"):
758 expire = present + 86400 * self.config.get("pwd_expire_days")
759 user_data["_admin"]["modified"] = present
760 user_data["_admin"]["password_expire_time"] = expire
delacruzramo01b15d32019-07-02 14:37:47 +0200761 # Project-Role Mappings
762 # TODO: Check that user_info NEVER includes "project_role_mappings"
763 if "project_role_mappings" not in user_data:
764 user_data["project_role_mappings"] = []
765 for prm in user_info.get("add_project_role_mappings", []):
766 user_data["project_role_mappings"].append(prm)
767 for prm in user_info.get("remove_project_role_mappings", []):
768 for pidf in ["project", "project_name"]:
769 for ridf in ["role", "role_name"]:
770 try:
garciadeblas4568a372021-03-24 09:19:48 +0100771 user_data["project_role_mappings"].remove(
772 {"role": prm[ridf], "project": prm[pidf]}
773 )
delacruzramo01b15d32019-07-02 14:37:47 +0200774 except KeyError:
775 pass
776 except ValueError:
777 pass
delacruzramo3d6881c2019-12-04 13:42:26 +0100778 idf = BaseTopic.id_field("users", uid)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530779 self.db.set_one(self.users_collection, {idf: uid}, user_data)
delacruzramo3d6881c2019-12-04 13:42:26 +0100780 if user_info.get("remove_project_role_mappings"):
delacruzramoad682a52019-12-10 16:26:34 +0100781 idf = "user_id" if idf == "_id" else idf
Adurti9f2ac992024-03-25 10:58:29 +0000782 if not user_data.get("project_role_mappings") or user_info.get(
783 "remove_session_project"
784 ):
785 self.db.del_list(self.tokens_collection, {idf: uid})
delacruzramo01b15d32019-07-02 14:37:47 +0200786
787 def delete_user(self, user_id):
788 """
789 Delete user.
790
791 :param user_id: user identifier.
792 :raises AuthconnOperationException: if user deletion failed.
793 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530794 self.db.del_one(self.users_collection, {"_id": user_id})
795 self.db.del_list(self.tokens_collection, {"user_id": user_id})
delacruzramoceb8baf2019-06-21 14:25:38 +0200796 return True
delacruzramo01b15d32019-07-02 14:37:47 +0200797
798 def get_user_list(self, filter_q=None):
799 """
800 Get user list.
801
tierno5ec768a2020-03-31 09:46:44 +0000802 :param filter_q: dictionary to filter user list by:
803 name (username is also admitted). If a user id is equal to the filter name, it is also provided
804 other
delacruzramo01b15d32019-07-02 14:37:47 +0200805 :return: returns a list of users.
806 """
807 filt = filter_q or {}
tierno5ec768a2020-03-31 09:46:44 +0000808 if "name" in filt: # backward compatibility
809 filt["username"] = filt.pop("name")
810 if filt.get("username") and is_valid_uuid(filt["username"]):
811 # username cannot be a uuid. If this is the case, change from username to _id
812 filt["_id"] = filt.pop("username")
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530813 users = self.db.get_list(self.users_collection, filt)
tierno1546f2a2019-08-20 15:38:11 +0000814 project_id_name = {}
815 role_id_name = {}
delacruzramo01b15d32019-07-02 14:37:47 +0200816 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000817 prms = user.get("project_role_mappings")
818 projects = user.get("projects")
819 if prms:
820 projects = []
821 # add project_name and role_name. Generate projects for backward compatibility
delacruzramo01b15d32019-07-02 14:37:47 +0200822 for prm in prms:
tierno1546f2a2019-08-20 15:38:11 +0000823 project_id = prm["project"]
824 if project_id not in project_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100825 pr = self.db.get_one(
826 self.projects_collection,
827 {BaseTopic.id_field("projects", project_id): project_id},
828 fail_on_empty=False,
829 )
tierno1546f2a2019-08-20 15:38:11 +0000830 project_id_name[project_id] = pr["name"] if pr else None
831 prm["project_name"] = project_id_name[project_id]
832 if prm["project_name"] not in projects:
833 projects.append(prm["project_name"])
834
835 role_id = prm["role"]
836 if role_id not in role_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100837 role = self.db.get_one(
838 self.roles_collection,
839 {BaseTopic.id_field("roles", role_id): role_id},
840 fail_on_empty=False,
841 )
tierno1546f2a2019-08-20 15:38:11 +0000842 role_id_name[role_id] = role["name"] if role else None
843 prm["role_name"] = role_id_name[role_id]
844 user["projects"] = projects # for backward compatibility
845 elif projects:
846 # user created with an old version. Create a project_role mapping with role project_admin
847 user["project_role_mappings"] = []
garciadeblas4568a372021-03-24 09:19:48 +0100848 role = self.db.get_one(
849 self.roles_collection,
850 {BaseTopic.id_field("roles", "project_admin"): "project_admin"},
851 )
tierno1546f2a2019-08-20 15:38:11 +0000852 for p_id_name in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100853 pr = self.db.get_one(
854 self.projects_collection,
855 {BaseTopic.id_field("projects", p_id_name): p_id_name},
856 )
857 prm = {
858 "project": pr["_id"],
859 "project_name": pr["name"],
860 "role_name": "project_admin",
861 "role": role["_id"],
862 }
tierno1546f2a2019-08-20 15:38:11 +0000863 user["project_role_mappings"].append(prm)
864 else:
865 user["projects"] = []
866 user["project_role_mappings"] = []
867
delacruzramo01b15d32019-07-02 14:37:47 +0200868 return users
869
870 def get_project_list(self, filter_q={}):
871 """
872 Get role list.
873
874 :return: returns the list of projects.
875 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530876 return self.db.get_list(self.projects_collection, filter_q)
delacruzramo01b15d32019-07-02 14:37:47 +0200877
878 def create_project(self, project_info):
879 """
880 Create a project.
881
882 :param project: full project info.
883 :return: the internal id of the created project
884 :raises AuthconnOperationException: if project creation failed.
885 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530886 pid = self.db.create(self.projects_collection, project_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200887 return pid
888
889 def delete_project(self, project_id):
890 """
891 Delete a project.
892
893 :param project_id: project identifier.
894 :raises AuthconnOperationException: if project deletion failed.
895 """
delacruzramoad682a52019-12-10 16:26:34 +0100896 idf = BaseTopic.id_field("projects", project_id)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530897 r = self.db.del_one(self.projects_collection, {idf: project_id})
delacruzramoad682a52019-12-10 16:26:34 +0100898 idf = "project_id" if idf == "_id" else "project_name"
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530899 self.db.del_list(self.tokens_collection, {idf: project_id})
delacruzramo01b15d32019-07-02 14:37:47 +0200900 return r
901
902 def update_project(self, project_id, project_info):
903 """
904 Change the name of a project
905
906 :param project_id: project to be changed
907 :param project_info: full project info
908 :return: None
909 :raises AuthconnOperationException: if project update failed.
910 """
garciadeblas4568a372021-03-24 09:19:48 +0100911 self.db.set_one(
912 self.projects_collection,
913 {BaseTopic.id_field("projects", project_id): project_id},
914 project_info,
915 )
jeganbe1a3df2024-06-04 12:05:19 +0000916
917 def generate_otp(self):
jegan804b3592025-06-30 12:40:50 +0530918 otp = "".join(str(secrets.randbelow(10)) for i in range(0, 4))
jeganbe1a3df2024-06-04 12:05:19 +0000919 return otp
920
921 def send_email(self, indata):
922 user = indata.get("username")
923 user_rows = self.db.get_list(self.users_collection, {"username": user})
924 sender_password = None
925 otp_expiry_time = self.config.get("otp_expiry_time", 300)
926 if not re.match(email_schema["pattern"], indata.get("email_id")):
927 raise AuthException(
928 "Invalid email-id",
929 http_code=HTTPStatus.BAD_REQUEST,
930 )
931 if self.config.get("sender_email"):
932 sender_email = self.config["sender_email"]
933 else:
934 raise AuthException(
935 "sender_email not found",
936 http_code=HTTPStatus.NOT_FOUND,
937 )
938 if self.config.get("smtp_server"):
939 smtp_server = self.config["smtp_server"]
940 else:
941 raise AuthException(
942 "smtp server not found",
943 http_code=HTTPStatus.NOT_FOUND,
944 )
945 if self.config.get("smtp_port"):
946 smtp_port = self.config["smtp_port"]
947 else:
948 raise AuthException(
949 "smtp port not found",
950 http_code=HTTPStatus.NOT_FOUND,
951 )
952 sender_password = self.config.get("sender_password") or None
953 if user_rows:
954 user_data = user_rows[0]
955 user_status = user_data["_admin"]["user_status"]
956 if not user_data.get("project_role_mappings", None):
957 raise AuthException(
958 "can't find a default project for this user",
959 http_code=HTTPStatus.UNAUTHORIZED,
960 )
961 if user_status != "active" and user_status != "always-active":
962 raise AuthException(
963 f"User account is {user_status}.Please contact the system administrator.",
964 http_code=HTTPStatus.UNAUTHORIZED,
965 )
966 if user_data.get("email_id"):
967 if user_data["email_id"] == indata.get("email_id"):
968 otp = self.generate_otp()
escaleira9e393822025-04-03 18:53:24 +0100969 encode_otp = hash_password(
970 password=otp, rounds=self.config.get("password_rounds", 12)
971 )
jeganbe1a3df2024-06-04 12:05:19 +0000972 otp_field = {encode_otp: time() + otp_expiry_time, "retries": 0}
973 user_data["OTP"] = otp_field
974 uid = user_data["_id"]
975 idf = BaseTopic.id_field("users", uid)
976 reciever_email = user_data["email_id"]
977 email_template_path = self.config.get("email_template")
978 with open(email_template_path, "r") as et:
979 email_template = et.read()
980 msg = EmailMessage()
981 msg = MIMEMultipart("alternative")
982 html_content = email_template.format(
983 username=user_data["username"],
984 otp=otp,
985 validity=otp_expiry_time // 60,
986 )
987 html = MIMEText(html_content, "html")
988 msg["Subject"] = "OSM password reset request"
989 msg.attach(html)
990 with smtplib.SMTP(smtp_server, smtp_port) as smtp:
991 smtp.starttls()
992 if sender_password:
993 smtp.login(sender_email, sender_password)
994 smtp.sendmail(sender_email, reciever_email, msg.as_string())
995 self.db.set_one(self.users_collection, {idf: uid}, user_data)
996 return {"email": "sent"}
997 else:
998 raise AuthException(
999 "No email id is registered for this user.Please contact the system administrator.",
1000 http_code=HTTPStatus.NOT_FOUND,
1001 )
1002 else:
1003 raise AuthException(
1004 "user not found",
1005 http_code=HTTPStatus.NOT_FOUND,
1006 )
1007
1008 def validate_otp(self, indata):
1009 otp = indata.get("otp")
1010 user = indata.get("username")
1011 user_rows = self.db.get_list(self.users_collection, {"username": user})
1012 user_data = user_rows[0]
1013 uid = user_data["_id"]
1014 idf = BaseTopic.id_field("users", uid)
1015 retry_count = self.config.get("retry_count", 3)
1016 if user_data:
jeganbe1a3df2024-06-04 12:05:19 +00001017 if not user_data.get("OTP"):
1018 otp_field = {"retries": 1}
1019 user_data["OTP"] = otp_field
1020 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1021 return {"retries": user_data["OTP"]["retries"]}
1022 for key, value in user_data["OTP"].items():
1023 curr_time = time()
escaleira9e393822025-04-03 18:53:24 +01001024 if (
1025 verify_password(password=otp, hashed_password_hex=key)
1026 and curr_time < value
1027 ):
jeganbe1a3df2024-06-04 12:05:19 +00001028 user_data["OTP"] = {}
1029 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1030 return {"valid": "True", "password_change": "True"}
1031 else:
1032 user_data["OTP"]["retries"] += 1
1033 self.db.set_one(self.users_collection, {idf: uid}, user_data)
1034 if user_data["OTP"].get("retries") >= retry_count:
1035 raise AuthException(
1036 "Invalid OTP. Maximum retries exceeded",
1037 http_code=HTTPStatus.TOO_MANY_REQUESTS,
1038 )
1039 return {"retry_count": user_data["OTP"]["retries"]}