Coverage for osm_nbi/authconn_internal.py: 27%

472 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-10 20:04 +0000

1# -*- 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 

27__author__ = ( 

28 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>, " 

29 "Alfonso Tierno <alfonso.tiernosepulveda@telefoncia.com" 

30) 

31__date__ = "$06-jun-2019 11:16:08$" 

32 

33import logging 

34import re 

35 

36from osm_nbi.authconn import ( 

37 Authconn, 

38 AuthException, 

39 AuthconnConflictException, 

40) # , AuthconnOperationException 

41from osm_common.dbbase import DbException 

42from osm_nbi.base_topic import BaseTopic 

43from osm_nbi.utils import cef_event, cef_event_builder 

44from osm_nbi.validation import is_valid_uuid, email_schema 

45from time import time, sleep 

46from http import HTTPStatus 

47from uuid import uuid4 

48from hashlib import sha256 

49from copy import deepcopy 

50from random import choice as random_choice 

51import smtplib 

52from email.message import EmailMessage 

53from email.mime.text import MIMEText 

54from email.mime.multipart import MIMEMultipart 

55 

56 

57class AuthconnInternal(Authconn): 

58 token_time_window = 2 # seconds 

59 token_delay = 1 # seconds to wait upon second request within time window 

60 

61 users_collection = "users" 

62 roles_collection = "roles" 

63 projects_collection = "projects" 

64 tokens_collection = "tokens" 

65 

66 def __init__(self, config, db, role_permissions): 

67 Authconn.__init__(self, config, db, role_permissions) 

68 self.logger = logging.getLogger("nbi.authenticator.internal") 

69 

70 self.db = db 

71 # self.msg = msg 

72 # self.token_cache = token_cache 

73 

74 # To be Confirmed 

75 self.sess = None 

76 self.cef_logger = cef_event_builder(config) 

77 

78 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: 

96 raise AuthException( 

97 "Needed a token or Authorization HTTP header", 

98 http_code=HTTPStatus.UNAUTHORIZED, 

99 ) 

100 

101 now = time() 

102 

103 # get from database if not in cache 

104 # if not token_info: 

105 token_info = self.db.get_one(self.tokens_collection, {"_id": token}) 

106 if token_info["expires"] < now: 

107 raise AuthException( 

108 "Expired Token or Authorization HTTP header", 

109 http_code=HTTPStatus.UNAUTHORIZED, 

110 ) 

111 

112 return token_info 

113 

114 except DbException as e: 

115 if e.http_code == HTTPStatus.NOT_FOUND: 

116 raise AuthException( 

117 "Invalid Token or Authorization HTTP header", 

118 http_code=HTTPStatus.UNAUTHORIZED, 

119 ) 

120 else: 

121 raise 

122 except AuthException: 

123 raise 

124 except Exception: 

125 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 ) 

132 

133 def revoke_token(self, token): 

134 """ 

135 Invalidate a token. 

136 

137 :param token: token to be revoked 

138 """ 

139 try: 

140 # self.token_cache.pop(token, None) 

141 self.db.del_one(self.tokens_collection, {"_id": token}) 

142 return True 

143 except DbException as e: 

144 if e.http_code == HTTPStatus.NOT_FOUND: 

145 raise AuthException( 

146 "Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND 

147 ) 

148 else: 

149 # raise 

150 exmsg = "Error during token revocation using internal backend" 

151 self.logger.exception(exmsg) 

152 raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED) 

153 

154 def validate_user(self, user, password, otp=None): 

155 """ 

156 Validate username and password via appropriate backend. 

157 :param user: username of the user. 

158 :param password: password to be validated. 

159 """ 

160 user_rows = self.db.get_list( 

161 self.users_collection, {BaseTopic.id_field("users", user): user} 

162 ) 

163 now = time() 

164 user_content = None 

165 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 ) 

253 if otp: 

254 return user_content 

255 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 

292 return user_content 

293 

294 def authenticate(self, credentials, token_info=None): 

295 """ 

296 Authenticate a user using username/password or previous token_info plus project; its creates a new token 

297 

298 :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 

303 :param token_info: previous token_info to obtain authorization 

304 :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 

314 user = credentials.get("username") 

315 password = credentials.get("password") 

316 project = credentials.get("project_id") 

317 otp_validation = credentials.get("otp") 

318 

319 # Try using username/password 

320 if otp_validation: 

321 user_content = self.validate_user(user, password=None, otp=otp_validation) 

322 elif user: 

323 user_content = self.validate_user(user, password) 

324 if not user_content: 

325 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)) 

337 raise AuthException( 

338 "Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED 

339 ) 

340 if not user_content.get("_admin", None): 

341 raise AuthException( 

342 "No default project for this user.", 

343 http_code=HTTPStatus.UNAUTHORIZED, 

344 ) 

345 elif token_info: 

346 user_rows = self.db.get_list( 

347 self.users_collection, {"username": token_info["username"]} 

348 ) 

349 if user_rows: 

350 user_content = user_rows[0] 

351 else: 

352 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED) 

353 else: 

354 raise AuthException( 

355 "Provide credentials: username/password or Authorization Bearer token", 

356 http_code=HTTPStatus.UNAUTHORIZED, 

357 ) 

358 # Delay upon second request within time window 

359 if ( 

360 now - user_content["_admin"].get("last_token_time", 0) 

361 < self.token_time_window 

362 ): 

363 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 

366 user_data = { 

367 "_admin.last_token_time": now, 

368 "_admin.retry_count": 0, 

369 } 

370 self.db.set_one( 

371 self.users_collection, 

372 {"_id": user_content["_id"]}, 

373 user_data, 

374 ) 

375 

376 token_id = "".join( 

377 random_choice( 

378 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 

379 ) 

380 for _ in range(0, 32) 

381 ) 

382 

383 # projects = user_content.get("projects", []) 

384 prm_list = user_content.get("project_role_mappings", []) 

385 

386 if not project: 

387 project = prm_list[0]["project"] if prm_list else None 

388 if not project: 

389 raise AuthException( 

390 "can't find a default project for this user", 

391 http_code=HTTPStatus.UNAUTHORIZED, 

392 ) 

393 

394 projects = [prm["project"] for prm in prm_list] 

395 

396 proj = self.db.get_one( 

397 self.projects_collection, {BaseTopic.id_field("projects", project): project} 

398 ) 

399 project_name = proj["name"] 

400 project_id = proj["_id"] 

401 if project_name not in projects and project_id not in projects: 

402 raise AuthException( 

403 "project {} not allowed for this user".format(project), 

404 http_code=HTTPStatus.UNAUTHORIZED, 

405 ) 

406 

407 # 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) 

412 

413 # add token roles 

414 roles = [] 

415 roles_list = [] 

416 for prm in prm_list: 

417 if prm["project"] in [project_id, project_name]: 

418 role = self.db.get_one( 

419 self.roles_collection, 

420 {BaseTopic.id_field("roles", prm["role"]): prm["role"]}, 

421 ) 

422 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: 

428 rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})[ 

429 "_id" 

430 ] 

431 roles_list = [{"name": "project_admin", "id": rid}] 

432 

433 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 

451 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, 

462 "login_count": login_count, 

463 "last_login": last_token_time, 

464 "admin_show": admin_show, 

465 "user_show": user_show, 

466 } 

467 

468 self.db.create(self.tokens_collection, new_token) 

469 return deepcopy(new_token) 

470 

471 def get_role_list(self, filter_q={}): 

472 """ 

473 Get role list. 

474 

475 :return: returns the list of roles. 

476 """ 

477 return self.db.get_list(self.roles_collection, filter_q) 

478 

479 def create_role(self, role_info): 

480 """ 

481 Create a role. 

482 

483 :param role_info: full role info. 

484 :return: returns the role id. 

485 :raises AuthconnOperationException: if role creation failed. 

486 """ 

487 # TODO: Check that role name does not exist ? 

488 rid = str(uuid4()) 

489 role_info["_id"] = rid 

490 rid = self.db.create(self.roles_collection, role_info) 

491 return rid 

492 

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 """ 

500 rc = self.db.del_one(self.roles_collection, {"_id": role_id}) 

501 self.db.del_list(self.tokens_collection, {"roles.id": role_id}) 

502 return rc 

503 

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"] 

513 self.db.set_one(self.roles_collection, {"_id": rid}, role_info) 

514 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 

526 user_info["_admin"]["user_status"] = "active" 

527 present = time() 

528 if not user_info["username"] == "admin": 

529 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 

539 if "password" in user_info: 

540 user_info["password"] = sha256( 

541 user_info["password"].encode("utf-8") + salt.encode("utf-8") 

542 ).hexdigest() 

543 user_info["_admin"]["password_history"] = {salt: user_info["password"]} 

544 # "projects" are not stored any more 

545 if "projects" in user_info: 

546 del user_info["projects"] 

547 self.db.create(self.users_collection, user_info) 

548 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"] 

557 old_pwd = user_info.get("old_password") 

558 unlock = user_info.get("unlock") 

559 renew = user_info.get("renew") 

560 permission_id = user_info.get("system_admin_id") 

561 now = time() 

562 

563 user_data = self.db.get_one( 

564 self.users_collection, {BaseTopic.id_field("users", uid): uid} 

565 ) 

566 if old_pwd: 

567 salt = user_data["_admin"]["salt"] 

568 shadow_password = sha256( 

569 old_pwd.encode("utf-8") + salt.encode("utf-8") 

570 ).hexdigest() 

571 if shadow_password != user_data["password"]: 

572 raise AuthconnConflictException( 

573 "Incorrect password", http_code=HTTPStatus.CONFLICT 

574 ) 

575 # 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 

603 if now > user_data["_admin"]["account_expire_time"]: 

604 user_data["_admin"]["user_status"] = "expired" 

605 else: 

606 user_data["_admin"]["user_status"] = "active" 

607 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 

649 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" 

656 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 ) 

665 BaseTopic.format_on_edit(user_data, user_info) 

666 # User Name 

667 usnm = user_info.get("username") 

668 email_id = user_info.get("email_id") 

669 if usnm: 

670 user_data["username"] = usnm 

671 if email_id: 

672 user_data["email_id"] = email_id 

673 # If password is given and is not already encripted 

674 pswd = user_info.get("password") 

675 if pswd and ( 

676 len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd) 

677 ): # TODO: Improve check? 

678 cef_event( 

679 self.cef_logger, 

680 { 

681 "name": "Change Password", 

682 "sourceUserName": user_data["username"], 

683 "message": "User {} changing Password for user {}, Outcome=Success".format( 

684 user_info.get("session_user"), user_data["username"] 

685 ), 

686 "severity": "2", 

687 }, 

688 ) 

689 self.logger.info("{}".format(self.cef_logger)) 

690 salt = uuid4().hex 

691 if "_admin" not in user_data: 

692 user_data["_admin"] = {} 

693 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 ) 

705 user_data["_admin"]["salt"] = salt 

706 user_data["password"] = sha256( 

707 pswd.encode("utf-8") + salt.encode("utf-8") 

708 ).hexdigest() 

709 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 

713 if not user_data["username"] == "admin": 

714 if self.config.get("user_management"): 

715 present = time() 

716 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 

720 # 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: 

730 user_data["project_role_mappings"].remove( 

731 {"role": prm[ridf], "project": prm[pidf]} 

732 ) 

733 except KeyError: 

734 pass 

735 except ValueError: 

736 pass 

737 idf = BaseTopic.id_field("users", uid) 

738 self.db.set_one(self.users_collection, {idf: uid}, user_data) 

739 if user_info.get("remove_project_role_mappings"): 

740 idf = "user_id" if idf == "_id" else idf 

741 if not user_data.get("project_role_mappings") or user_info.get( 

742 "remove_session_project" 

743 ): 

744 self.db.del_list(self.tokens_collection, {idf: uid}) 

745 

746 def delete_user(self, user_id): 

747 """ 

748 Delete user. 

749 

750 :param user_id: user identifier. 

751 :raises AuthconnOperationException: if user deletion failed. 

752 """ 

753 self.db.del_one(self.users_collection, {"_id": user_id}) 

754 self.db.del_list(self.tokens_collection, {"user_id": user_id}) 

755 return True 

756 

757 def get_user_list(self, filter_q=None): 

758 """ 

759 Get user list. 

760 

761 :param filter_q: dictionary to filter user list by: 

762 name (username is also admitted). If a user id is equal to the filter name, it is also provided 

763 other 

764 :return: returns a list of users. 

765 """ 

766 filt = filter_q or {} 

767 if "name" in filt: # backward compatibility 

768 filt["username"] = filt.pop("name") 

769 if filt.get("username") and is_valid_uuid(filt["username"]): 

770 # username cannot be a uuid. If this is the case, change from username to _id 

771 filt["_id"] = filt.pop("username") 

772 users = self.db.get_list(self.users_collection, filt) 

773 project_id_name = {} 

774 role_id_name = {} 

775 for user in users: 

776 prms = user.get("project_role_mappings") 

777 projects = user.get("projects") 

778 if prms: 

779 projects = [] 

780 # add project_name and role_name. Generate projects for backward compatibility 

781 for prm in prms: 

782 project_id = prm["project"] 

783 if project_id not in project_id_name: 

784 pr = self.db.get_one( 

785 self.projects_collection, 

786 {BaseTopic.id_field("projects", project_id): project_id}, 

787 fail_on_empty=False, 

788 ) 

789 project_id_name[project_id] = pr["name"] if pr else None 

790 prm["project_name"] = project_id_name[project_id] 

791 if prm["project_name"] not in projects: 

792 projects.append(prm["project_name"]) 

793 

794 role_id = prm["role"] 

795 if role_id not in role_id_name: 

796 role = self.db.get_one( 

797 self.roles_collection, 

798 {BaseTopic.id_field("roles", role_id): role_id}, 

799 fail_on_empty=False, 

800 ) 

801 role_id_name[role_id] = role["name"] if role else None 

802 prm["role_name"] = role_id_name[role_id] 

803 user["projects"] = projects # for backward compatibility 

804 elif projects: 

805 # user created with an old version. Create a project_role mapping with role project_admin 

806 user["project_role_mappings"] = [] 

807 role = self.db.get_one( 

808 self.roles_collection, 

809 {BaseTopic.id_field("roles", "project_admin"): "project_admin"}, 

810 ) 

811 for p_id_name in projects: 

812 pr = self.db.get_one( 

813 self.projects_collection, 

814 {BaseTopic.id_field("projects", p_id_name): p_id_name}, 

815 ) 

816 prm = { 

817 "project": pr["_id"], 

818 "project_name": pr["name"], 

819 "role_name": "project_admin", 

820 "role": role["_id"], 

821 } 

822 user["project_role_mappings"].append(prm) 

823 else: 

824 user["projects"] = [] 

825 user["project_role_mappings"] = [] 

826 

827 return users 

828 

829 def get_project_list(self, filter_q={}): 

830 """ 

831 Get role list. 

832 

833 :return: returns the list of projects. 

834 """ 

835 return self.db.get_list(self.projects_collection, filter_q) 

836 

837 def create_project(self, project_info): 

838 """ 

839 Create a project. 

840 

841 :param project: full project info. 

842 :return: the internal id of the created project 

843 :raises AuthconnOperationException: if project creation failed. 

844 """ 

845 pid = self.db.create(self.projects_collection, project_info) 

846 return pid 

847 

848 def delete_project(self, project_id): 

849 """ 

850 Delete a project. 

851 

852 :param project_id: project identifier. 

853 :raises AuthconnOperationException: if project deletion failed. 

854 """ 

855 idf = BaseTopic.id_field("projects", project_id) 

856 r = self.db.del_one(self.projects_collection, {idf: project_id}) 

857 idf = "project_id" if idf == "_id" else "project_name" 

858 self.db.del_list(self.tokens_collection, {idf: project_id}) 

859 return r 

860 

861 def update_project(self, project_id, project_info): 

862 """ 

863 Change the name of a project 

864 

865 :param project_id: project to be changed 

866 :param project_info: full project info 

867 :return: None 

868 :raises AuthconnOperationException: if project update failed. 

869 """ 

870 self.db.set_one( 

871 self.projects_collection, 

872 {BaseTopic.id_field("projects", project_id): project_id}, 

873 project_info, 

874 ) 

875 

876 def generate_otp(self): 

877 otp = "".join(random_choice("0123456789") for i in range(0, 4)) 

878 return otp 

879 

880 def send_email(self, indata): 

881 user = indata.get("username") 

882 user_rows = self.db.get_list(self.users_collection, {"username": user}) 

883 sender_password = None 

884 otp_expiry_time = self.config.get("otp_expiry_time", 300) 

885 if not re.match(email_schema["pattern"], indata.get("email_id")): 

886 raise AuthException( 

887 "Invalid email-id", 

888 http_code=HTTPStatus.BAD_REQUEST, 

889 ) 

890 if self.config.get("sender_email"): 

891 sender_email = self.config["sender_email"] 

892 else: 

893 raise AuthException( 

894 "sender_email not found", 

895 http_code=HTTPStatus.NOT_FOUND, 

896 ) 

897 if self.config.get("smtp_server"): 

898 smtp_server = self.config["smtp_server"] 

899 else: 

900 raise AuthException( 

901 "smtp server not found", 

902 http_code=HTTPStatus.NOT_FOUND, 

903 ) 

904 if self.config.get("smtp_port"): 

905 smtp_port = self.config["smtp_port"] 

906 else: 

907 raise AuthException( 

908 "smtp port not found", 

909 http_code=HTTPStatus.NOT_FOUND, 

910 ) 

911 sender_password = self.config.get("sender_password") or None 

912 if user_rows: 

913 user_data = user_rows[0] 

914 user_status = user_data["_admin"]["user_status"] 

915 if not user_data.get("project_role_mappings", None): 

916 raise AuthException( 

917 "can't find a default project for this user", 

918 http_code=HTTPStatus.UNAUTHORIZED, 

919 ) 

920 if user_status != "active" and user_status != "always-active": 

921 raise AuthException( 

922 f"User account is {user_status}.Please contact the system administrator.", 

923 http_code=HTTPStatus.UNAUTHORIZED, 

924 ) 

925 if user_data.get("email_id"): 

926 if user_data["email_id"] == indata.get("email_id"): 

927 otp = self.generate_otp() 

928 encode_otp = ( 

929 sha256( 

930 otp.encode("utf-8") 

931 + user_data["_admin"]["salt"].encode("utf-8") 

932 ) 

933 ).hexdigest() 

934 otp_field = {encode_otp: time() + otp_expiry_time, "retries": 0} 

935 user_data["OTP"] = otp_field 

936 uid = user_data["_id"] 

937 idf = BaseTopic.id_field("users", uid) 

938 reciever_email = user_data["email_id"] 

939 email_template_path = self.config.get("email_template") 

940 with open(email_template_path, "r") as et: 

941 email_template = et.read() 

942 msg = EmailMessage() 

943 msg = MIMEMultipart("alternative") 

944 html_content = email_template.format( 

945 username=user_data["username"], 

946 otp=otp, 

947 validity=otp_expiry_time // 60, 

948 ) 

949 html = MIMEText(html_content, "html") 

950 msg["Subject"] = "OSM password reset request" 

951 msg.attach(html) 

952 with smtplib.SMTP(smtp_server, smtp_port) as smtp: 

953 smtp.starttls() 

954 if sender_password: 

955 smtp.login(sender_email, sender_password) 

956 smtp.sendmail(sender_email, reciever_email, msg.as_string()) 

957 self.db.set_one(self.users_collection, {idf: uid}, user_data) 

958 return {"email": "sent"} 

959 else: 

960 raise AuthException( 

961 "No email id is registered for this user.Please contact the system administrator.", 

962 http_code=HTTPStatus.NOT_FOUND, 

963 ) 

964 else: 

965 raise AuthException( 

966 "user not found", 

967 http_code=HTTPStatus.NOT_FOUND, 

968 ) 

969 

970 def validate_otp(self, indata): 

971 otp = indata.get("otp") 

972 user = indata.get("username") 

973 user_rows = self.db.get_list(self.users_collection, {"username": user}) 

974 user_data = user_rows[0] 

975 uid = user_data["_id"] 

976 idf = BaseTopic.id_field("users", uid) 

977 retry_count = self.config.get("retry_count", 3) 

978 if user_data: 

979 salt = user_data["_admin"]["salt"] 

980 actual_otp = sha256(otp.encode("utf-8") + salt.encode("utf-8")).hexdigest() 

981 if not user_data.get("OTP"): 

982 otp_field = {"retries": 1} 

983 user_data["OTP"] = otp_field 

984 self.db.set_one(self.users_collection, {idf: uid}, user_data) 

985 return {"retries": user_data["OTP"]["retries"]} 

986 for key, value in user_data["OTP"].items(): 

987 curr_time = time() 

988 if key == actual_otp and curr_time < value: 

989 user_data["OTP"] = {} 

990 self.db.set_one(self.users_collection, {idf: uid}, user_data) 

991 return {"valid": "True", "password_change": "True"} 

992 else: 

993 user_data["OTP"]["retries"] += 1 

994 self.db.set_one(self.users_collection, {idf: uid}, user_data) 

995 if user_data["OTP"].get("retries") >= retry_count: 

996 raise AuthException( 

997 "Invalid OTP. Maximum retries exceeded", 

998 http_code=HTTPStatus.TOO_MANY_REQUESTS, 

999 ) 

1000 return {"retry_count": user_data["OTP"]["retries"]}