blob: cf8c55a5258517bf8349d978d6966cd1ea26b508 [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
jeganbe1a3df2024-06-04 12:05:19 +000044from osm_nbi.validation import is_valid_uuid, email_schema
delacruzramoad682a52019-12-10 16:26:34 +010045from time import time, sleep
delacruzramoceb8baf2019-06-21 14:25:38 +020046from http import HTTPStatus
47from uuid import uuid4
48from hashlib import sha256
49from copy import deepcopy
50from random import choice as random_choice
jeganbe1a3df2024-06-04 12:05:19 +000051import smtplib
52from email.message import EmailMessage
53from email.mime.text import MIMEText
54from email.mime.multipart import MIMEMultipart
delacruzramoceb8baf2019-06-21 14:25:38 +020055
56
57class AuthconnInternal(Authconn):
garciadeblas4568a372021-03-24 09:19:48 +010058 token_time_window = 2 # seconds
59 token_delay = 1 # seconds to wait upon second request within time window
delacruzramoceb8baf2019-06-21 14:25:38 +020060
K Sai Kiran7ddb0732020-10-30 11:14:44 +053061 users_collection = "users"
62 roles_collection = "roles"
63 projects_collection = "projects"
64 tokens_collection = "tokens"
65
tierno9e87a7f2020-03-23 09:24:10 +000066 def __init__(self, config, db, role_permissions):
67 Authconn.__init__(self, config, db, role_permissions)
delacruzramoceb8baf2019-06-21 14:25:38 +020068 self.logger = logging.getLogger("nbi.authenticator.internal")
69
delacruzramoceb8baf2019-06-21 14:25:38 +020070 self.db = db
delacruzramoad682a52019-12-10 16:26:34 +010071 # self.msg = msg
72 # self.token_cache = token_cache
delacruzramoceb8baf2019-06-21 14:25:38 +020073
74 # To be Confirmed
delacruzramoceb8baf2019-06-21 14:25:38 +020075 self.sess = None
elumalai7802ff82023-04-24 20:38:32 +053076 self.cef_logger = cef_event_builder(config)
delacruzramoceb8baf2019-06-21 14:25:38 +020077
delacruzramoceb8baf2019-06-21 14:25:38 +020078 def validate_token(self, token):
79 """
80 Check if the token is valid.
81
82 :param token: token to validate
83 :return: dictionary with information associated with the token:
84 "_id": token id
85 "project_id": project id
86 "project_name": project name
87 "user_id": user id
88 "username": user name
89 "roles": list with dict containing {name, id}
90 "expires": expiration date
91 If the token is not valid an exception is raised.
92 """
93
94 try:
95 if not token:
garciadeblas4568a372021-03-24 09:19:48 +010096 raise AuthException(
97 "Needed a token or Authorization HTTP header",
98 http_code=HTTPStatus.UNAUTHORIZED,
99 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200100
delacruzramoceb8baf2019-06-21 14:25:38 +0200101 now = time()
delacruzramoceb8baf2019-06-21 14:25:38 +0200102
103 # get from database if not in cache
delacruzramoad682a52019-12-10 16:26:34 +0100104 # if not token_info:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530105 token_info = self.db.get_one(self.tokens_collection, {"_id": token})
delacruzramoad682a52019-12-10 16:26:34 +0100106 if token_info["expires"] < now:
garciadeblas4568a372021-03-24 09:19:48 +0100107 raise AuthException(
108 "Expired Token or Authorization HTTP header",
109 http_code=HTTPStatus.UNAUTHORIZED,
110 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200111
tierno701018c2019-06-25 11:13:14 +0000112 return token_info
delacruzramoceb8baf2019-06-21 14:25:38 +0200113
114 except DbException as e:
115 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100116 raise AuthException(
117 "Invalid Token or Authorization HTTP header",
118 http_code=HTTPStatus.UNAUTHORIZED,
119 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200120 else:
121 raise
122 except AuthException:
tiernoe1eb3b22019-08-26 15:59:24 +0000123 raise
delacruzramoceb8baf2019-06-21 14:25:38 +0200124 except Exception:
garciadeblas4568a372021-03-24 09:19:48 +0100125 self.logger.exception(
126 "Error during token validation using internal backend"
127 )
128 raise AuthException(
129 "Error during token validation using internal backend",
130 http_code=HTTPStatus.UNAUTHORIZED,
131 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200132
133 def revoke_token(self, token):
134 """
135 Invalidate a token.
136
137 :param token: token to be revoked
138 """
139 try:
delacruzramoad682a52019-12-10 16:26:34 +0100140 # self.token_cache.pop(token, None)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530141 self.db.del_one(self.tokens_collection, {"_id": token})
delacruzramoceb8baf2019-06-21 14:25:38 +0200142 return True
143 except DbException as e:
144 if e.http_code == HTTPStatus.NOT_FOUND:
garciadeblas4568a372021-03-24 09:19:48 +0100145 raise AuthException(
146 "Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND
147 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200148 else:
149 # raise
delacruzramoad682a52019-12-10 16:26:34 +0100150 exmsg = "Error during token revocation using internal backend"
151 self.logger.exception(exmsg)
152 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
delacruzramoceb8baf2019-06-21 14:25:38 +0200153
jeganbe1a3df2024-06-04 12:05:19 +0000154 def validate_user(self, user, password, otp=None):
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530155 """
156 Validate username and password via appropriate backend.
157 :param user: username of the user.
158 :param password: password to be validated.
159 """
garciadeblas4568a372021-03-24 09:19:48 +0100160 user_rows = self.db.get_list(
161 self.users_collection, {BaseTopic.id_field("users", user): user}
162 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200163 now = time()
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530164 user_content = None
garciadeblas6d83f8f2023-06-19 22:34:49 +0200165 if user:
166 user_rows = self.db.get_list(
167 self.users_collection,
168 {BaseTopic.id_field(self.users_collection, user): user},
169 )
170 if user_rows:
171 user_content = user_rows[0]
172 # Updating user_status for every system_admin id role login
173 mapped_roles = user_content.get("project_role_mappings")
174 for role in mapped_roles:
175 role_id = role.get("role")
176 role_assigned = self.db.get_one(
177 self.roles_collection,
178 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
179 )
180
181 if role_assigned.get("permissions")["admin"]:
182 if role_assigned.get("permissions")["default"]:
183 if self.config.get("user_management"):
184 filt = {}
185 users = self.db.get_list(self.users_collection, filt)
186 for user_info in users:
187 if not user_info.get("username") == "admin":
188 if not user_info.get("_admin").get(
189 "account_expire_time"
190 ):
191 expire = now + 86400 * self.config.get(
192 "account_expire_days"
193 )
194 self.db.set_one(
195 self.users_collection,
196 {"_id": user_info["_id"]},
197 {"_admin.account_expire_time": expire},
198 )
199 else:
200 if now > user_info.get("_admin").get(
201 "account_expire_time"
202 ):
203 self.db.set_one(
204 self.users_collection,
205 {"_id": user_info["_id"]},
206 {"_admin.user_status": "expired"},
207 )
208 break
209
210 # To add "admin" user_status key while upgrading osm setup with feature enabled
211 if user_content.get("username") == "admin":
212 if self.config.get("user_management"):
213 self.db.set_one(
214 self.users_collection,
215 {"_id": user_content["_id"]},
216 {"_admin.user_status": "always-active"},
217 )
218
219 if not user_content.get("username") == "admin":
220 if self.config.get("user_management"):
221 if not user_content.get("_admin").get("account_expire_time"):
222 account_expire_time = now + 86400 * self.config.get(
223 "account_expire_days"
224 )
225 self.db.set_one(
226 self.users_collection,
227 {"_id": user_content["_id"]},
228 {"_admin.account_expire_time": account_expire_time},
229 )
230 else:
231 account_expire_time = user_content.get("_admin").get(
232 "account_expire_time"
233 )
234
235 if now > account_expire_time:
236 self.db.set_one(
237 self.users_collection,
238 {"_id": user_content["_id"]},
239 {"_admin.user_status": "expired"},
240 )
241 raise AuthException(
242 "Account expired", http_code=HTTPStatus.UNAUTHORIZED
243 )
244
245 if user_content.get("_admin").get("user_status") == "locked":
246 raise AuthException(
247 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
248 )
249 elif user_content.get("_admin").get("user_status") == "expired":
250 raise AuthException(
251 "Failed to login as the account is expired"
252 )
jeganbe1a3df2024-06-04 12:05:19 +0000253 if otp:
254 return user_content
garciadeblas6d83f8f2023-06-19 22:34:49 +0200255 salt = user_content["_admin"]["salt"]
256 shadow_password = sha256(
257 password.encode("utf-8") + salt.encode("utf-8")
258 ).hexdigest()
259 if shadow_password != user_content["password"]:
260 count = 1
261 if user_content.get("_admin").get("retry_count") >= 0:
262 count += user_content.get("_admin").get("retry_count")
263 self.db.set_one(
264 self.users_collection,
265 {"_id": user_content["_id"]},
266 {"_admin.retry_count": count},
267 )
268 self.logger.debug(
269 "Failed Authentications count: {}".format(count)
270 )
271
272 if user_content.get("username") == "admin":
273 user_content = None
274 else:
275 if not self.config.get("user_management"):
276 user_content = None
277 else:
278 if (
279 user_content.get("_admin").get("retry_count")
280 >= self.config["max_pwd_attempt"] - 1
281 ):
282 self.db.set_one(
283 self.users_collection,
284 {"_id": user_content["_id"]},
285 {"_admin.user_status": "locked"},
286 )
287 raise AuthException(
288 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
289 )
290 else:
291 user_content = None
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530292 return user_content
293
tierno6486f742020-02-13 16:30:14 +0000294 def authenticate(self, credentials, token_info=None):
delacruzramoceb8baf2019-06-21 14:25:38 +0200295 """
tierno701018c2019-06-25 11:13:14 +0000296 Authenticate a user using username/password or previous token_info plus project; its creates a new token
delacruzramoceb8baf2019-06-21 14:25:38 +0200297
tierno6486f742020-02-13 16:30:14 +0000298 :param credentials: dictionary that contains:
299 username: name, id or None
300 password: password or None
301 project_id: name, id, or None. If None first found project will be used to get an scope token
302 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000303 :param token_info: previous token_info to obtain authorization
delacruzramoceb8baf2019-06-21 14:25:38 +0200304 :return: the scoped token info or raises an exception. The token is a dictionary with:
305 _id: token string id,
306 username: username,
307 project_id: scoped_token project_id,
308 project_name: scoped_token project_name,
309 expires: epoch time when it expires,
310 """
311
312 now = time()
313 user_content = None
tierno6486f742020-02-13 16:30:14 +0000314 user = credentials.get("username")
315 password = credentials.get("password")
316 project = credentials.get("project_id")
jeganbe1a3df2024-06-04 12:05:19 +0000317 otp_validation = credentials.get("otp")
delacruzramoceb8baf2019-06-21 14:25:38 +0200318
delacruzramo01b15d32019-07-02 14:37:47 +0200319 # Try using username/password
jeganbe1a3df2024-06-04 12:05:19 +0000320 if otp_validation:
321 user_content = self.validate_user(user, password=None, otp=otp_validation)
322 elif user:
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530323 user_content = self.validate_user(user, password)
delacruzramo01b15d32019-07-02 14:37:47 +0200324 if not user_content:
elumalai7802ff82023-04-24 20:38:32 +0530325 cef_event(
326 self.cef_logger,
327 {
328 "name": "User login",
329 "sourceUserName": user,
330 "message": "Invalid username/password Project={} Outcome=Failure".format(
331 project
332 ),
333 "severity": "3",
334 },
335 )
336 self.logger.exception("{}".format(self.cef_logger))
garciadeblas4568a372021-03-24 09:19:48 +0100337 raise AuthException(
338 "Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED
339 )
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530340 if not user_content.get("_admin", None):
garciadeblas4568a372021-03-24 09:19:48 +0100341 raise AuthException(
342 "No default project for this user.",
343 http_code=HTTPStatus.UNAUTHORIZED,
344 )
delacruzramo01b15d32019-07-02 14:37:47 +0200345 elif token_info:
garciadeblas4568a372021-03-24 09:19:48 +0100346 user_rows = self.db.get_list(
347 self.users_collection, {"username": token_info["username"]}
348 )
delacruzramo01b15d32019-07-02 14:37:47 +0200349 if user_rows:
350 user_content = user_rows[0]
delacruzramoceb8baf2019-06-21 14:25:38 +0200351 else:
delacruzramo01b15d32019-07-02 14:37:47 +0200352 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
353 else:
garciadeblas4568a372021-03-24 09:19:48 +0100354 raise AuthException(
355 "Provide credentials: username/password or Authorization Bearer token",
356 http_code=HTTPStatus.UNAUTHORIZED,
357 )
delacruzramoad682a52019-12-10 16:26:34 +0100358 # Delay upon second request within time window
garciadeblas4568a372021-03-24 09:19:48 +0100359 if (
360 now - user_content["_admin"].get("last_token_time", 0)
361 < self.token_time_window
362 ):
delacruzramoad682a52019-12-10 16:26:34 +0100363 sleep(self.token_delay)
364 # user_content["_admin"]["last_token_time"] = now
365 # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions
garciadeblas6d83f8f2023-06-19 22:34:49 +0200366 user_data = {
367 "_admin.last_token_time": now,
368 "_admin.retry_count": 0,
369 }
garciadeblas4568a372021-03-24 09:19:48 +0100370 self.db.set_one(
371 self.users_collection,
372 {"_id": user_content["_id"]},
garciadeblas6d83f8f2023-06-19 22:34:49 +0200373 user_data,
garciadeblas4568a372021-03-24 09:19:48 +0100374 )
delacruzramoad682a52019-12-10 16:26:34 +0100375
garciadeblas4568a372021-03-24 09:19:48 +0100376 token_id = "".join(
377 random_choice(
378 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
379 )
380 for _ in range(0, 32)
381 )
delacruzramoceb8baf2019-06-21 14:25:38 +0200382
delacruzramo01b15d32019-07-02 14:37:47 +0200383 # projects = user_content.get("projects", [])
384 prm_list = user_content.get("project_role_mappings", [])
delacruzramoceb8baf2019-06-21 14:25:38 +0200385
delacruzramo01b15d32019-07-02 14:37:47 +0200386 if not project:
387 project = prm_list[0]["project"] if prm_list else None
388 if not project:
garciadeblas4568a372021-03-24 09:19:48 +0100389 raise AuthException(
390 "can't find a default project for this user",
391 http_code=HTTPStatus.UNAUTHORIZED,
392 )
tierno701018c2019-06-25 11:13:14 +0000393
delacruzramo01b15d32019-07-02 14:37:47 +0200394 projects = [prm["project"] for prm in prm_list]
delacruzramoceb8baf2019-06-21 14:25:38 +0200395
garciadeblas4568a372021-03-24 09:19:48 +0100396 proj = self.db.get_one(
397 self.projects_collection, {BaseTopic.id_field("projects", project): project}
398 )
delacruzramo01b15d32019-07-02 14:37:47 +0200399 project_name = proj["name"]
400 project_id = proj["_id"]
401 if project_name not in projects and project_id not in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100402 raise AuthException(
403 "project {} not allowed for this user".format(project),
404 http_code=HTTPStatus.UNAUTHORIZED,
405 )
tierno701018c2019-06-25 11:13:14 +0000406
delacruzramo01b15d32019-07-02 14:37:47 +0200407 # TODO remove admin, this vill be used by roles RBAC
408 if project_name == "admin":
409 token_admin = True
410 else:
411 token_admin = proj.get("admin", False)
delacruzramoceb8baf2019-06-21 14:25:38 +0200412
delacruzramo01b15d32019-07-02 14:37:47 +0200413 # add token roles
414 roles = []
415 roles_list = []
416 for prm in prm_list:
417 if prm["project"] in [project_id, project_name]:
garciadeblas4568a372021-03-24 09:19:48 +0100418 role = self.db.get_one(
419 self.roles_collection,
420 {BaseTopic.id_field("roles", prm["role"]): prm["role"]},
421 )
delacruzramo01b15d32019-07-02 14:37:47 +0200422 rid = role["_id"]
423 if rid not in roles:
424 rnm = role["name"]
425 roles.append(rid)
426 roles_list.append({"name": rnm, "id": rid})
427 if not roles_list:
garciadeblas4568a372021-03-24 09:19:48 +0100428 rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})[
429 "_id"
430 ]
delacruzramo01b15d32019-07-02 14:37:47 +0200431 roles_list = [{"name": "project_admin", "id": rid}]
delacruzramoceb8baf2019-06-21 14:25:38 +0200432
garciadeblas6d83f8f2023-06-19 22:34:49 +0200433 login_count = user_content.get("_admin").get("retry_count")
434 last_token_time = user_content.get("_admin").get("last_token_time")
435
436 admin_show = False
437 user_show = False
438 if self.config.get("user_management"):
439 for role in roles_list:
440 role_id = role.get("id")
441 permission = self.db.get_one(
442 self.roles_collection,
443 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
444 )
445 if permission.get("permissions")["admin"]:
446 if permission.get("permissions")["default"]:
447 admin_show = True
448 break
449 else:
450 user_show = True
garciadeblas4568a372021-03-24 09:19:48 +0100451 new_token = {
452 "issued_at": now,
453 "expires": now + 3600,
454 "_id": token_id,
455 "id": token_id,
456 "project_id": proj["_id"],
457 "project_name": proj["name"],
458 "username": user_content["username"],
459 "user_id": user_content["_id"],
460 "admin": token_admin,
461 "roles": roles_list,
garciadeblas6d83f8f2023-06-19 22:34:49 +0200462 "login_count": login_count,
463 "last_login": last_token_time,
464 "admin_show": admin_show,
465 "user_show": user_show,
garciadeblas4568a372021-03-24 09:19:48 +0100466 }
delacruzramoceb8baf2019-06-21 14:25:38 +0200467
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530468 self.db.create(self.tokens_collection, new_token)
delacruzramo01b15d32019-07-02 14:37:47 +0200469 return deepcopy(new_token)
470
471 def get_role_list(self, filter_q={}):
delacruzramoceb8baf2019-06-21 14:25:38 +0200472 """
473 Get role list.
474
475 :return: returns the list of roles.
476 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530477 return self.db.get_list(self.roles_collection, filter_q)
delacruzramoceb8baf2019-06-21 14:25:38 +0200478
delacruzramo01b15d32019-07-02 14:37:47 +0200479 def create_role(self, role_info):
delacruzramoceb8baf2019-06-21 14:25:38 +0200480 """
481 Create a role.
482
delacruzramo01b15d32019-07-02 14:37:47 +0200483 :param role_info: full role info.
484 :return: returns the role id.
delacruzramoceb8baf2019-06-21 14:25:38 +0200485 :raises AuthconnOperationException: if role creation failed.
486 """
delacruzramoceb8baf2019-06-21 14:25:38 +0200487 # TODO: Check that role name does not exist ?
delacruzramo01b15d32019-07-02 14:37:47 +0200488 rid = str(uuid4())
489 role_info["_id"] = rid
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530490 rid = self.db.create(self.roles_collection, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200491 return rid
delacruzramoceb8baf2019-06-21 14:25:38 +0200492
493 def delete_role(self, role_id):
494 """
495 Delete a role.
496
497 :param role_id: role identifier.
498 :raises AuthconnOperationException: if role deletion failed.
499 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530500 rc = self.db.del_one(self.roles_collection, {"_id": role_id})
501 self.db.del_list(self.tokens_collection, {"roles.id": role_id})
delacruzramoad682a52019-12-10 16:26:34 +0100502 return rc
delacruzramo01b15d32019-07-02 14:37:47 +0200503
504 def update_role(self, role_info):
505 """
506 Update a role.
507
508 :param role_info: full role info.
509 :return: returns the role name and id.
510 :raises AuthconnOperationException: if user creation failed.
511 """
512 rid = role_info["_id"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530513 self.db.set_one(self.roles_collection, {"_id": rid}, role_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200514 return {"_id": rid, "name": role_info["name"]}
515
516 def create_user(self, user_info):
517 """
518 Create a user.
519
520 :param user_info: full user info.
521 :return: returns the username and id of the user.
522 """
523 BaseTopic.format_on_new(user_info, make_public=False)
524 salt = uuid4().hex
525 user_info["_admin"]["salt"] = salt
garciadeblas6d83f8f2023-06-19 22:34:49 +0200526 user_info["_admin"]["user_status"] = "active"
selvi.ja9a1fc82022-04-04 06:54:30 +0000527 present = time()
528 if not user_info["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200529 if self.config.get("user_management"):
530 user_info["_admin"]["modified"] = present
531 user_info["_admin"]["password_expire_time"] = present
532 account_expire_time = present + 86400 * self.config.get(
533 "account_expire_days"
534 )
535 user_info["_admin"]["account_expire_time"] = account_expire_time
536
537 user_info["_admin"]["retry_count"] = 0
538 user_info["_admin"]["last_token_time"] = present
delacruzramo01b15d32019-07-02 14:37:47 +0200539 if "password" in user_info:
garciadeblas4568a372021-03-24 09:19:48 +0100540 user_info["password"] = sha256(
541 user_info["password"].encode("utf-8") + salt.encode("utf-8")
542 ).hexdigest()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200543 user_info["_admin"]["password_history"] = {salt: user_info["password"]}
delacruzramo01b15d32019-07-02 14:37:47 +0200544 # "projects" are not stored any more
545 if "projects" in user_info:
546 del user_info["projects"]
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530547 self.db.create(self.users_collection, user_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200548 return {"username": user_info["username"], "_id": user_info["_id"]}
549
550 def update_user(self, user_info):
551 """
552 Change the user name and/or password.
553
554 :param user_info: user info modifications
555 """
556 uid = user_info["_id"]
selvi.ja9a1fc82022-04-04 06:54:30 +0000557 old_pwd = user_info.get("old_password")
garciadeblas6d83f8f2023-06-19 22:34:49 +0200558 unlock = user_info.get("unlock")
559 renew = user_info.get("renew")
560 permission_id = user_info.get("system_admin_id")
Adurti0c9b0102023-11-08 11:16:32 +0000561 now = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200562
garciadeblas4568a372021-03-24 09:19:48 +0100563 user_data = self.db.get_one(
564 self.users_collection, {BaseTopic.id_field("users", uid): uid}
565 )
selvi.ja9a1fc82022-04-04 06:54:30 +0000566 if old_pwd:
567 salt = user_data["_admin"]["salt"]
garciadeblasf2af4a12023-01-24 16:56:54 +0100568 shadow_password = sha256(
569 old_pwd.encode("utf-8") + salt.encode("utf-8")
570 ).hexdigest()
selvi.ja9a1fc82022-04-04 06:54:30 +0000571 if shadow_password != user_data["password"]:
572 raise AuthconnConflictException(
garciadeblasf2af4a12023-01-24 16:56:54 +0100573 "Incorrect password", http_code=HTTPStatus.CONFLICT
selvi.ja9a1fc82022-04-04 06:54:30 +0000574 )
garciadeblas6d83f8f2023-06-19 22:34:49 +0200575 # Unlocking the user
576 if unlock:
577 system_user = None
578 unlock_state = False
579 if not permission_id:
580 raise AuthconnConflictException(
581 "system_admin_id is the required field to unlock the user",
582 http_code=HTTPStatus.CONFLICT,
583 )
584 else:
585 system_user = self.db.get_one(
586 self.users_collection,
587 {
588 BaseTopic.id_field(
589 self.users_collection, permission_id
590 ): permission_id
591 },
592 )
593 mapped_roles = system_user.get("project_role_mappings")
594 for role in mapped_roles:
595 role_id = role.get("role")
596 role_assigned = self.db.get_one(
597 self.roles_collection,
598 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
599 )
600 if role_assigned.get("permissions")["admin"]:
601 if role_assigned.get("permissions")["default"]:
602 user_data["_admin"]["retry_count"] = 0
Adurti0c9b0102023-11-08 11:16:32 +0000603 if now > user_data["_admin"]["account_expire_time"]:
604 user_data["_admin"]["user_status"] = "expired"
605 else:
606 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200607 unlock_state = True
608 break
609 if not unlock_state:
610 raise AuthconnConflictException(
611 "User '{}' does not have the privilege to unlock the user".format(
612 permission_id
613 ),
614 http_code=HTTPStatus.CONFLICT,
615 )
616 # Renewing the user
617 if renew:
618 system_user = None
619 renew_state = False
620 if not permission_id:
621 raise AuthconnConflictException(
622 "system_admin_id is the required field to renew the user",
623 http_code=HTTPStatus.CONFLICT,
624 )
625 else:
626 system_user = self.db.get_one(
627 self.users_collection,
628 {
629 BaseTopic.id_field(
630 self.users_collection, permission_id
631 ): permission_id
632 },
633 )
634 mapped_roles = system_user.get("project_role_mappings")
635 for role in mapped_roles:
636 role_id = role.get("role")
637 role_assigned = self.db.get_one(
638 self.roles_collection,
639 {BaseTopic.id_field(self.roles_collection, role_id): role_id},
640 )
641 if role_assigned.get("permissions")["admin"]:
642 if role_assigned.get("permissions")["default"]:
643 present = time()
644 account_expire = (
645 present + 86400 * self.config["account_expire_days"]
646 )
647 user_data["_admin"]["modified"] = present
648 user_data["_admin"]["account_expire_time"] = account_expire
Adurti0c9b0102023-11-08 11:16:32 +0000649 if (
650 user_data["_admin"]["retry_count"]
651 >= self.config["max_pwd_attempt"]
652 ):
653 user_data["_admin"]["user_status"] = "locked"
654 else:
655 user_data["_admin"]["user_status"] = "active"
garciadeblas6d83f8f2023-06-19 22:34:49 +0200656 renew_state = True
657 break
658 if not renew_state:
659 raise AuthconnConflictException(
660 "User '{}' does not have the privilege to renew the user".format(
661 permission_id
662 ),
663 http_code=HTTPStatus.CONFLICT,
664 )
delacruzramo01b15d32019-07-02 14:37:47 +0200665 BaseTopic.format_on_edit(user_data, user_info)
666 # User Name
667 usnm = user_info.get("username")
jeganbe1a3df2024-06-04 12:05:19 +0000668 email_id = user_info.get("email_id")
delacruzramo01b15d32019-07-02 14:37:47 +0200669 if usnm:
670 user_data["username"] = usnm
jeganbe1a3df2024-06-04 12:05:19 +0000671 if email_id:
672 user_data["email_id"] = email_id
delacruzramo01b15d32019-07-02 14:37:47 +0200673 # If password is given and is not already encripted
674 pswd = user_info.get("password")
garciadeblas4568a372021-03-24 09:19:48 +0100675 if pswd and (
676 len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd)
677 ): # TODO: Improve check?
elumalai7802ff82023-04-24 20:38:32 +0530678 cef_event(
679 self.cef_logger,
680 {
681 "name": "Change Password",
682 "sourceUserName": user_data["username"],
garciadeblasf53612b2024-07-12 14:44:37 +0200683 "message": "User {} changing Password for user {}, Outcome=Success".format(
684 user_info.get("session_user"), user_data["username"]
685 ),
elumalai7802ff82023-04-24 20:38:32 +0530686 "severity": "2",
687 },
688 )
689 self.logger.info("{}".format(self.cef_logger))
delacruzramo01b15d32019-07-02 14:37:47 +0200690 salt = uuid4().hex
691 if "_admin" not in user_data:
692 user_data["_admin"] = {}
garciadeblas6d83f8f2023-06-19 22:34:49 +0200693 if user_data.get("_admin").get("password_history"):
694 old_pwds = user_data.get("_admin").get("password_history")
695 else:
696 old_pwds = {}
697 for k, v in old_pwds.items():
698 shadow_password = sha256(
699 pswd.encode("utf-8") + k.encode("utf-8")
700 ).hexdigest()
701 if v == shadow_password:
702 raise AuthconnConflictException(
703 "Password is used before", http_code=HTTPStatus.CONFLICT
704 )
delacruzramo01b15d32019-07-02 14:37:47 +0200705 user_data["_admin"]["salt"] = salt
garciadeblas4568a372021-03-24 09:19:48 +0100706 user_data["password"] = sha256(
707 pswd.encode("utf-8") + salt.encode("utf-8")
708 ).hexdigest()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200709 if len(old_pwds) >= 3:
710 old_pwds.pop(list(old_pwds.keys())[0])
711 old_pwds.update({salt: user_data["password"]})
712 user_data["_admin"]["password_history"] = old_pwds
selvi.ja9a1fc82022-04-04 06:54:30 +0000713 if not user_data["username"] == "admin":
garciadeblas6d83f8f2023-06-19 22:34:49 +0200714 if self.config.get("user_management"):
selvi.ja9a1fc82022-04-04 06:54:30 +0000715 present = time()
garciadeblas6d83f8f2023-06-19 22:34:49 +0200716 if self.config.get("pwd_expire_days"):
717 expire = present + 86400 * self.config.get("pwd_expire_days")
718 user_data["_admin"]["modified"] = present
719 user_data["_admin"]["password_expire_time"] = expire
delacruzramo01b15d32019-07-02 14:37:47 +0200720 # Project-Role Mappings
721 # TODO: Check that user_info NEVER includes "project_role_mappings"
722 if "project_role_mappings" not in user_data:
723 user_data["project_role_mappings"] = []
724 for prm in user_info.get("add_project_role_mappings", []):
725 user_data["project_role_mappings"].append(prm)
726 for prm in user_info.get("remove_project_role_mappings", []):
727 for pidf in ["project", "project_name"]:
728 for ridf in ["role", "role_name"]:
729 try:
garciadeblas4568a372021-03-24 09:19:48 +0100730 user_data["project_role_mappings"].remove(
731 {"role": prm[ridf], "project": prm[pidf]}
732 )
delacruzramo01b15d32019-07-02 14:37:47 +0200733 except KeyError:
734 pass
735 except ValueError:
736 pass
delacruzramo3d6881c2019-12-04 13:42:26 +0100737 idf = BaseTopic.id_field("users", uid)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530738 self.db.set_one(self.users_collection, {idf: uid}, user_data)
delacruzramo3d6881c2019-12-04 13:42:26 +0100739 if user_info.get("remove_project_role_mappings"):
delacruzramoad682a52019-12-10 16:26:34 +0100740 idf = "user_id" if idf == "_id" else idf
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530741 self.db.del_list(self.tokens_collection, {idf: uid})
delacruzramo01b15d32019-07-02 14:37:47 +0200742
743 def delete_user(self, user_id):
744 """
745 Delete user.
746
747 :param user_id: user identifier.
748 :raises AuthconnOperationException: if user deletion failed.
749 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530750 self.db.del_one(self.users_collection, {"_id": user_id})
751 self.db.del_list(self.tokens_collection, {"user_id": user_id})
delacruzramoceb8baf2019-06-21 14:25:38 +0200752 return True
delacruzramo01b15d32019-07-02 14:37:47 +0200753
754 def get_user_list(self, filter_q=None):
755 """
756 Get user list.
757
tierno5ec768a2020-03-31 09:46:44 +0000758 :param filter_q: dictionary to filter user list by:
759 name (username is also admitted). If a user id is equal to the filter name, it is also provided
760 other
delacruzramo01b15d32019-07-02 14:37:47 +0200761 :return: returns a list of users.
762 """
763 filt = filter_q or {}
tierno5ec768a2020-03-31 09:46:44 +0000764 if "name" in filt: # backward compatibility
765 filt["username"] = filt.pop("name")
766 if filt.get("username") and is_valid_uuid(filt["username"]):
767 # username cannot be a uuid. If this is the case, change from username to _id
768 filt["_id"] = filt.pop("username")
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530769 users = self.db.get_list(self.users_collection, filt)
tierno1546f2a2019-08-20 15:38:11 +0000770 project_id_name = {}
771 role_id_name = {}
delacruzramo01b15d32019-07-02 14:37:47 +0200772 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000773 prms = user.get("project_role_mappings")
774 projects = user.get("projects")
775 if prms:
776 projects = []
777 # add project_name and role_name. Generate projects for backward compatibility
delacruzramo01b15d32019-07-02 14:37:47 +0200778 for prm in prms:
tierno1546f2a2019-08-20 15:38:11 +0000779 project_id = prm["project"]
780 if project_id not in project_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100781 pr = self.db.get_one(
782 self.projects_collection,
783 {BaseTopic.id_field("projects", project_id): project_id},
784 fail_on_empty=False,
785 )
tierno1546f2a2019-08-20 15:38:11 +0000786 project_id_name[project_id] = pr["name"] if pr else None
787 prm["project_name"] = project_id_name[project_id]
788 if prm["project_name"] not in projects:
789 projects.append(prm["project_name"])
790
791 role_id = prm["role"]
792 if role_id not in role_id_name:
garciadeblas4568a372021-03-24 09:19:48 +0100793 role = self.db.get_one(
794 self.roles_collection,
795 {BaseTopic.id_field("roles", role_id): role_id},
796 fail_on_empty=False,
797 )
tierno1546f2a2019-08-20 15:38:11 +0000798 role_id_name[role_id] = role["name"] if role else None
799 prm["role_name"] = role_id_name[role_id]
800 user["projects"] = projects # for backward compatibility
801 elif projects:
802 # user created with an old version. Create a project_role mapping with role project_admin
803 user["project_role_mappings"] = []
garciadeblas4568a372021-03-24 09:19:48 +0100804 role = self.db.get_one(
805 self.roles_collection,
806 {BaseTopic.id_field("roles", "project_admin"): "project_admin"},
807 )
tierno1546f2a2019-08-20 15:38:11 +0000808 for p_id_name in projects:
garciadeblas4568a372021-03-24 09:19:48 +0100809 pr = self.db.get_one(
810 self.projects_collection,
811 {BaseTopic.id_field("projects", p_id_name): p_id_name},
812 )
813 prm = {
814 "project": pr["_id"],
815 "project_name": pr["name"],
816 "role_name": "project_admin",
817 "role": role["_id"],
818 }
tierno1546f2a2019-08-20 15:38:11 +0000819 user["project_role_mappings"].append(prm)
820 else:
821 user["projects"] = []
822 user["project_role_mappings"] = []
823
delacruzramo01b15d32019-07-02 14:37:47 +0200824 return users
825
826 def get_project_list(self, filter_q={}):
827 """
828 Get role list.
829
830 :return: returns the list of projects.
831 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530832 return self.db.get_list(self.projects_collection, filter_q)
delacruzramo01b15d32019-07-02 14:37:47 +0200833
834 def create_project(self, project_info):
835 """
836 Create a project.
837
838 :param project: full project info.
839 :return: the internal id of the created project
840 :raises AuthconnOperationException: if project creation failed.
841 """
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530842 pid = self.db.create(self.projects_collection, project_info)
delacruzramo01b15d32019-07-02 14:37:47 +0200843 return pid
844
845 def delete_project(self, project_id):
846 """
847 Delete a project.
848
849 :param project_id: project identifier.
850 :raises AuthconnOperationException: if project deletion failed.
851 """
delacruzramoad682a52019-12-10 16:26:34 +0100852 idf = BaseTopic.id_field("projects", project_id)
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530853 r = self.db.del_one(self.projects_collection, {idf: project_id})
delacruzramoad682a52019-12-10 16:26:34 +0100854 idf = "project_id" if idf == "_id" else "project_name"
K Sai Kiran7ddb0732020-10-30 11:14:44 +0530855 self.db.del_list(self.tokens_collection, {idf: project_id})
delacruzramo01b15d32019-07-02 14:37:47 +0200856 return r
857
858 def update_project(self, project_id, project_info):
859 """
860 Change the name of a project
861
862 :param project_id: project to be changed
863 :param project_info: full project info
864 :return: None
865 :raises AuthconnOperationException: if project update failed.
866 """
garciadeblas4568a372021-03-24 09:19:48 +0100867 self.db.set_one(
868 self.projects_collection,
869 {BaseTopic.id_field("projects", project_id): project_id},
870 project_info,
871 )
jeganbe1a3df2024-06-04 12:05:19 +0000872
873 def generate_otp(self):
874 otp = "".join(random_choice("0123456789") for i in range(0, 4))
875 return otp
876
877 def send_email(self, indata):
878 user = indata.get("username")
879 user_rows = self.db.get_list(self.users_collection, {"username": user})
880 sender_password = None
881 otp_expiry_time = self.config.get("otp_expiry_time", 300)
882 if not re.match(email_schema["pattern"], indata.get("email_id")):
883 raise AuthException(
884 "Invalid email-id",
885 http_code=HTTPStatus.BAD_REQUEST,
886 )
887 if self.config.get("sender_email"):
888 sender_email = self.config["sender_email"]
889 else:
890 raise AuthException(
891 "sender_email not found",
892 http_code=HTTPStatus.NOT_FOUND,
893 )
894 if self.config.get("smtp_server"):
895 smtp_server = self.config["smtp_server"]
896 else:
897 raise AuthException(
898 "smtp server not found",
899 http_code=HTTPStatus.NOT_FOUND,
900 )
901 if self.config.get("smtp_port"):
902 smtp_port = self.config["smtp_port"]
903 else:
904 raise AuthException(
905 "smtp port not found",
906 http_code=HTTPStatus.NOT_FOUND,
907 )
908 sender_password = self.config.get("sender_password") or None
909 if user_rows:
910 user_data = user_rows[0]
911 user_status = user_data["_admin"]["user_status"]
912 if not user_data.get("project_role_mappings", None):
913 raise AuthException(
914 "can't find a default project for this user",
915 http_code=HTTPStatus.UNAUTHORIZED,
916 )
917 if user_status != "active" and user_status != "always-active":
918 raise AuthException(
919 f"User account is {user_status}.Please contact the system administrator.",
920 http_code=HTTPStatus.UNAUTHORIZED,
921 )
922 if user_data.get("email_id"):
923 if user_data["email_id"] == indata.get("email_id"):
924 otp = self.generate_otp()
925 encode_otp = (
926 sha256(
927 otp.encode("utf-8")
928 + user_data["_admin"]["salt"].encode("utf-8")
929 )
930 ).hexdigest()
931 otp_field = {encode_otp: time() + otp_expiry_time, "retries": 0}
932 user_data["OTP"] = otp_field
933 uid = user_data["_id"]
934 idf = BaseTopic.id_field("users", uid)
935 reciever_email = user_data["email_id"]
936 email_template_path = self.config.get("email_template")
937 with open(email_template_path, "r") as et:
938 email_template = et.read()
939 msg = EmailMessage()
940 msg = MIMEMultipart("alternative")
941 html_content = email_template.format(
942 username=user_data["username"],
943 otp=otp,
944 validity=otp_expiry_time // 60,
945 )
946 html = MIMEText(html_content, "html")
947 msg["Subject"] = "OSM password reset request"
948 msg.attach(html)
949 with smtplib.SMTP(smtp_server, smtp_port) as smtp:
950 smtp.starttls()
951 if sender_password:
952 smtp.login(sender_email, sender_password)
953 smtp.sendmail(sender_email, reciever_email, msg.as_string())
954 self.db.set_one(self.users_collection, {idf: uid}, user_data)
955 return {"email": "sent"}
956 else:
957 raise AuthException(
958 "No email id is registered for this user.Please contact the system administrator.",
959 http_code=HTTPStatus.NOT_FOUND,
960 )
961 else:
962 raise AuthException(
963 "user not found",
964 http_code=HTTPStatus.NOT_FOUND,
965 )
966
967 def validate_otp(self, indata):
968 otp = indata.get("otp")
969 user = indata.get("username")
970 user_rows = self.db.get_list(self.users_collection, {"username": user})
971 user_data = user_rows[0]
972 uid = user_data["_id"]
973 idf = BaseTopic.id_field("users", uid)
974 retry_count = self.config.get("retry_count", 3)
975 if user_data:
976 salt = user_data["_admin"]["salt"]
977 actual_otp = sha256(otp.encode("utf-8") + salt.encode("utf-8")).hexdigest()
978 if not user_data.get("OTP"):
979 otp_field = {"retries": 1}
980 user_data["OTP"] = otp_field
981 self.db.set_one(self.users_collection, {idf: uid}, user_data)
982 return {"retries": user_data["OTP"]["retries"]}
983 for key, value in user_data["OTP"].items():
984 curr_time = time()
985 if key == actual_otp and curr_time < value:
986 user_data["OTP"] = {}
987 self.db.set_one(self.users_collection, {idf: uid}, user_data)
988 return {"valid": "True", "password_change": "True"}
989 else:
990 user_data["OTP"]["retries"] += 1
991 self.db.set_one(self.users_collection, {idf: uid}, user_data)
992 if user_data["OTP"].get("retries") >= retry_count:
993 raise AuthException(
994 "Invalid OTP. Maximum retries exceeded",
995 http_code=HTTPStatus.TOO_MANY_REQUESTS,
996 )
997 return {"retry_count": user_data["OTP"]["retries"]}