Coverage for osm_nbi/admin_topics.py: 68%

801 statements  

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

1# -*- coding: utf-8 -*- 

2 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 

12# implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15 

16# import logging 

17from uuid import uuid4 

18from hashlib import sha256 

19from http import HTTPStatus 

20from time import time 

21from osm_nbi.validation import ( 

22 user_new_schema, 

23 user_edit_schema, 

24 project_new_schema, 

25 project_edit_schema, 

26 vim_account_new_schema, 

27 vim_account_edit_schema, 

28 sdn_new_schema, 

29 sdn_edit_schema, 

30 wim_account_new_schema, 

31 wim_account_edit_schema, 

32 roles_new_schema, 

33 roles_edit_schema, 

34 k8scluster_new_schema, 

35 k8scluster_edit_schema, 

36 k8srepo_new_schema, 

37 k8srepo_edit_schema, 

38 vca_new_schema, 

39 vca_edit_schema, 

40 osmrepo_new_schema, 

41 osmrepo_edit_schema, 

42 validate_input, 

43 ValidationError, 

44 is_valid_uuid, 

45) # To check that User/Project Names don't look like UUIDs 

46from osm_nbi.base_topic import BaseTopic, EngineException 

47from osm_nbi.authconn import AuthconnNotFoundException, AuthconnConflictException 

48from osm_common.dbbase import deep_update_rfc7396 

49import copy 

50 

51__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>" 

52 

53 

54class UserTopic(BaseTopic): 

55 topic = "users" 

56 topic_msg = "users" 

57 schema_new = user_new_schema 

58 schema_edit = user_edit_schema 

59 multiproject = False 

60 

61 def __init__(self, db, fs, msg, auth): 

62 BaseTopic.__init__(self, db, fs, msg, auth) 

63 

64 @staticmethod 

65 def _get_project_filter(session): 

66 """ 

67 Generates a filter dictionary for querying database users. 

68 Current policy is admin can show all, non admin, only its own user. 

69 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

70 :return: 

71 """ 

72 if session["admin"]: # allows all 

73 return {} 

74 else: 

75 return {"_id": session["user_id"]} 

76 

77 def check_conflict_on_new(self, session, indata): 

78 # check username not exists 

79 if self.db.get_one( 

80 self.topic, 

81 {"username": indata.get("username")}, 

82 fail_on_empty=False, 

83 fail_on_more=False, 

84 ): 

85 raise EngineException( 

86 "username '{}' exists".format(indata["username"]), HTTPStatus.CONFLICT 

87 ) 

88 # check projects 

89 if not session["force"]: 

90 for p in indata.get("projects") or []: 

91 # To allow project addressing by Name as well as ID 

92 if not self.db.get_one( 

93 "projects", 

94 {BaseTopic.id_field("projects", p): p}, 

95 fail_on_empty=False, 

96 fail_on_more=False, 

97 ): 

98 raise EngineException( 

99 "project '{}' does not exist".format(p), HTTPStatus.CONFLICT 

100 ) 

101 

102 def check_conflict_on_del(self, session, _id, db_content): 

103 """ 

104 Check if deletion can be done because of dependencies if it is not force. To override 

105 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

106 :param _id: internal _id 

107 :param db_content: The database content of this item _id 

108 :return: None if ok or raises EngineException with the conflict 

109 """ 

110 if _id == session["username"]: 

111 raise EngineException( 

112 "You cannot delete your own user", http_code=HTTPStatus.CONFLICT 

113 ) 

114 

115 @staticmethod 

116 def format_on_new(content, project_id=None, make_public=False): 

117 BaseTopic.format_on_new(content, make_public=False) 

118 # Removed so that the UUID is kept, to allow User Name modification 

119 # content["_id"] = content["username"] 

120 salt = uuid4().hex 

121 content["_admin"]["salt"] = salt 

122 if content.get("password"): 

123 content["password"] = sha256( 

124 content["password"].encode("utf-8") + salt.encode("utf-8") 

125 ).hexdigest() 

126 if content.get("project_role_mappings"): 

127 projects = [ 

128 mapping["project"] for mapping in content["project_role_mappings"] 

129 ] 

130 

131 if content.get("projects"): 

132 content["projects"] += projects 

133 else: 

134 content["projects"] = projects 

135 

136 @staticmethod 

137 def format_on_edit(final_content, edit_content): 

138 BaseTopic.format_on_edit(final_content, edit_content) 

139 if edit_content.get("password"): 

140 salt = uuid4().hex 

141 final_content["_admin"]["salt"] = salt 

142 final_content["password"] = sha256( 

143 edit_content["password"].encode("utf-8") + salt.encode("utf-8") 

144 ).hexdigest() 

145 return None 

146 

147 def edit(self, session, _id, indata=None, kwargs=None, content=None): 

148 if not session["admin"]: 

149 raise EngineException( 

150 "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED 

151 ) 

152 # Names that look like UUIDs are not allowed 

153 name = (indata if indata else kwargs).get("username") 

154 if is_valid_uuid(name): 

155 raise EngineException( 

156 "Usernames that look like UUIDs are not allowed", 

157 http_code=HTTPStatus.UNPROCESSABLE_ENTITY, 

158 ) 

159 return BaseTopic.edit( 

160 self, session, _id, indata=indata, kwargs=kwargs, content=content 

161 ) 

162 

163 def new(self, rollback, session, indata=None, kwargs=None, headers=None): 

164 if not session["admin"]: 

165 raise EngineException( 

166 "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED 

167 ) 

168 # Names that look like UUIDs are not allowed 

169 name = indata["username"] if indata else kwargs["username"] 

170 if is_valid_uuid(name): 

171 raise EngineException( 

172 "Usernames that look like UUIDs are not allowed", 

173 http_code=HTTPStatus.UNPROCESSABLE_ENTITY, 

174 ) 

175 return BaseTopic.new( 

176 self, rollback, session, indata=indata, kwargs=kwargs, headers=headers 

177 ) 

178 

179 

180class ProjectTopic(BaseTopic): 

181 topic = "projects" 

182 topic_msg = "projects" 

183 schema_new = project_new_schema 

184 schema_edit = project_edit_schema 

185 multiproject = False 

186 

187 def __init__(self, db, fs, msg, auth): 

188 BaseTopic.__init__(self, db, fs, msg, auth) 

189 

190 @staticmethod 

191 def _get_project_filter(session): 

192 """ 

193 Generates a filter dictionary for querying database users. 

194 Current policy is admin can show all, non admin, only its own user. 

195 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

196 :return: 

197 """ 

198 if session["admin"]: # allows all 

199 return {} 

200 else: 

201 return {"_id.cont": session["project_id"]} 

202 

203 def check_conflict_on_new(self, session, indata): 

204 if not indata.get("name"): 

205 raise EngineException("missing 'name'") 

206 # check name not exists 

207 if self.db.get_one( 

208 self.topic, 

209 {"name": indata.get("name")}, 

210 fail_on_empty=False, 

211 fail_on_more=False, 

212 ): 

213 raise EngineException( 

214 "name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT 

215 ) 

216 

217 @staticmethod 

218 def format_on_new(content, project_id=None, make_public=False): 

219 BaseTopic.format_on_new(content, None) 

220 # Removed so that the UUID is kept, to allow Project Name modification 

221 # content["_id"] = content["name"] 

222 

223 def check_conflict_on_del(self, session, _id, db_content): 

224 """ 

225 Check if deletion can be done because of dependencies if it is not force. To override 

226 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

227 :param _id: internal _id 

228 :param db_content: The database content of this item _id 

229 :return: None if ok or raises EngineException with the conflict 

230 """ 

231 if _id in session["project_id"]: 

232 raise EngineException( 

233 "You cannot delete your own project", http_code=HTTPStatus.CONFLICT 

234 ) 

235 if session["force"]: 

236 return 

237 _filter = {"projects": _id} 

238 if self.db.get_list("users", _filter): 

239 raise EngineException( 

240 "There is some USER that contains this project", 

241 http_code=HTTPStatus.CONFLICT, 

242 ) 

243 

244 def edit(self, session, _id, indata=None, kwargs=None, content=None): 

245 if not session["admin"]: 

246 raise EngineException( 

247 "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED 

248 ) 

249 # Names that look like UUIDs are not allowed 

250 name = (indata if indata else kwargs).get("name") 

251 if is_valid_uuid(name): 

252 raise EngineException( 

253 "Project names that look like UUIDs are not allowed", 

254 http_code=HTTPStatus.UNPROCESSABLE_ENTITY, 

255 ) 

256 return BaseTopic.edit( 

257 self, session, _id, indata=indata, kwargs=kwargs, content=content 

258 ) 

259 

260 def new(self, rollback, session, indata=None, kwargs=None, headers=None): 

261 if not session["admin"]: 

262 raise EngineException( 

263 "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED 

264 ) 

265 # Names that look like UUIDs are not allowed 

266 name = indata["name"] if indata else kwargs["name"] 

267 if is_valid_uuid(name): 

268 raise EngineException( 

269 "Project names that look like UUIDs are not allowed", 

270 http_code=HTTPStatus.UNPROCESSABLE_ENTITY, 

271 ) 

272 return BaseTopic.new( 

273 self, rollback, session, indata=indata, kwargs=kwargs, headers=headers 

274 ) 

275 

276 

277class CommonVimWimSdn(BaseTopic): 

278 """Common class for VIM, WIM SDN just to unify methods that are equal to all of them""" 

279 

280 config_to_encrypt = ( 

281 {} 

282 ) # what keys at config must be encrypted because contains passwords 

283 password_to_encrypt = "" # key that contains a password 

284 

285 @staticmethod 

286 def _create_operation(op_type, params=None): 

287 """ 

288 Creates a dictionary with the information to an operation, similar to ns-lcm-op 

289 :param op_type: can be create, edit, delete 

290 :param params: operation input parameters 

291 :return: new dictionary with 

292 """ 

293 now = time() 

294 return { 

295 "lcmOperationType": op_type, 

296 "operationState": "PROCESSING", 

297 "startTime": now, 

298 "statusEnteredTime": now, 

299 "detailed-status": "", 

300 "operationParams": params, 

301 } 

302 

303 def check_conflict_on_new(self, session, indata): 

304 """ 

305 Check that the data to be inserted is valid. It is checked that name is unique 

306 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

307 :param indata: data to be inserted 

308 :return: None or raises EngineException 

309 """ 

310 self.check_unique_name(session, indata["name"], _id=None) 

311 

312 def check_conflict_on_edit(self, session, final_content, edit_content, _id): 

313 """ 

314 Check that the data to be edited/uploaded is valid. It is checked that name is unique 

315 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

316 :param final_content: data once modified. This method may change it. 

317 :param edit_content: incremental data that contains the modifications to apply 

318 :param _id: internal _id 

319 :return: None or raises EngineException 

320 """ 

321 if not session["force"] and edit_content.get("name"): 

322 self.check_unique_name(session, edit_content["name"], _id=_id) 

323 

324 return final_content 

325 

326 def format_on_edit(self, final_content, edit_content): 

327 """ 

328 Modifies final_content inserting admin information upon edition 

329 :param final_content: final content to be stored at database 

330 :param edit_content: user requested update content 

331 :return: operation id 

332 """ 

333 super().format_on_edit(final_content, edit_content) 

334 

335 # encrypt passwords 

336 schema_version = final_content.get("schema_version") 

337 if schema_version: 

338 if edit_content.get(self.password_to_encrypt): 

339 final_content[self.password_to_encrypt] = self.db.encrypt( 

340 edit_content[self.password_to_encrypt], 

341 schema_version=schema_version, 

342 salt=final_content["_id"], 

343 ) 

344 config_to_encrypt_keys = self.config_to_encrypt.get( 

345 schema_version 

346 ) or self.config_to_encrypt.get("default") 

347 if edit_content.get("config") and config_to_encrypt_keys: 

348 for p in config_to_encrypt_keys: 

349 if edit_content["config"].get(p): 

350 final_content["config"][p] = self.db.encrypt( 

351 edit_content["config"][p], 

352 schema_version=schema_version, 

353 salt=final_content["_id"], 

354 ) 

355 if edit_content.get("config", {}).get("credentials"): 

356 cloud_credentials = edit_content["config"]["credentials"] 

357 if cloud_credentials.get("clientSecret"): 

358 edit_content["config"]["credentials"][ 

359 "clientSecret" 

360 ] = self.db.encrypt( 

361 edit_content["config"]["credentials"]["clientSecret"], 

362 schema_version=schema_version, 

363 salt=edit_content["_id"], 

364 ) 

365 elif cloud_credentials.get("SecretAccessKey"): 

366 edit_content["config"]["credentials"][ 

367 "SecretAccessKey" 

368 ] = self.db.encrypt( 

369 edit_content["config"]["credentials"]["SecretAccessKey"], 

370 schema_version=schema_version, 

371 salt=edit_content["_id"], 

372 ) 

373 

374 # create edit operation 

375 final_content["_admin"]["operations"].append(self._create_operation("edit")) 

376 return "{}:{}".format( 

377 final_content["_id"], len(final_content["_admin"]["operations"]) - 1 

378 ) 

379 

380 def format_on_new(self, content, project_id=None, make_public=False): 

381 """ 

382 Modifies content descriptor to include _admin and insert create operation 

383 :param content: descriptor to be modified 

384 :param project_id: if included, it add project read/write permissions. Can be None or a list 

385 :param make_public: if included it is generated as public for reading. 

386 :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified 

387 """ 

388 super().format_on_new(content, project_id=project_id, make_public=make_public) 

389 content["schema_version"] = schema_version = "1.11" 

390 content["key"] = "registered" 

391 

392 # encrypt passwords 

393 if content.get(self.password_to_encrypt): 

394 content[self.password_to_encrypt] = self.db.encrypt( 

395 content[self.password_to_encrypt], 

396 schema_version=schema_version, 

397 salt=content["_id"], 

398 ) 

399 config_to_encrypt_keys = self.config_to_encrypt.get( 

400 schema_version 

401 ) or self.config_to_encrypt.get("default") 

402 if content.get("config") and config_to_encrypt_keys: 

403 for p in config_to_encrypt_keys: 

404 if content["config"].get(p): 

405 content["config"][p] = self.db.encrypt( 

406 content["config"][p], 

407 schema_version=schema_version, 

408 salt=content["_id"], 

409 ) 

410 if content.get("config", {}).get("credentials"): 

411 cloud_credentials = content["config"]["credentials"] 

412 if cloud_credentials.get("clientSecret"): 

413 content["config"]["credentials"]["clientSecret"] = self.db.encrypt( 

414 content["config"]["credentials"]["clientSecret"], 

415 schema_version=schema_version, 

416 salt=content["_id"], 

417 ) 

418 elif cloud_credentials.get("SecretAccessKey"): 

419 content["config"]["credentials"]["SecretAccessKey"] = self.db.encrypt( 

420 content["config"]["credentials"]["SecretAccessKey"], 

421 schema_version=schema_version, 

422 salt=content["_id"], 

423 ) 

424 

425 content["_admin"]["operationalState"] = "PROCESSING" 

426 

427 # create operation 

428 content["_admin"]["operations"] = [self._create_operation("create")] 

429 content["_admin"]["current_operation"] = None 

430 # create Resource in Openstack based VIM 

431 if content.get("vim_type"): 

432 if content["vim_type"] == "openstack": 

433 compute = { 

434 "ram": {"total": None, "used": None}, 

435 "vcpus": {"total": None, "used": None}, 

436 "instances": {"total": None, "used": None}, 

437 } 

438 storage = { 

439 "volumes": {"total": None, "used": None}, 

440 "snapshots": {"total": None, "used": None}, 

441 "storage": {"total": None, "used": None}, 

442 } 

443 network = { 

444 "networks": {"total": None, "used": None}, 

445 "subnets": {"total": None, "used": None}, 

446 "floating_ips": {"total": None, "used": None}, 

447 } 

448 content["resources"] = { 

449 "compute": compute, 

450 "storage": storage, 

451 "network": network, 

452 } 

453 

454 return "{}:0".format(content["_id"]) 

455 

456 def delete(self, session, _id, dry_run=False, not_send_msg=None): 

457 """ 

458 Delete item by its internal _id 

459 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

460 :param _id: server internal id 

461 :param dry_run: make checking but do not delete 

462 :param not_send_msg: To not send message (False) or store content (list) instead 

463 :return: operation id if it is ordered to delete. None otherwise 

464 """ 

465 

466 filter_q = self._get_project_filter(session) 

467 filter_q["_id"] = _id 

468 db_content = self.db.get_one(self.topic, filter_q) 

469 

470 self.check_conflict_on_del(session, _id, db_content) 

471 if dry_run: 

472 return None 

473 

474 # remove reference from project_read if there are more projects referencing it. If it last one, 

475 # do not remove reference, but order via kafka to delete it 

476 if session["project_id"]: 

477 other_projects_referencing = next( 

478 ( 

479 p 

480 for p in db_content["_admin"]["projects_read"] 

481 if p not in session["project_id"] and p != "ANY" 

482 ), 

483 None, 

484 ) 

485 

486 # check if there are projects referencing it (apart from ANY, that means, public).... 

487 if other_projects_referencing: 

488 # remove references but not delete 

489 update_dict_pull = { 

490 "_admin.projects_read": session["project_id"], 

491 "_admin.projects_write": session["project_id"], 

492 } 

493 self.db.set_one( 

494 self.topic, filter_q, update_dict=None, pull_list=update_dict_pull 

495 ) 

496 return None 

497 else: 

498 can_write = next( 

499 ( 

500 p 

501 for p in db_content["_admin"]["projects_write"] 

502 if p == "ANY" or p in session["project_id"] 

503 ), 

504 None, 

505 ) 

506 if not can_write: 

507 raise EngineException( 

508 "You have not write permission to delete it", 

509 http_code=HTTPStatus.UNAUTHORIZED, 

510 ) 

511 

512 # It must be deleted 

513 if session["force"]: 

514 self.db.del_one(self.topic, {"_id": _id}) 

515 op_id = None 

516 self._send_msg( 

517 "deleted", {"_id": _id, "op_id": op_id}, not_send_msg=not_send_msg 

518 ) 

519 else: 

520 update_dict = {"_admin.to_delete": True} 

521 self.db.set_one( 

522 self.topic, 

523 {"_id": _id}, 

524 update_dict=update_dict, 

525 push={"_admin.operations": self._create_operation("delete")}, 

526 ) 

527 # the number of operations is the operation_id. db_content does not contains the new operation inserted, 

528 # so the -1 is not needed 

529 op_id = "{}:{}".format( 

530 db_content["_id"], len(db_content["_admin"]["operations"]) 

531 ) 

532 self._send_msg( 

533 "delete", {"_id": _id, "op_id": op_id}, not_send_msg=not_send_msg 

534 ) 

535 return op_id 

536 

537 

538class VimAccountTopic(CommonVimWimSdn): 

539 topic = "vim_accounts" 

540 topic_msg = "vim_account" 

541 schema_new = vim_account_new_schema 

542 schema_edit = vim_account_edit_schema 

543 multiproject = True 

544 password_to_encrypt = "vim_password" 

545 config_to_encrypt = { 

546 "1.1": ("admin_password", "nsx_password", "vcenter_password"), 

547 "default": ( 

548 "admin_password", 

549 "nsx_password", 

550 "vcenter_password", 

551 "vrops_password", 

552 ), 

553 } 

554 

555 def check_conflict_on_del(self, session, _id, db_content): 

556 """ 

557 Check if deletion can be done because of dependencies if it is not force. To override 

558 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

559 :param _id: internal _id 

560 :param db_content: The database content of this item _id 

561 :return: None if ok or raises EngineException with the conflict 

562 """ 

563 if session["force"]: 

564 return 

565 # check if used by VNF 

566 if self.db.get_list("vnfrs", {"vim-account-id": _id}): 

567 raise EngineException( 

568 "There is at least one VNF using this VIM account", 

569 http_code=HTTPStatus.CONFLICT, 

570 ) 

571 super().check_conflict_on_del(session, _id, db_content) 

572 

573 

574class WimAccountTopic(CommonVimWimSdn): 

575 topic = "wim_accounts" 

576 topic_msg = "wim_account" 

577 schema_new = wim_account_new_schema 

578 schema_edit = wim_account_edit_schema 

579 multiproject = True 

580 password_to_encrypt = "password" 

581 config_to_encrypt = {} 

582 

583 

584class SdnTopic(CommonVimWimSdn): 

585 topic = "sdns" 

586 topic_msg = "sdn" 

587 quota_name = "sdn_controllers" 

588 schema_new = sdn_new_schema 

589 schema_edit = sdn_edit_schema 

590 multiproject = True 

591 password_to_encrypt = "password" 

592 config_to_encrypt = {} 

593 

594 def _obtain_url(self, input, create): 

595 if input.get("ip") or input.get("port"): 

596 if not input.get("ip") or not input.get("port") or input.get("url"): 

597 raise ValidationError( 

598 "You must provide both 'ip' and 'port' (deprecated); or just 'url' (prefered)" 

599 ) 

600 input["url"] = "http://{}:{}/".format(input["ip"], input["port"]) 

601 del input["ip"] 

602 del input["port"] 

603 elif create and not input.get("url"): 

604 raise ValidationError("You must provide 'url'") 

605 return input 

606 

607 def _validate_input_new(self, input, force=False): 

608 input = super()._validate_input_new(input, force) 

609 return self._obtain_url(input, True) 

610 

611 def _validate_input_edit(self, input, content, force=False): 

612 input = super()._validate_input_edit(input, content, force) 

613 return self._obtain_url(input, False) 

614 

615 

616class K8sClusterTopic(CommonVimWimSdn): 

617 topic = "k8sclusters" 

618 topic_msg = "k8scluster" 

619 schema_new = k8scluster_new_schema 

620 schema_edit = k8scluster_edit_schema 

621 multiproject = True 

622 password_to_encrypt = None 

623 config_to_encrypt = {} 

624 

625 def format_on_new(self, content, project_id=None, make_public=False): 

626 oid = super().format_on_new(content, project_id, make_public) 

627 self.db.encrypt_decrypt_fields( 

628 content["credentials"], 

629 "encrypt", 

630 ["password", "secret"], 

631 schema_version=content["schema_version"], 

632 salt=content["_id"], 

633 ) 

634 # Add Helm/Juju Repo lists 

635 repos = {"helm-chart": [], "juju-bundle": []} 

636 for proj in content["_admin"]["projects_read"]: 

637 if proj != "ANY": 

638 for repo in self.db.get_list( 

639 "k8srepos", {"_admin.projects_read": proj} 

640 ): 

641 if repo["_id"] not in repos[repo["type"]]: 

642 repos[repo["type"]].append(repo["_id"]) 

643 for k in repos: 

644 content["_admin"][k.replace("-", "_") + "_repos"] = repos[k] 

645 return oid 

646 

647 def format_on_edit(self, final_content, edit_content): 

648 if final_content.get("schema_version") and edit_content.get("credentials"): 

649 self.db.encrypt_decrypt_fields( 

650 edit_content["credentials"], 

651 "encrypt", 

652 ["password", "secret"], 

653 schema_version=final_content["schema_version"], 

654 salt=final_content["_id"], 

655 ) 

656 deep_update_rfc7396( 

657 final_content["credentials"], edit_content["credentials"] 

658 ) 

659 oid = super().format_on_edit(final_content, edit_content) 

660 return oid 

661 

662 def check_conflict_on_edit(self, session, final_content, edit_content, _id): 

663 final_content = super(CommonVimWimSdn, self).check_conflict_on_edit( 

664 session, final_content, edit_content, _id 

665 ) 

666 final_content = super().check_conflict_on_edit( 

667 session, final_content, edit_content, _id 

668 ) 

669 # Update Helm/Juju Repo lists 

670 repos = {"helm-chart": [], "juju-bundle": []} 

671 for proj in session.get("set_project", []): 

672 if proj != "ANY": 

673 for repo in self.db.get_list( 

674 "k8srepos", {"_admin.projects_read": proj} 

675 ): 

676 if repo["_id"] not in repos[repo["type"]]: 

677 repos[repo["type"]].append(repo["_id"]) 

678 for k in repos: 

679 rlist = k.replace("-", "_") + "_repos" 

680 if rlist not in final_content["_admin"]: 

681 final_content["_admin"][rlist] = [] 

682 final_content["_admin"][rlist] += repos[k] 

683 return final_content 

684 

685 def check_conflict_on_del(self, session, _id, db_content): 

686 """ 

687 Check if deletion can be done because of dependencies if it is not force. To override 

688 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

689 :param _id: internal _id 

690 :param db_content: The database content of this item _id 

691 :return: None if ok or raises EngineException with the conflict 

692 """ 

693 if session["force"]: 

694 return 

695 # check if used by VNF 

696 filter_q = {"kdur.k8s-cluster.id": _id} 

697 if session["project_id"]: 

698 filter_q["_admin.projects_read.cont"] = session["project_id"] 

699 if self.db.get_list("vnfrs", filter_q): 

700 raise EngineException( 

701 "There is at least one VNF using this k8scluster", 

702 http_code=HTTPStatus.CONFLICT, 

703 ) 

704 super().check_conflict_on_del(session, _id, db_content) 

705 

706 

707class VcaTopic(CommonVimWimSdn): 

708 topic = "vca" 

709 topic_msg = "vca" 

710 schema_new = vca_new_schema 

711 schema_edit = vca_edit_schema 

712 multiproject = True 

713 password_to_encrypt = None 

714 

715 def format_on_new(self, content, project_id=None, make_public=False): 

716 oid = super().format_on_new(content, project_id, make_public) 

717 content["schema_version"] = schema_version = "1.11" 

718 for key in ["secret", "cacert"]: 

719 content[key] = self.db.encrypt( 

720 content[key], schema_version=schema_version, salt=content["_id"] 

721 ) 

722 return oid 

723 

724 def format_on_edit(self, final_content, edit_content): 

725 oid = super().format_on_edit(final_content, edit_content) 

726 schema_version = final_content.get("schema_version") 

727 for key in ["secret", "cacert"]: 

728 if key in edit_content: 

729 final_content[key] = self.db.encrypt( 

730 edit_content[key], 

731 schema_version=schema_version, 

732 salt=final_content["_id"], 

733 ) 

734 return oid 

735 

736 def check_conflict_on_del(self, session, _id, db_content): 

737 """ 

738 Check if deletion can be done because of dependencies if it is not force. To override 

739 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

740 :param _id: internal _id 

741 :param db_content: The database content of this item _id 

742 :return: None if ok or raises EngineException with the conflict 

743 """ 

744 if session["force"]: 

745 return 

746 # check if used by VNF 

747 filter_q = {"vca": _id} 

748 if session["project_id"]: 

749 filter_q["_admin.projects_read.cont"] = session["project_id"] 

750 if self.db.get_list("vim_accounts", filter_q): 

751 raise EngineException( 

752 "There is at least one VIM account using this vca", 

753 http_code=HTTPStatus.CONFLICT, 

754 ) 

755 super().check_conflict_on_del(session, _id, db_content) 

756 

757 

758class K8sRepoTopic(CommonVimWimSdn): 

759 topic = "k8srepos" 

760 topic_msg = "k8srepo" 

761 schema_new = k8srepo_new_schema 

762 schema_edit = k8srepo_edit_schema 

763 multiproject = True 

764 password_to_encrypt = None 

765 config_to_encrypt = {} 

766 

767 def format_on_new(self, content, project_id=None, make_public=False): 

768 oid = super().format_on_new(content, project_id, make_public) 

769 # Update Helm/Juju Repo lists 

770 repo_list = content["type"].replace("-", "_") + "_repos" 

771 for proj in content["_admin"]["projects_read"]: 

772 if proj != "ANY": 

773 self.db.set_list( 

774 "k8sclusters", 

775 { 

776 "_admin.projects_read": proj, 

777 "_admin." + repo_list + ".ne": content["_id"], 

778 }, 

779 {}, 

780 push={"_admin." + repo_list: content["_id"]}, 

781 ) 

782 return oid 

783 

784 def delete(self, session, _id, dry_run=False, not_send_msg=None): 

785 type = self.db.get_one("k8srepos", {"_id": _id})["type"] 

786 oid = super().delete(session, _id, dry_run, not_send_msg) 

787 if oid: 

788 # Remove from Helm/Juju Repo lists 

789 repo_list = type.replace("-", "_") + "_repos" 

790 self.db.set_list( 

791 "k8sclusters", 

792 {"_admin." + repo_list: _id}, 

793 {}, 

794 pull={"_admin." + repo_list: _id}, 

795 ) 

796 return oid 

797 

798 

799class OsmRepoTopic(BaseTopic): 

800 topic = "osmrepos" 

801 topic_msg = "osmrepos" 

802 schema_new = osmrepo_new_schema 

803 schema_edit = osmrepo_edit_schema 

804 multiproject = True 

805 # TODO: Implement user/password 

806 

807 

808class UserTopicAuth(UserTopic): 

809 # topic = "users" 

810 topic_msg = "users" 

811 schema_new = user_new_schema 

812 schema_edit = user_edit_schema 

813 

814 def __init__(self, db, fs, msg, auth): 

815 UserTopic.__init__(self, db, fs, msg, auth) 

816 # self.auth = auth 

817 

818 def check_conflict_on_new(self, session, indata): 

819 """ 

820 Check that the data to be inserted is valid 

821 

822 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

823 :param indata: data to be inserted 

824 :return: None or raises EngineException 

825 """ 

826 username = indata.get("username") 

827 if is_valid_uuid(username): 

828 raise EngineException( 

829 "username '{}' cannot have a uuid format".format(username), 

830 HTTPStatus.UNPROCESSABLE_ENTITY, 

831 ) 

832 

833 # Check that username is not used, regardless keystone already checks this 

834 if self.auth.get_user_list(filter_q={"name": username}): 

835 raise EngineException( 

836 "username '{}' is already used".format(username), HTTPStatus.CONFLICT 

837 ) 

838 

839 if "projects" in indata.keys(): 

840 # convert to new format project_role_mappings 

841 role = self.auth.get_role_list({"name": "project_admin"}) 

842 if not role: 

843 role = self.auth.get_role_list() 

844 if not role: 

845 raise AuthconnNotFoundException( 

846 "Can't find default role for user '{}'".format(username) 

847 ) 

848 rid = role[0]["_id"] 

849 if not indata.get("project_role_mappings"): 

850 indata["project_role_mappings"] = [] 

851 for project in indata["projects"]: 

852 pid = self.auth.get_project(project)["_id"] 

853 prm = {"project": pid, "role": rid} 

854 if prm not in indata["project_role_mappings"]: 

855 indata["project_role_mappings"].append(prm) 

856 # raise EngineException("Format invalid: the keyword 'projects' is not allowed for keystone authentication", 

857 # HTTPStatus.BAD_REQUEST) 

858 

859 def check_conflict_on_edit(self, session, final_content, edit_content, _id): 

860 """ 

861 Check that the data to be edited/uploaded is valid 

862 

863 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

864 :param final_content: data once modified 

865 :param edit_content: incremental data that contains the modifications to apply 

866 :param _id: internal _id 

867 :return: None or raises EngineException 

868 """ 

869 

870 if "username" in edit_content: 

871 username = edit_content.get("username") 

872 if is_valid_uuid(username): 

873 raise EngineException( 

874 "username '{}' cannot have an uuid format".format(username), 

875 HTTPStatus.UNPROCESSABLE_ENTITY, 

876 ) 

877 

878 # Check that username is not used, regardless keystone already checks this 

879 if self.auth.get_user_list(filter_q={"name": username}): 

880 raise EngineException( 

881 "username '{}' is already used".format(username), 

882 HTTPStatus.CONFLICT, 

883 ) 

884 

885 if final_content["username"] == "admin": 

886 for mapping in edit_content.get("remove_project_role_mappings", ()): 

887 if mapping["project"] == "admin" and mapping.get("role") in ( 

888 None, 

889 "system_admin", 

890 ): 

891 # TODO make this also available for project id and role id 

892 raise EngineException( 

893 "You cannot remove system_admin role from admin user", 

894 http_code=HTTPStatus.FORBIDDEN, 

895 ) 

896 

897 return final_content 

898 

899 def check_conflict_on_del(self, session, _id, db_content): 

900 """ 

901 Check if deletion can be done because of dependencies if it is not force. To override 

902 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

903 :param _id: internal _id 

904 :param db_content: The database content of this item _id 

905 :return: None if ok or raises EngineException with the conflict 

906 """ 

907 if db_content["_id"] == session["user_id"]: 

908 raise EngineException( 

909 "You cannot delete your own login user ", http_code=HTTPStatus.CONFLICT 

910 ) 

911 # TODO: Check that user is not logged in ? How? (Would require listing current tokens) 

912 

913 @staticmethod 

914 def format_on_show(content): 

915 """ 

916 Modifies the content of the role information to separate the role 

917 metadata from the role definition. 

918 """ 

919 project_role_mappings = [] 

920 

921 if "projects" in content: 

922 for project in content["projects"]: 

923 for role in project["roles"]: 

924 project_role_mappings.append( 

925 { 

926 "project": project["_id"], 

927 "project_name": project["name"], 

928 "role": role["_id"], 

929 "role_name": role["name"], 

930 } 

931 ) 

932 del content["projects"] 

933 content["project_role_mappings"] = project_role_mappings 

934 

935 return content 

936 

937 def new(self, rollback, session, indata=None, kwargs=None, headers=None): 

938 """ 

939 Creates a new entry into the authentication backend. 

940 

941 NOTE: Overrides BaseTopic functionality because it doesn't require access to database. 

942 

943 :param rollback: list to append created items at database in case a rollback may to be done 

944 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

945 :param indata: data to be inserted 

946 :param kwargs: used to override the indata descriptor 

947 :param headers: http request headers 

948 :return: _id: identity of the inserted data, operation _id (None) 

949 """ 

950 try: 

951 content = BaseTopic._remove_envelop(indata) 

952 

953 # Override descriptor with query string kwargs 

954 BaseTopic._update_input_with_kwargs(content, kwargs) 

955 content = self._validate_input_new(content, session["force"]) 

956 self.check_conflict_on_new(session, content) 

957 # self.format_on_new(content, session["project_id"], make_public=session["public"]) 

958 now = time() 

959 content["_admin"] = {"created": now, "modified": now} 

960 prms = [] 

961 for prm in content.get("project_role_mappings", []): 

962 proj = self.auth.get_project(prm["project"], not session["force"]) 

963 role = self.auth.get_role(prm["role"], not session["force"]) 

964 pid = proj["_id"] if proj else None 

965 rid = role["_id"] if role else None 

966 prl = {"project": pid, "role": rid} 

967 if prl not in prms: 

968 prms.append(prl) 

969 content["project_role_mappings"] = prms 

970 # _id = self.auth.create_user(content["username"], content["password"])["_id"] 

971 _id = self.auth.create_user(content)["_id"] 

972 

973 rollback.append({"topic": self.topic, "_id": _id}) 

974 # del content["password"] 

975 self._send_msg("created", content, not_send_msg=None) 

976 return _id, None 

977 except ValidationError as e: 

978 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

979 

980 def show(self, session, _id, filter_q=None, api_req=False): 

981 """ 

982 Get complete information on an topic 

983 

984 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

985 :param _id: server internal id or username 

986 :param filter_q: dict: query parameter 

987 :param api_req: True if this call is serving an external API request. False if serving internal request. 

988 :return: dictionary, raise exception if not found. 

989 """ 

990 # Allow _id to be a name or uuid 

991 filter_q = {"username": _id} 

992 # users = self.auth.get_user_list(filter_q) 

993 users = self.list(session, filter_q) # To allow default filtering (Bug 853) 

994 if len(users) == 1: 

995 return users[0] 

996 elif len(users) > 1: 

997 raise EngineException( 

998 "Too many users found for '{}'".format(_id), HTTPStatus.CONFLICT 

999 ) 

1000 else: 

1001 raise EngineException( 

1002 "User '{}' not found".format(_id), HTTPStatus.NOT_FOUND 

1003 ) 

1004 

1005 def edit(self, session, _id, indata=None, kwargs=None, content=None): 

1006 """ 

1007 Updates an user entry. 

1008 

1009 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1010 :param _id: 

1011 :param indata: data to be inserted 

1012 :param kwargs: used to override the indata descriptor 

1013 :param content: 

1014 :return: _id: identity of the inserted data. 

1015 """ 

1016 indata = self._remove_envelop(indata) 

1017 

1018 # Override descriptor with query string kwargs 

1019 if kwargs: 

1020 BaseTopic._update_input_with_kwargs(indata, kwargs) 

1021 try: 

1022 if not content: 

1023 content = self.show(session, _id) 

1024 

1025 indata = self._validate_input_edit(indata, content, force=session["force"]) 

1026 content = self.check_conflict_on_edit(session, content, indata, _id=_id) 

1027 # self.format_on_edit(content, indata) 

1028 

1029 if not ( 

1030 "password" in indata 

1031 or "username" in indata 

1032 or indata.get("remove_project_role_mappings") 

1033 or indata.get("add_project_role_mappings") 

1034 or indata.get("project_role_mappings") 

1035 or indata.get("projects") 

1036 or indata.get("add_projects") 

1037 or indata.get("unlock") 

1038 or indata.get("renew") 

1039 or indata.get("email_id") 

1040 ): 

1041 return _id 

1042 if indata.get("project_role_mappings") and ( 

1043 indata.get("remove_project_role_mappings") 

1044 or indata.get("add_project_role_mappings") 

1045 ): 

1046 raise EngineException( 

1047 "Option 'project_role_mappings' is incompatible with 'add_project_role_mappings" 

1048 "' or 'remove_project_role_mappings'", 

1049 http_code=HTTPStatus.BAD_REQUEST, 

1050 ) 

1051 

1052 if indata.get("projects") or indata.get("add_projects"): 

1053 role = self.auth.get_role_list({"name": "project_admin"}) 

1054 if not role: 

1055 role = self.auth.get_role_list() 

1056 if not role: 

1057 raise AuthconnNotFoundException( 

1058 "Can't find a default role for user '{}'".format( 

1059 content["username"] 

1060 ) 

1061 ) 

1062 rid = role[0]["_id"] 

1063 if "add_project_role_mappings" not in indata: 

1064 indata["add_project_role_mappings"] = [] 

1065 if "remove_project_role_mappings" not in indata: 

1066 indata["remove_project_role_mappings"] = [] 

1067 if isinstance(indata.get("projects"), dict): 

1068 # backward compatible 

1069 for k, v in indata["projects"].items(): 

1070 if k.startswith("$") and v is None: 

1071 indata["remove_project_role_mappings"].append( 

1072 {"project": k[1:]} 

1073 ) 

1074 elif k.startswith("$+"): 

1075 indata["add_project_role_mappings"].append( 

1076 {"project": v, "role": rid} 

1077 ) 

1078 del indata["projects"] 

1079 for proj in indata.get("projects", []) + indata.get("add_projects", []): 

1080 indata["add_project_role_mappings"].append( 

1081 {"project": proj, "role": rid} 

1082 ) 

1083 if ( 

1084 indata.get("remove_project_role_mappings") 

1085 or indata.get("add_project_role_mappings") 

1086 or indata.get("project_role_mappings") 

1087 ): 

1088 user_details = self.db.get_one("users", {"_id": session.get("user_id")}) 

1089 edit_role = False 

1090 for pr in user_details["project_role_mappings"]: 

1091 role_id = pr.get("role") 

1092 role_details = self.db.get_one("roles", {"_id": role_id}) 

1093 if role_details["permissions"].get("default"): 

1094 if "roles" not in role_details["permissions"] or role_details[ 

1095 "permissions" 

1096 ].get("roles"): 

1097 edit_role = True 

1098 elif role_details["permissions"].get("roles"): 

1099 edit_role = True 

1100 if not edit_role: 

1101 raise EngineException( 

1102 "User {} has no privileges to edit or delete project-role mappings".format( 

1103 session.get("username") 

1104 ), 

1105 http_code=HTTPStatus.UNPROCESSABLE_ENTITY, 

1106 ) 

1107 

1108 # check before deleting project-role 

1109 delete_session_project = False 

1110 if indata.get("remove_project_role_mappings"): 

1111 for pr in indata["remove_project_role_mappings"]: 

1112 project_name = pr.get("project") 

1113 project_details = self.db.get_one( 

1114 "projects", {"_id": session.get("project_id")[0]} 

1115 ) 

1116 if project_details["name"] == project_name: 

1117 delete_session_project = True 

1118 

1119 # password change 

1120 if indata.get("password"): 

1121 if not session.get("admin_show"): 

1122 if not indata.get("system_admin_id"): 

1123 if _id != session["user_id"]: 

1124 raise EngineException( 

1125 "You are not allowed to change other users password", 

1126 http_code=HTTPStatus.BAD_REQUEST, 

1127 ) 

1128 if not indata.get("old_password"): 

1129 raise EngineException( 

1130 "Password change requires old password or admin ID", 

1131 http_code=HTTPStatus.BAD_REQUEST, 

1132 ) 

1133 

1134 # username change 

1135 if indata.get("username"): 

1136 if not session.get("admin_show"): 

1137 if not indata.get("system_admin_id"): 

1138 if _id != session["user_id"]: 

1139 raise EngineException( 

1140 "You are not allowed to change other users username", 

1141 http_code=HTTPStatus.BAD_REQUEST, 

1142 ) 

1143 

1144 # user = self.show(session, _id) # Already in 'content' 

1145 original_mapping = content["project_role_mappings"] 

1146 

1147 mappings_to_add = [] 

1148 mappings_to_remove = [] 

1149 

1150 # remove 

1151 for to_remove in indata.get("remove_project_role_mappings", ()): 

1152 for mapping in original_mapping: 

1153 if to_remove["project"] in ( 

1154 mapping["project"], 

1155 mapping["project_name"], 

1156 ): 

1157 if not to_remove.get("role") or to_remove["role"] in ( 

1158 mapping["role"], 

1159 mapping["role_name"], 

1160 ): 

1161 mappings_to_remove.append(mapping) 

1162 if len(original_mapping) == 0 or len(mappings_to_remove) == 0: 

1163 pid = self.auth.get_project(to_remove["project"])["_id"] 

1164 if to_remove.get("role"): 

1165 rid = self.auth.get_role(to_remove["role"])["_id"] 

1166 

1167 raise AuthconnNotFoundException( 

1168 "User is not mapped with project '{}' or role '{}'".format( 

1169 to_remove["project"], to_remove["role"] 

1170 ) 

1171 ) 

1172 raise AuthconnNotFoundException( 

1173 "User is not mapped with project '{}'".format( 

1174 to_remove["project"] 

1175 ) 

1176 ) 

1177 

1178 # add 

1179 for to_add in indata.get("add_project_role_mappings", ()): 

1180 for mapping in original_mapping: 

1181 if to_add["project"] in ( 

1182 mapping["project"], 

1183 mapping["project_name"], 

1184 ) and to_add["role"] in ( 

1185 mapping["role"], 

1186 mapping["role_name"], 

1187 ): 

1188 if mapping in mappings_to_remove: # do not remove 

1189 mappings_to_remove.remove(mapping) 

1190 break # do not add, it is already at user 

1191 else: 

1192 pid = self.auth.get_project(to_add["project"])["_id"] 

1193 rid = self.auth.get_role(to_add["role"])["_id"] 

1194 mappings_to_add.append({"project": pid, "role": rid}) 

1195 

1196 # set 

1197 if indata.get("project_role_mappings"): 

1198 duplicates = [] 

1199 for pr in indata.get("project_role_mappings"): 

1200 if pr not in duplicates: 

1201 duplicates.append(pr) 

1202 if len(indata.get("project_role_mappings")) > len(duplicates): 

1203 raise EngineException( 

1204 "Project-role combination should not be repeated", 

1205 http_code=HTTPStatus.UNPROCESSABLE_ENTITY, 

1206 ) 

1207 for to_set in indata["project_role_mappings"]: 

1208 for mapping in original_mapping: 

1209 if to_set["project"] in ( 

1210 mapping["project"], 

1211 mapping["project_name"], 

1212 ) and to_set["role"] in ( 

1213 mapping["role"], 

1214 mapping["role_name"], 

1215 ): 

1216 if mapping in mappings_to_remove: # do not remove 

1217 mappings_to_remove.remove(mapping) 

1218 break # do not add, it is already at user 

1219 else: 

1220 pid = self.auth.get_project(to_set["project"])["_id"] 

1221 rid = self.auth.get_role(to_set["role"])["_id"] 

1222 mappings_to_add.append({"project": pid, "role": rid}) 

1223 for mapping in original_mapping: 

1224 for to_set in indata["project_role_mappings"]: 

1225 if to_set["project"] in ( 

1226 mapping["project"], 

1227 mapping["project_name"], 

1228 ) and to_set["role"] in ( 

1229 mapping["role"], 

1230 mapping["role_name"], 

1231 ): 

1232 break 

1233 else: 

1234 # delete 

1235 if mapping not in mappings_to_remove: # do not remove 

1236 mappings_to_remove.append(mapping) 

1237 

1238 self.auth.update_user( 

1239 { 

1240 "_id": _id, 

1241 "username": indata.get("username"), 

1242 "password": indata.get("password"), 

1243 "old_password": indata.get("old_password"), 

1244 "add_project_role_mappings": mappings_to_add, 

1245 "remove_project_role_mappings": mappings_to_remove, 

1246 "system_admin_id": indata.get("system_admin_id"), 

1247 "unlock": indata.get("unlock"), 

1248 "renew": indata.get("renew"), 

1249 "session_user": session.get("username"), 

1250 "email_id": indata.get("email_id"), 

1251 "remove_session_project": delete_session_project, 

1252 } 

1253 ) 

1254 data_to_send = {"_id": _id, "changes": indata} 

1255 self._send_msg("edited", data_to_send, not_send_msg=None) 

1256 

1257 # return _id 

1258 except ValidationError as e: 

1259 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

1260 

1261 def list(self, session, filter_q=None, api_req=False): 

1262 """ 

1263 Get a list of the topic that matches a filter 

1264 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1265 :param filter_q: filter of data to be applied 

1266 :param api_req: True if this call is serving an external API request. False if serving internal request. 

1267 :return: The list, it can be empty if no one match the filter. 

1268 """ 

1269 user_list = self.auth.get_user_list(filter_q) 

1270 if not session["allow_show_user_project_role"]: 

1271 # Bug 853 - Default filtering 

1272 user_list = [usr for usr in user_list if usr["_id"] == session["user_id"]] 

1273 return user_list 

1274 

1275 def delete(self, session, _id, dry_run=False, not_send_msg=None): 

1276 """ 

1277 Delete item by its internal _id 

1278 

1279 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1280 :param _id: server internal id 

1281 :param force: indicates if deletion must be forced in case of conflict 

1282 :param dry_run: make checking but do not delete 

1283 :param not_send_msg: To not send message (False) or store content (list) instead 

1284 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... 

1285 """ 

1286 # Allow _id to be a name or uuid 

1287 user = self.auth.get_user(_id) 

1288 uid = user["_id"] 

1289 self.check_conflict_on_del(session, uid, user) 

1290 if not dry_run: 

1291 v = self.auth.delete_user(uid) 

1292 self._send_msg("deleted", user, not_send_msg=not_send_msg) 

1293 return v 

1294 return None 

1295 

1296 

1297class ProjectTopicAuth(ProjectTopic): 

1298 # topic = "projects" 

1299 topic_msg = "project" 

1300 schema_new = project_new_schema 

1301 schema_edit = project_edit_schema 

1302 

1303 def __init__(self, db, fs, msg, auth): 

1304 ProjectTopic.__init__(self, db, fs, msg, auth) 

1305 # self.auth = auth 

1306 

1307 def check_conflict_on_new(self, session, indata): 

1308 """ 

1309 Check that the data to be inserted is valid 

1310 

1311 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1312 :param indata: data to be inserted 

1313 :return: None or raises EngineException 

1314 """ 

1315 project_name = indata.get("name") 

1316 if is_valid_uuid(project_name): 

1317 raise EngineException( 

1318 "project name '{}' cannot have an uuid format".format(project_name), 

1319 HTTPStatus.UNPROCESSABLE_ENTITY, 

1320 ) 

1321 

1322 project_list = self.auth.get_project_list(filter_q={"name": project_name}) 

1323 

1324 if project_list: 

1325 raise EngineException( 

1326 "project '{}' exists".format(project_name), HTTPStatus.CONFLICT 

1327 ) 

1328 

1329 def check_conflict_on_edit(self, session, final_content, edit_content, _id): 

1330 """ 

1331 Check that the data to be edited/uploaded is valid 

1332 

1333 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1334 :param final_content: data once modified 

1335 :param edit_content: incremental data that contains the modifications to apply 

1336 :param _id: internal _id 

1337 :return: None or raises EngineException 

1338 """ 

1339 

1340 project_name = edit_content.get("name") 

1341 if project_name != final_content["name"]: # It is a true renaming 

1342 if is_valid_uuid(project_name): 

1343 raise EngineException( 

1344 "project name '{}' cannot have an uuid format".format(project_name), 

1345 HTTPStatus.UNPROCESSABLE_ENTITY, 

1346 ) 

1347 

1348 if final_content["name"] == "admin": 

1349 raise EngineException( 

1350 "You cannot rename project 'admin'", http_code=HTTPStatus.CONFLICT 

1351 ) 

1352 

1353 # Check that project name is not used, regardless keystone already checks this 

1354 if project_name and self.auth.get_project_list( 

1355 filter_q={"name": project_name} 

1356 ): 

1357 raise EngineException( 

1358 "project '{}' is already used".format(project_name), 

1359 HTTPStatus.CONFLICT, 

1360 ) 

1361 return final_content 

1362 

1363 def check_conflict_on_del(self, session, _id, db_content): 

1364 """ 

1365 Check if deletion can be done because of dependencies if it is not force. To override 

1366 

1367 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1368 :param _id: internal _id 

1369 :param db_content: The database content of this item _id 

1370 :return: None if ok or raises EngineException with the conflict 

1371 """ 

1372 

1373 def check_rw_projects(topic, title, id_field): 

1374 for desc in self.db.get_list(topic): 

1375 if ( 

1376 _id 

1377 in desc["_admin"]["projects_read"] 

1378 + desc["_admin"]["projects_write"] 

1379 ): 

1380 raise EngineException( 

1381 "Project '{}' ({}) is being used by {} '{}'".format( 

1382 db_content["name"], _id, title, desc[id_field] 

1383 ), 

1384 HTTPStatus.CONFLICT, 

1385 ) 

1386 

1387 if _id in session["project_id"]: 

1388 raise EngineException( 

1389 "You cannot delete your own project", http_code=HTTPStatus.CONFLICT 

1390 ) 

1391 

1392 if db_content["name"] == "admin": 

1393 raise EngineException( 

1394 "You cannot delete project 'admin'", http_code=HTTPStatus.CONFLICT 

1395 ) 

1396 

1397 # If any user is using this project, raise CONFLICT exception 

1398 if not session["force"]: 

1399 for user in self.auth.get_user_list(): 

1400 for prm in user.get("project_role_mappings"): 

1401 if prm["project"] == _id: 

1402 raise EngineException( 

1403 "Project '{}' ({}) is being used by user '{}'".format( 

1404 db_content["name"], _id, user["username"] 

1405 ), 

1406 HTTPStatus.CONFLICT, 

1407 ) 

1408 

1409 # If any VNFD, NSD, NST, PDU, etc. is using this project, raise CONFLICT exception 

1410 if not session["force"]: 

1411 check_rw_projects("vnfds", "VNF Descriptor", "id") 

1412 check_rw_projects("nsds", "NS Descriptor", "id") 

1413 check_rw_projects("nsts", "NS Template", "id") 

1414 check_rw_projects("pdus", "PDU Descriptor", "name") 

1415 

1416 def new(self, rollback, session, indata=None, kwargs=None, headers=None): 

1417 """ 

1418 Creates a new entry into the authentication backend. 

1419 

1420 NOTE: Overrides BaseTopic functionality because it doesn't require access to database. 

1421 

1422 :param rollback: list to append created items at database in case a rollback may to be done 

1423 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1424 :param indata: data to be inserted 

1425 :param kwargs: used to override the indata descriptor 

1426 :param headers: http request headers 

1427 :return: _id: identity of the inserted data, operation _id (None) 

1428 """ 

1429 try: 

1430 content = BaseTopic._remove_envelop(indata) 

1431 

1432 # Override descriptor with query string kwargs 

1433 BaseTopic._update_input_with_kwargs(content, kwargs) 

1434 content = self._validate_input_new(content, session["force"]) 

1435 self.check_conflict_on_new(session, content) 

1436 self.format_on_new( 

1437 content, project_id=session["project_id"], make_public=session["public"] 

1438 ) 

1439 self.create_gitname(content, session) 

1440 _id = self.auth.create_project(content) 

1441 rollback.append({"topic": self.topic, "_id": _id}) 

1442 self._send_msg("created", content, not_send_msg=None) 

1443 return _id, None 

1444 except ValidationError as e: 

1445 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

1446 

1447 def show(self, session, _id, filter_q=None, api_req=False): 

1448 """ 

1449 Get complete information on an topic 

1450 

1451 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1452 :param _id: server internal id 

1453 :param filter_q: dict: query parameter 

1454 :param api_req: True if this call is serving an external API request. False if serving internal request. 

1455 :return: dictionary, raise exception if not found. 

1456 """ 

1457 # Allow _id to be a name or uuid 

1458 filter_q = {self.id_field(self.topic, _id): _id} 

1459 # projects = self.auth.get_project_list(filter_q=filter_q) 

1460 projects = self.list(session, filter_q) # To allow default filtering (Bug 853) 

1461 if len(projects) == 1: 

1462 return projects[0] 

1463 elif len(projects) > 1: 

1464 raise EngineException("Too many projects found", HTTPStatus.CONFLICT) 

1465 else: 

1466 raise EngineException("Project not found", HTTPStatus.NOT_FOUND) 

1467 

1468 def list(self, session, filter_q=None, api_req=False): 

1469 """ 

1470 Get a list of the topic that matches a filter 

1471 

1472 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1473 :param filter_q: filter of data to be applied 

1474 :return: The list, it can be empty if no one match the filter. 

1475 """ 

1476 project_list = self.auth.get_project_list(filter_q) 

1477 if not session["allow_show_user_project_role"]: 

1478 # Bug 853 - Default filtering 

1479 user = self.auth.get_user(session["user_id"]) 

1480 projects = [prm["project"] for prm in user["project_role_mappings"]] 

1481 project_list = [proj for proj in project_list if proj["_id"] in projects] 

1482 return project_list 

1483 

1484 def delete(self, session, _id, dry_run=False, not_send_msg=None): 

1485 """ 

1486 Delete item by its internal _id 

1487 

1488 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1489 :param _id: server internal id 

1490 :param dry_run: make checking but do not delete 

1491 :param not_send_msg: To not send message (False) or store content (list) instead 

1492 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... 

1493 """ 

1494 # Allow _id to be a name or uuid 

1495 proj = self.auth.get_project(_id) 

1496 pid = proj["_id"] 

1497 self.check_conflict_on_del(session, pid, proj) 

1498 if not dry_run: 

1499 v = self.auth.delete_project(pid) 

1500 self._send_msg("deleted", proj, not_send_msg=None) 

1501 return v 

1502 return None 

1503 

1504 def edit(self, session, _id, indata=None, kwargs=None, content=None): 

1505 """ 

1506 Updates a project entry. 

1507 

1508 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1509 :param _id: 

1510 :param indata: data to be inserted 

1511 :param kwargs: used to override the indata descriptor 

1512 :param content: 

1513 :return: _id: identity of the inserted data. 

1514 """ 

1515 indata = self._remove_envelop(indata) 

1516 

1517 # Override descriptor with query string kwargs 

1518 if kwargs: 

1519 BaseTopic._update_input_with_kwargs(indata, kwargs) 

1520 try: 

1521 if not content: 

1522 content = self.show(session, _id) 

1523 indata = self._validate_input_edit(indata, content, force=session["force"]) 

1524 content = self.check_conflict_on_edit(session, content, indata, _id=_id) 

1525 self.format_on_edit(content, indata) 

1526 content_original = copy.deepcopy(content) 

1527 deep_update_rfc7396(content, indata) 

1528 self.auth.update_project(content["_id"], content) 

1529 proj_data = {"_id": _id, "changes": indata, "original": content_original} 

1530 self._send_msg("edited", proj_data, not_send_msg=None) 

1531 except ValidationError as e: 

1532 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

1533 

1534 

1535class RoleTopicAuth(BaseTopic): 

1536 topic = "roles" 

1537 topic_msg = None # "roles" 

1538 schema_new = roles_new_schema 

1539 schema_edit = roles_edit_schema 

1540 multiproject = False 

1541 

1542 def __init__(self, db, fs, msg, auth): 

1543 BaseTopic.__init__(self, db, fs, msg, auth) 

1544 # self.auth = auth 

1545 self.operations = auth.role_permissions 

1546 # self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles" 

1547 

1548 @staticmethod 

1549 def validate_role_definition(operations, role_definitions): 

1550 """ 

1551 Validates the role definition against the operations defined in 

1552 the resources to operations files. 

1553 

1554 :param operations: operations list 

1555 :param role_definitions: role definition to test 

1556 :return: None if ok, raises ValidationError exception on error 

1557 """ 

1558 if not role_definitions.get("permissions"): 

1559 return 

1560 ignore_fields = ["admin", "default"] 

1561 for role_def in role_definitions["permissions"].keys(): 

1562 if role_def in ignore_fields: 

1563 continue 

1564 if role_def[-1] == ":": 

1565 raise ValidationError("Operation cannot end with ':'") 

1566 

1567 match = next( 

1568 ( 

1569 op 

1570 for op in operations 

1571 if op == role_def or op.startswith(role_def + ":") 

1572 ), 

1573 None, 

1574 ) 

1575 

1576 if not match: 

1577 raise ValidationError("Invalid permission '{}'".format(role_def)) 

1578 

1579 def _validate_input_new(self, input, force=False): 

1580 """ 

1581 Validates input user content for a new entry. 

1582 

1583 :param input: user input content for the new topic 

1584 :param force: may be used for being more tolerant 

1585 :return: The same input content, or a changed version of it. 

1586 """ 

1587 if self.schema_new: 

1588 validate_input(input, self.schema_new) 

1589 self.validate_role_definition(self.operations, input) 

1590 

1591 return input 

1592 

1593 def _validate_input_edit(self, input, content, force=False): 

1594 """ 

1595 Validates input user content for updating an entry. 

1596 

1597 :param input: user input content for the new topic 

1598 :param force: may be used for being more tolerant 

1599 :return: The same input content, or a changed version of it. 

1600 """ 

1601 if self.schema_edit: 

1602 validate_input(input, self.schema_edit) 

1603 self.validate_role_definition(self.operations, input) 

1604 

1605 return input 

1606 

1607 def check_conflict_on_new(self, session, indata): 

1608 """ 

1609 Check that the data to be inserted is valid 

1610 

1611 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1612 :param indata: data to be inserted 

1613 :return: None or raises EngineException 

1614 """ 

1615 # check name is not uuid 

1616 role_name = indata.get("name") 

1617 if is_valid_uuid(role_name): 

1618 raise EngineException( 

1619 "role name '{}' cannot have an uuid format".format(role_name), 

1620 HTTPStatus.UNPROCESSABLE_ENTITY, 

1621 ) 

1622 # check name not exists 

1623 name = indata["name"] 

1624 # if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False): 

1625 if self.auth.get_role_list({"name": name}): 

1626 raise EngineException( 

1627 "role name '{}' exists".format(name), HTTPStatus.CONFLICT 

1628 ) 

1629 

1630 def check_conflict_on_edit(self, session, final_content, edit_content, _id): 

1631 """ 

1632 Check that the data to be edited/uploaded is valid 

1633 

1634 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1635 :param final_content: data once modified 

1636 :param edit_content: incremental data that contains the modifications to apply 

1637 :param _id: internal _id 

1638 :return: None or raises EngineException 

1639 """ 

1640 if "default" not in final_content["permissions"]: 

1641 final_content["permissions"]["default"] = False 

1642 if "admin" not in final_content["permissions"]: 

1643 final_content["permissions"]["admin"] = False 

1644 

1645 # check name is not uuid 

1646 role_name = edit_content.get("name") 

1647 if is_valid_uuid(role_name): 

1648 raise EngineException( 

1649 "role name '{}' cannot have an uuid format".format(role_name), 

1650 HTTPStatus.UNPROCESSABLE_ENTITY, 

1651 ) 

1652 

1653 # Check renaming of admin roles 

1654 role = self.auth.get_role(_id) 

1655 if role["name"] in ["system_admin", "project_admin"]: 

1656 raise EngineException( 

1657 "You cannot rename role '{}'".format(role["name"]), 

1658 http_code=HTTPStatus.FORBIDDEN, 

1659 ) 

1660 

1661 # check name not exists 

1662 if "name" in edit_content: 

1663 role_name = edit_content["name"] 

1664 # if self.db.get_one(self.topic, {"name":role_name,"_id.ne":_id}, fail_on_empty=False, fail_on_more=False): 

1665 roles = self.auth.get_role_list({"name": role_name}) 

1666 if roles and roles[0][BaseTopic.id_field("roles", _id)] != _id: 

1667 raise EngineException( 

1668 "role name '{}' exists".format(role_name), HTTPStatus.CONFLICT 

1669 ) 

1670 

1671 return final_content 

1672 

1673 def check_conflict_on_del(self, session, _id, db_content): 

1674 """ 

1675 Check if deletion can be done because of dependencies if it is not force. To override 

1676 

1677 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1678 :param _id: internal _id 

1679 :param db_content: The database content of this item _id 

1680 :return: None if ok or raises EngineException with the conflict 

1681 """ 

1682 role = self.auth.get_role(_id) 

1683 if role["name"] in ["system_admin", "project_admin"]: 

1684 raise EngineException( 

1685 "You cannot delete role '{}'".format(role["name"]), 

1686 http_code=HTTPStatus.FORBIDDEN, 

1687 ) 

1688 

1689 # If any user is using this role, raise CONFLICT exception 

1690 if not session["force"]: 

1691 for user in self.auth.get_user_list(): 

1692 for prm in user.get("project_role_mappings"): 

1693 if prm["role"] == _id: 

1694 raise EngineException( 

1695 "Role '{}' ({}) is being used by user '{}'".format( 

1696 role["name"], _id, user["username"] 

1697 ), 

1698 HTTPStatus.CONFLICT, 

1699 ) 

1700 

1701 @staticmethod 

1702 def format_on_new(content, project_id=None, make_public=False): # TO BE REMOVED ? 

1703 """ 

1704 Modifies content descriptor to include _admin 

1705 

1706 :param content: descriptor to be modified 

1707 :param project_id: if included, it add project read/write permissions 

1708 :param make_public: if included it is generated as public for reading. 

1709 :return: None, but content is modified 

1710 """ 

1711 now = time() 

1712 if "_admin" not in content: 

1713 content["_admin"] = {} 

1714 if not content["_admin"].get("created"): 

1715 content["_admin"]["created"] = now 

1716 content["_admin"]["modified"] = now 

1717 

1718 if "permissions" not in content: 

1719 content["permissions"] = {} 

1720 

1721 if "default" not in content["permissions"]: 

1722 content["permissions"]["default"] = False 

1723 if "admin" not in content["permissions"]: 

1724 content["permissions"]["admin"] = False 

1725 

1726 @staticmethod 

1727 def format_on_edit(final_content, edit_content): 

1728 """ 

1729 Modifies final_content descriptor to include the modified date. 

1730 

1731 :param final_content: final descriptor generated 

1732 :param edit_content: alterations to be include 

1733 :return: None, but final_content is modified 

1734 """ 

1735 if "_admin" in final_content: 

1736 final_content["_admin"]["modified"] = time() 

1737 

1738 if "permissions" not in final_content: 

1739 final_content["permissions"] = {} 

1740 

1741 if "default" not in final_content["permissions"]: 

1742 final_content["permissions"]["default"] = False 

1743 if "admin" not in final_content["permissions"]: 

1744 final_content["permissions"]["admin"] = False 

1745 return None 

1746 

1747 def show(self, session, _id, filter_q=None, api_req=False): 

1748 """ 

1749 Get complete information on an topic 

1750 

1751 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1752 :param _id: server internal id 

1753 :param filter_q: dict: query parameter 

1754 :param api_req: True if this call is serving an external API request. False if serving internal request. 

1755 :return: dictionary, raise exception if not found. 

1756 """ 

1757 filter_q = {BaseTopic.id_field(self.topic, _id): _id} 

1758 # roles = self.auth.get_role_list(filter_q) 

1759 roles = self.list(session, filter_q) # To allow default filtering (Bug 853) 

1760 if not roles: 

1761 raise AuthconnNotFoundException( 

1762 "Not found any role with filter {}".format(filter_q) 

1763 ) 

1764 elif len(roles) > 1: 

1765 raise AuthconnConflictException( 

1766 "Found more than one role with filter {}".format(filter_q) 

1767 ) 

1768 return roles[0] 

1769 

1770 def list(self, session, filter_q=None, api_req=False): 

1771 """ 

1772 Get a list of the topic that matches a filter 

1773 

1774 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1775 :param filter_q: filter of data to be applied 

1776 :return: The list, it can be empty if no one match the filter. 

1777 """ 

1778 role_list = self.auth.get_role_list(filter_q) 

1779 if not session["allow_show_user_project_role"]: 

1780 # Bug 853 - Default filtering 

1781 user = self.auth.get_user(session["user_id"]) 

1782 roles = [prm["role"] for prm in user["project_role_mappings"]] 

1783 role_list = [role for role in role_list if role["_id"] in roles] 

1784 return role_list 

1785 

1786 def new(self, rollback, session, indata=None, kwargs=None, headers=None): 

1787 """ 

1788 Creates a new entry into database. 

1789 

1790 :param rollback: list to append created items at database in case a rollback may to be done 

1791 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1792 :param indata: data to be inserted 

1793 :param kwargs: used to override the indata descriptor 

1794 :param headers: http request headers 

1795 :return: _id: identity of the inserted data, operation _id (None) 

1796 """ 

1797 try: 

1798 content = self._remove_envelop(indata) 

1799 

1800 # Override descriptor with query string kwargs 

1801 self._update_input_with_kwargs(content, kwargs) 

1802 content = self._validate_input_new(content, session["force"]) 

1803 self.check_conflict_on_new(session, content) 

1804 self.format_on_new( 

1805 content, project_id=session["project_id"], make_public=session["public"] 

1806 ) 

1807 # role_name = content["name"] 

1808 rid = self.auth.create_role(content) 

1809 content["_id"] = rid 

1810 # _id = self.db.create(self.topic, content) 

1811 rollback.append({"topic": self.topic, "_id": rid}) 

1812 # self._send_msg("created", content, not_send_msg=not_send_msg) 

1813 return rid, None 

1814 except ValidationError as e: 

1815 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

1816 

1817 def delete(self, session, _id, dry_run=False, not_send_msg=None): 

1818 """ 

1819 Delete item by its internal _id 

1820 

1821 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1822 :param _id: server internal id 

1823 :param dry_run: make checking but do not delete 

1824 :param not_send_msg: To not send message (False) or store content (list) instead 

1825 :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... 

1826 """ 

1827 filter_q = {BaseTopic.id_field(self.topic, _id): _id} 

1828 roles = self.auth.get_role_list(filter_q) 

1829 if not roles: 

1830 raise AuthconnNotFoundException( 

1831 "Not found any role with filter {}".format(filter_q) 

1832 ) 

1833 elif len(roles) > 1: 

1834 raise AuthconnConflictException( 

1835 "Found more than one role with filter {}".format(filter_q) 

1836 ) 

1837 rid = roles[0]["_id"] 

1838 self.check_conflict_on_del(session, rid, None) 

1839 # filter_q = {"_id": _id} 

1840 # filter_q = {BaseTopic.id_field(self.topic, _id): _id} # To allow role addressing by name 

1841 if not dry_run: 

1842 v = self.auth.delete_role(rid) 

1843 # v = self.db.del_one(self.topic, filter_q) 

1844 return v 

1845 return None 

1846 

1847 def edit(self, session, _id, indata=None, kwargs=None, content=None): 

1848 """ 

1849 Updates a role entry. 

1850 

1851 :param session: contains "username", "admin", "force", "public", "project_id", "set_project" 

1852 :param _id: 

1853 :param indata: data to be inserted 

1854 :param kwargs: used to override the indata descriptor 

1855 :param content: 

1856 :return: _id: identity of the inserted data. 

1857 """ 

1858 if kwargs: 

1859 self._update_input_with_kwargs(indata, kwargs) 

1860 try: 

1861 if not content: 

1862 content = self.show(session, _id) 

1863 indata = self._validate_input_edit(indata, content, force=session["force"]) 

1864 deep_update_rfc7396(content, indata) 

1865 content = self.check_conflict_on_edit(session, content, indata, _id=_id) 

1866 self.format_on_edit(content, indata) 

1867 self.auth.update_role(content) 

1868 except ValidationError as e: 

1869 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)