Coverage for osm_nbi/authconn_internal.py: 32%

374 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2024-06-30 10:14 +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 

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 

51 

52 

53class AuthconnInternal(Authconn): 

54 token_time_window = 2 # seconds 

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

56 

57 users_collection = "users" 

58 roles_collection = "roles" 

59 projects_collection = "projects" 

60 tokens_collection = "tokens" 

61 

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

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

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

65 

66 self.db = db 

67 # self.msg = msg 

68 # self.token_cache = token_cache 

69 

70 # To be Confirmed 

71 self.sess = None 

72 self.cef_logger = cef_event_builder(config) 

73 

74 def validate_token(self, token): 

75 """ 

76 Check if the token is valid. 

77 

78 :param token: token to validate 

79 :return: dictionary with information associated with the token: 

80 "_id": token id 

81 "project_id": project id 

82 "project_name": project name 

83 "user_id": user id 

84 "username": user name 

85 "roles": list with dict containing {name, id} 

86 "expires": expiration date 

87 If the token is not valid an exception is raised. 

88 """ 

89 

90 try: 

91 if not token: 

92 raise AuthException( 

93 "Needed a token or Authorization HTTP header", 

94 http_code=HTTPStatus.UNAUTHORIZED, 

95 ) 

96 

97 now = time() 

98 

99 # get from database if not in cache 

100 # if not token_info: 

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

102 if token_info["expires"] < now: 

103 raise AuthException( 

104 "Expired Token or Authorization HTTP header", 

105 http_code=HTTPStatus.UNAUTHORIZED, 

106 ) 

107 

108 return token_info 

109 

110 except DbException as e: 

111 if e.http_code == HTTPStatus.NOT_FOUND: 

112 raise AuthException( 

113 "Invalid Token or Authorization HTTP header", 

114 http_code=HTTPStatus.UNAUTHORIZED, 

115 ) 

116 else: 

117 raise 

118 except AuthException: 

119 raise 

120 except Exception: 

121 self.logger.exception( 

122 "Error during token validation using internal backend" 

123 ) 

124 raise AuthException( 

125 "Error during token validation using internal backend", 

126 http_code=HTTPStatus.UNAUTHORIZED, 

127 ) 

128 

129 def revoke_token(self, token): 

130 """ 

131 Invalidate a token. 

132 

133 :param token: token to be revoked 

134 """ 

135 try: 

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

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

138 return True 

139 except DbException as e: 

140 if e.http_code == HTTPStatus.NOT_FOUND: 

141 raise AuthException( 

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

143 ) 

144 else: 

145 # raise 

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

147 self.logger.exception(exmsg) 

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

149 

150 def validate_user(self, user, password): 

151 """ 

152 Validate username and password via appropriate backend. 

153 :param user: username of the user. 

154 :param password: password to be validated. 

155 """ 

156 user_rows = self.db.get_list( 

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

158 ) 

159 now = time() 

160 user_content = None 

161 if user: 

162 user_rows = self.db.get_list( 

163 self.users_collection, 

164 {BaseTopic.id_field(self.users_collection, user): user}, 

165 ) 

166 if user_rows: 

167 user_content = user_rows[0] 

168 # Updating user_status for every system_admin id role login 

169 mapped_roles = user_content.get("project_role_mappings") 

170 for role in mapped_roles: 

171 role_id = role.get("role") 

172 role_assigned = self.db.get_one( 

173 self.roles_collection, 

174 {BaseTopic.id_field(self.roles_collection, role_id): role_id}, 

175 ) 

176 

177 if role_assigned.get("permissions")["admin"]: 

178 if role_assigned.get("permissions")["default"]: 

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

180 filt = {} 

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

182 for user_info in users: 

183 if not user_info.get("username") == "admin": 

184 if not user_info.get("_admin").get( 

185 "account_expire_time" 

186 ): 

187 expire = now + 86400 * self.config.get( 

188 "account_expire_days" 

189 ) 

190 self.db.set_one( 

191 self.users_collection, 

192 {"_id": user_info["_id"]}, 

193 {"_admin.account_expire_time": expire}, 

194 ) 

195 else: 

196 if now > user_info.get("_admin").get( 

197 "account_expire_time" 

198 ): 

199 self.db.set_one( 

200 self.users_collection, 

201 {"_id": user_info["_id"]}, 

202 {"_admin.user_status": "expired"}, 

203 ) 

204 break 

205 

206 # To add "admin" user_status key while upgrading osm setup with feature enabled 

207 if user_content.get("username") == "admin": 

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

209 self.db.set_one( 

210 self.users_collection, 

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

212 {"_admin.user_status": "always-active"}, 

213 ) 

214 

215 if not user_content.get("username") == "admin": 

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

217 if not user_content.get("_admin").get("account_expire_time"): 

218 account_expire_time = now + 86400 * self.config.get( 

219 "account_expire_days" 

220 ) 

221 self.db.set_one( 

222 self.users_collection, 

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

224 {"_admin.account_expire_time": account_expire_time}, 

225 ) 

226 else: 

227 account_expire_time = user_content.get("_admin").get( 

228 "account_expire_time" 

229 ) 

230 

231 if now > account_expire_time: 

232 self.db.set_one( 

233 self.users_collection, 

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

235 {"_admin.user_status": "expired"}, 

236 ) 

237 raise AuthException( 

238 "Account expired", http_code=HTTPStatus.UNAUTHORIZED 

239 ) 

240 

241 if user_content.get("_admin").get("user_status") == "locked": 

242 raise AuthException( 

243 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS" 

244 ) 

245 elif user_content.get("_admin").get("user_status") == "expired": 

246 raise AuthException( 

247 "Failed to login as the account is expired" 

248 ) 

249 

250 salt = user_content["_admin"]["salt"] 

251 shadow_password = sha256( 

252 password.encode("utf-8") + salt.encode("utf-8") 

253 ).hexdigest() 

254 if shadow_password != user_content["password"]: 

255 count = 1 

256 if user_content.get("_admin").get("retry_count") >= 0: 

257 count += user_content.get("_admin").get("retry_count") 

258 self.db.set_one( 

259 self.users_collection, 

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

261 {"_admin.retry_count": count}, 

262 ) 

263 self.logger.debug( 

264 "Failed Authentications count: {}".format(count) 

265 ) 

266 

267 if user_content.get("username") == "admin": 

268 user_content = None 

269 else: 

270 if not self.config.get("user_management"): 

271 user_content = None 

272 else: 

273 if ( 

274 user_content.get("_admin").get("retry_count") 

275 >= self.config["max_pwd_attempt"] - 1 

276 ): 

277 self.db.set_one( 

278 self.users_collection, 

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

280 {"_admin.user_status": "locked"}, 

281 ) 

282 raise AuthException( 

283 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS" 

284 ) 

285 else: 

286 user_content = None 

287 return user_content 

288 

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

290 """ 

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

292 

293 :param credentials: dictionary that contains: 

294 username: name, id or None 

295 password: password or None 

296 project_id: name, id, or None. If None first found project will be used to get an scope token 

297 other items are allowed and ignored 

298 :param token_info: previous token_info to obtain authorization 

299 :return: the scoped token info or raises an exception. The token is a dictionary with: 

300 _id: token string id, 

301 username: username, 

302 project_id: scoped_token project_id, 

303 project_name: scoped_token project_name, 

304 expires: epoch time when it expires, 

305 """ 

306 

307 now = time() 

308 user_content = None 

309 user = credentials.get("username") 

310 password = credentials.get("password") 

311 project = credentials.get("project_id") 

312 

313 # Try using username/password 

314 if user: 

315 user_content = self.validate_user(user, password) 

316 if not user_content: 

317 cef_event( 

318 self.cef_logger, 

319 { 

320 "name": "User login", 

321 "sourceUserName": user, 

322 "message": "Invalid username/password Project={} Outcome=Failure".format( 

323 project 

324 ), 

325 "severity": "3", 

326 }, 

327 ) 

328 self.logger.exception("{}".format(self.cef_logger)) 

329 raise AuthException( 

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

331 ) 

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

333 raise AuthException( 

334 "No default project for this user.", 

335 http_code=HTTPStatus.UNAUTHORIZED, 

336 ) 

337 elif token_info: 

338 user_rows = self.db.get_list( 

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

340 ) 

341 if user_rows: 

342 user_content = user_rows[0] 

343 else: 

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

345 else: 

346 raise AuthException( 

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

348 http_code=HTTPStatus.UNAUTHORIZED, 

349 ) 

350 # Delay upon second request within time window 

351 if ( 

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

353 < self.token_time_window 

354 ): 

355 sleep(self.token_delay) 

356 # user_content["_admin"]["last_token_time"] = now 

357 # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions 

358 user_data = { 

359 "_admin.last_token_time": now, 

360 "_admin.retry_count": 0, 

361 } 

362 self.db.set_one( 

363 self.users_collection, 

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

365 user_data, 

366 ) 

367 

368 token_id = "".join( 

369 random_choice( 

370 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 

371 ) 

372 for _ in range(0, 32) 

373 ) 

374 

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

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

377 

378 if not project: 

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

380 if not project: 

381 raise AuthException( 

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

383 http_code=HTTPStatus.UNAUTHORIZED, 

384 ) 

385 

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

387 

388 proj = self.db.get_one( 

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

390 ) 

391 project_name = proj["name"] 

392 project_id = proj["_id"] 

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

394 raise AuthException( 

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

396 http_code=HTTPStatus.UNAUTHORIZED, 

397 ) 

398 

399 # TODO remove admin, this vill be used by roles RBAC 

400 if project_name == "admin": 

401 token_admin = True 

402 else: 

403 token_admin = proj.get("admin", False) 

404 

405 # add token roles 

406 roles = [] 

407 roles_list = [] 

408 for prm in prm_list: 

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

410 role = self.db.get_one( 

411 self.roles_collection, 

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

413 ) 

414 rid = role["_id"] 

415 if rid not in roles: 

416 rnm = role["name"] 

417 roles.append(rid) 

418 roles_list.append({"name": rnm, "id": rid}) 

419 if not roles_list: 

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

421 "_id" 

422 ] 

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

424 

425 login_count = user_content.get("_admin").get("retry_count") 

426 last_token_time = user_content.get("_admin").get("last_token_time") 

427 

428 admin_show = False 

429 user_show = False 

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

431 for role in roles_list: 

432 role_id = role.get("id") 

433 permission = self.db.get_one( 

434 self.roles_collection, 

435 {BaseTopic.id_field(self.roles_collection, role_id): role_id}, 

436 ) 

437 if permission.get("permissions")["admin"]: 

438 if permission.get("permissions")["default"]: 

439 admin_show = True 

440 break 

441 else: 

442 user_show = True 

443 new_token = { 

444 "issued_at": now, 

445 "expires": now + 3600, 

446 "_id": token_id, 

447 "id": token_id, 

448 "project_id": proj["_id"], 

449 "project_name": proj["name"], 

450 "username": user_content["username"], 

451 "user_id": user_content["_id"], 

452 "admin": token_admin, 

453 "roles": roles_list, 

454 "login_count": login_count, 

455 "last_login": last_token_time, 

456 "admin_show": admin_show, 

457 "user_show": user_show, 

458 } 

459 

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

461 return deepcopy(new_token) 

462 

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

464 """ 

465 Get role list. 

466 

467 :return: returns the list of roles. 

468 """ 

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

470 

471 def create_role(self, role_info): 

472 """ 

473 Create a role. 

474 

475 :param role_info: full role info. 

476 :return: returns the role id. 

477 :raises AuthconnOperationException: if role creation failed. 

478 """ 

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

480 rid = str(uuid4()) 

481 role_info["_id"] = rid 

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

483 return rid 

484 

485 def delete_role(self, role_id): 

486 """ 

487 Delete a role. 

488 

489 :param role_id: role identifier. 

490 :raises AuthconnOperationException: if role deletion failed. 

491 """ 

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

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

494 return rc 

495 

496 def update_role(self, role_info): 

497 """ 

498 Update a role. 

499 

500 :param role_info: full role info. 

501 :return: returns the role name and id. 

502 :raises AuthconnOperationException: if user creation failed. 

503 """ 

504 rid = role_info["_id"] 

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

506 return {"_id": rid, "name": role_info["name"]} 

507 

508 def create_user(self, user_info): 

509 """ 

510 Create a user. 

511 

512 :param user_info: full user info. 

513 :return: returns the username and id of the user. 

514 """ 

515 BaseTopic.format_on_new(user_info, make_public=False) 

516 salt = uuid4().hex 

517 user_info["_admin"]["salt"] = salt 

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

519 present = time() 

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

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

522 user_info["_admin"]["modified"] = present 

523 user_info["_admin"]["password_expire_time"] = present 

524 account_expire_time = present + 86400 * self.config.get( 

525 "account_expire_days" 

526 ) 

527 user_info["_admin"]["account_expire_time"] = account_expire_time 

528 

529 user_info["_admin"]["retry_count"] = 0 

530 user_info["_admin"]["last_token_time"] = present 

531 if "password" in user_info: 

532 user_info["password"] = sha256( 

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

534 ).hexdigest() 

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

536 # "projects" are not stored any more 

537 if "projects" in user_info: 

538 del user_info["projects"] 

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

540 return {"username": user_info["username"], "_id": user_info["_id"]} 

541 

542 def update_user(self, user_info): 

543 """ 

544 Change the user name and/or password. 

545 

546 :param user_info: user info modifications 

547 """ 

548 uid = user_info["_id"] 

549 old_pwd = user_info.get("old_password") 

550 unlock = user_info.get("unlock") 

551 renew = user_info.get("renew") 

552 permission_id = user_info.get("system_admin_id") 

553 

554 user_data = self.db.get_one( 

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

556 ) 

557 if old_pwd: 

558 salt = user_data["_admin"]["salt"] 

559 shadow_password = sha256( 

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

561 ).hexdigest() 

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

563 raise AuthconnConflictException( 

564 "Incorrect password", http_code=HTTPStatus.CONFLICT 

565 ) 

566 # Unlocking the user 

567 if unlock: 

568 system_user = None 

569 unlock_state = False 

570 if not permission_id: 

571 raise AuthconnConflictException( 

572 "system_admin_id is the required field to unlock the user", 

573 http_code=HTTPStatus.CONFLICT, 

574 ) 

575 else: 

576 system_user = self.db.get_one( 

577 self.users_collection, 

578 { 

579 BaseTopic.id_field( 

580 self.users_collection, permission_id 

581 ): permission_id 

582 }, 

583 ) 

584 mapped_roles = system_user.get("project_role_mappings") 

585 for role in mapped_roles: 

586 role_id = role.get("role") 

587 role_assigned = self.db.get_one( 

588 self.roles_collection, 

589 {BaseTopic.id_field(self.roles_collection, role_id): role_id}, 

590 ) 

591 if role_assigned.get("permissions")["admin"]: 

592 if role_assigned.get("permissions")["default"]: 

593 user_data["_admin"]["retry_count"] = 0 

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

595 unlock_state = True 

596 break 

597 if not unlock_state: 

598 raise AuthconnConflictException( 

599 "User '{}' does not have the privilege to unlock the user".format( 

600 permission_id 

601 ), 

602 http_code=HTTPStatus.CONFLICT, 

603 ) 

604 # Renewing the user 

605 if renew: 

606 system_user = None 

607 renew_state = False 

608 if not permission_id: 

609 raise AuthconnConflictException( 

610 "system_admin_id is the required field to renew the user", 

611 http_code=HTTPStatus.CONFLICT, 

612 ) 

613 else: 

614 system_user = self.db.get_one( 

615 self.users_collection, 

616 { 

617 BaseTopic.id_field( 

618 self.users_collection, permission_id 

619 ): permission_id 

620 }, 

621 ) 

622 mapped_roles = system_user.get("project_role_mappings") 

623 for role in mapped_roles: 

624 role_id = role.get("role") 

625 role_assigned = self.db.get_one( 

626 self.roles_collection, 

627 {BaseTopic.id_field(self.roles_collection, role_id): role_id}, 

628 ) 

629 if role_assigned.get("permissions")["admin"]: 

630 if role_assigned.get("permissions")["default"]: 

631 present = time() 

632 account_expire = ( 

633 present + 86400 * self.config["account_expire_days"] 

634 ) 

635 user_data["_admin"]["modified"] = present 

636 user_data["_admin"]["account_expire_time"] = account_expire 

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

638 renew_state = True 

639 break 

640 if not renew_state: 

641 raise AuthconnConflictException( 

642 "User '{}' does not have the privilege to renew the user".format( 

643 permission_id 

644 ), 

645 http_code=HTTPStatus.CONFLICT, 

646 ) 

647 BaseTopic.format_on_edit(user_data, user_info) 

648 # User Name 

649 usnm = user_info.get("username") 

650 if usnm: 

651 user_data["username"] = usnm 

652 # If password is given and is not already encripted 

653 pswd = user_info.get("password") 

654 if pswd and ( 

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

656 ): # TODO: Improve check? 

657 cef_event( 

658 self.cef_logger, 

659 { 

660 "name": "Change Password", 

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

662 "message": "Changing Password for user, Outcome=Success", 

663 "severity": "2", 

664 }, 

665 ) 

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

667 salt = uuid4().hex 

668 if "_admin" not in user_data: 

669 user_data["_admin"] = {} 

670 if user_data.get("_admin").get("password_history"): 

671 old_pwds = user_data.get("_admin").get("password_history") 

672 else: 

673 old_pwds = {} 

674 for k, v in old_pwds.items(): 

675 shadow_password = sha256( 

676 pswd.encode("utf-8") + k.encode("utf-8") 

677 ).hexdigest() 

678 if v == shadow_password: 

679 raise AuthconnConflictException( 

680 "Password is used before", http_code=HTTPStatus.CONFLICT 

681 ) 

682 user_data["_admin"]["salt"] = salt 

683 user_data["password"] = sha256( 

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

685 ).hexdigest() 

686 if len(old_pwds) >= 3: 

687 old_pwds.pop(list(old_pwds.keys())[0]) 

688 old_pwds.update({salt: user_data["password"]}) 

689 user_data["_admin"]["password_history"] = old_pwds 

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

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

692 present = time() 

693 if self.config.get("pwd_expire_days"): 

694 expire = present + 86400 * self.config.get("pwd_expire_days") 

695 user_data["_admin"]["modified"] = present 

696 user_data["_admin"]["password_expire_time"] = expire 

697 # Project-Role Mappings 

698 # TODO: Check that user_info NEVER includes "project_role_mappings" 

699 if "project_role_mappings" not in user_data: 

700 user_data["project_role_mappings"] = [] 

701 for prm in user_info.get("add_project_role_mappings", []): 

702 user_data["project_role_mappings"].append(prm) 

703 for prm in user_info.get("remove_project_role_mappings", []): 

704 for pidf in ["project", "project_name"]: 

705 for ridf in ["role", "role_name"]: 

706 try: 

707 user_data["project_role_mappings"].remove( 

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

709 ) 

710 except KeyError: 

711 pass 

712 except ValueError: 

713 pass 

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

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

716 if user_info.get("remove_project_role_mappings"): 

717 idf = "user_id" if idf == "_id" else idf 

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

719 

720 def delete_user(self, user_id): 

721 """ 

722 Delete user. 

723 

724 :param user_id: user identifier. 

725 :raises AuthconnOperationException: if user deletion failed. 

726 """ 

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

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

729 return True 

730 

731 def get_user_list(self, filter_q=None): 

732 """ 

733 Get user list. 

734 

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

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

737 other 

738 :return: returns a list of users. 

739 """ 

740 filt = filter_q or {} 

741 if "name" in filt: # backward compatibility 

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

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

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

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

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

747 project_id_name = {} 

748 role_id_name = {} 

749 for user in users: 

750 prms = user.get("project_role_mappings") 

751 projects = user.get("projects") 

752 if prms: 

753 projects = [] 

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

755 for prm in prms: 

756 project_id = prm["project"] 

757 if project_id not in project_id_name: 

758 pr = self.db.get_one( 

759 self.projects_collection, 

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

761 fail_on_empty=False, 

762 ) 

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

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

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

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

767 

768 role_id = prm["role"] 

769 if role_id not in role_id_name: 

770 role = self.db.get_one( 

771 self.roles_collection, 

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

773 fail_on_empty=False, 

774 ) 

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

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

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

778 elif projects: 

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

780 user["project_role_mappings"] = [] 

781 role = self.db.get_one( 

782 self.roles_collection, 

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

784 ) 

785 for p_id_name in projects: 

786 pr = self.db.get_one( 

787 self.projects_collection, 

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

789 ) 

790 prm = { 

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

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

793 "role_name": "project_admin", 

794 "role": role["_id"], 

795 } 

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

797 else: 

798 user["projects"] = [] 

799 user["project_role_mappings"] = [] 

800 

801 return users 

802 

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

804 """ 

805 Get role list. 

806 

807 :return: returns the list of projects. 

808 """ 

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

810 

811 def create_project(self, project_info): 

812 """ 

813 Create a project. 

814 

815 :param project: full project info. 

816 :return: the internal id of the created project 

817 :raises AuthconnOperationException: if project creation failed. 

818 """ 

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

820 return pid 

821 

822 def delete_project(self, project_id): 

823 """ 

824 Delete a project. 

825 

826 :param project_id: project identifier. 

827 :raises AuthconnOperationException: if project deletion failed. 

828 """ 

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

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

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

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

833 return r 

834 

835 def update_project(self, project_id, project_info): 

836 """ 

837 Change the name of a project 

838 

839 :param project_id: project to be changed 

840 :param project_info: full project info 

841 :return: None 

842 :raises AuthconnOperationException: if project update failed. 

843 """ 

844 self.db.set_one( 

845 self.projects_collection, 

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

847 project_info, 

848 )