Coverage for osm_nbi/admin_topics.py: 70%

743 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2024-06-27 02:46 +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 {"username": session["username"]} 

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 

356 # create edit operation 

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

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

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

360 ) 

361 

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

363 """ 

364 Modifies content descriptor to include _admin and insert create operation 

365 :param content: descriptor to be modified 

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

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

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

369 """ 

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

371 content["schema_version"] = schema_version = "1.11" 

372 

373 # encrypt passwords 

374 if content.get(self.password_to_encrypt): 

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

376 content[self.password_to_encrypt], 

377 schema_version=schema_version, 

378 salt=content["_id"], 

379 ) 

380 config_to_encrypt_keys = self.config_to_encrypt.get( 

381 schema_version 

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

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

384 for p in config_to_encrypt_keys: 

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

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

387 content["config"][p], 

388 schema_version=schema_version, 

389 salt=content["_id"], 

390 ) 

391 

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

393 

394 # create operation 

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

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

397 # create Resource in Openstack based VIM 

398 if content.get("vim_type"): 

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

400 compute = { 

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

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

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

404 } 

405 storage = { 

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

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

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

409 } 

410 network = { 

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

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

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

414 } 

415 content["resources"] = { 

416 "compute": compute, 

417 "storage": storage, 

418 "network": network, 

419 } 

420 

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

422 

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

424 """ 

425 Delete item by its internal _id 

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

427 :param _id: server internal id 

428 :param dry_run: make checking but do not delete 

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

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

431 """ 

432 

433 filter_q = self._get_project_filter(session) 

434 filter_q["_id"] = _id 

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

436 

437 self.check_conflict_on_del(session, _id, db_content) 

438 if dry_run: 

439 return None 

440 

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

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

443 if session["project_id"]: 

444 other_projects_referencing = next( 

445 ( 

446 p 

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

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

449 ), 

450 None, 

451 ) 

452 

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

454 if other_projects_referencing: 

455 # remove references but not delete 

456 update_dict_pull = { 

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

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

459 } 

460 self.db.set_one( 

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

462 ) 

463 return None 

464 else: 

465 can_write = next( 

466 ( 

467 p 

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

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

470 ), 

471 None, 

472 ) 

473 if not can_write: 

474 raise EngineException( 

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

476 http_code=HTTPStatus.UNAUTHORIZED, 

477 ) 

478 

479 # It must be deleted 

480 if session["force"]: 

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

482 op_id = None 

483 self._send_msg( 

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

485 ) 

486 else: 

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

488 self.db.set_one( 

489 self.topic, 

490 {"_id": _id}, 

491 update_dict=update_dict, 

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

493 ) 

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

495 # so the -1 is not needed 

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

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

498 ) 

499 self._send_msg( 

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

501 ) 

502 return op_id 

503 

504 

505class VimAccountTopic(CommonVimWimSdn): 

506 topic = "vim_accounts" 

507 topic_msg = "vim_account" 

508 schema_new = vim_account_new_schema 

509 schema_edit = vim_account_edit_schema 

510 multiproject = True 

511 password_to_encrypt = "vim_password" 

512 config_to_encrypt = { 

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

514 "default": ( 

515 "admin_password", 

516 "nsx_password", 

517 "vcenter_password", 

518 "vrops_password", 

519 ), 

520 } 

521 

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

523 """ 

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

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

526 :param _id: internal _id 

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

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

529 """ 

530 if session["force"]: 

531 return 

532 # check if used by VNF 

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

534 raise EngineException( 

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

536 http_code=HTTPStatus.CONFLICT, 

537 ) 

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

539 

540 

541class WimAccountTopic(CommonVimWimSdn): 

542 topic = "wim_accounts" 

543 topic_msg = "wim_account" 

544 schema_new = wim_account_new_schema 

545 schema_edit = wim_account_edit_schema 

546 multiproject = True 

547 password_to_encrypt = "password" 

548 config_to_encrypt = {} 

549 

550 

551class SdnTopic(CommonVimWimSdn): 

552 topic = "sdns" 

553 topic_msg = "sdn" 

554 quota_name = "sdn_controllers" 

555 schema_new = sdn_new_schema 

556 schema_edit = sdn_edit_schema 

557 multiproject = True 

558 password_to_encrypt = "password" 

559 config_to_encrypt = {} 

560 

561 def _obtain_url(self, input, create): 

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

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

564 raise ValidationError( 

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

566 ) 

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

568 del input["ip"] 

569 del input["port"] 

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

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

572 return input 

573 

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

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

576 return self._obtain_url(input, True) 

577 

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

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

580 return self._obtain_url(input, False) 

581 

582 

583class K8sClusterTopic(CommonVimWimSdn): 

584 topic = "k8sclusters" 

585 topic_msg = "k8scluster" 

586 schema_new = k8scluster_new_schema 

587 schema_edit = k8scluster_edit_schema 

588 multiproject = True 

589 password_to_encrypt = None 

590 config_to_encrypt = {} 

591 

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

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

594 self.db.encrypt_decrypt_fields( 

595 content["credentials"], 

596 "encrypt", 

597 ["password", "secret"], 

598 schema_version=content["schema_version"], 

599 salt=content["_id"], 

600 ) 

601 # Add Helm/Juju Repo lists 

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

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

604 if proj != "ANY": 

605 for repo in self.db.get_list( 

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

607 ): 

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

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

610 for k in repos: 

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

612 return oid 

613 

614 def format_on_edit(self, final_content, edit_content): 

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

616 self.db.encrypt_decrypt_fields( 

617 edit_content["credentials"], 

618 "encrypt", 

619 ["password", "secret"], 

620 schema_version=final_content["schema_version"], 

621 salt=final_content["_id"], 

622 ) 

623 deep_update_rfc7396( 

624 final_content["credentials"], edit_content["credentials"] 

625 ) 

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

627 return oid 

628 

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

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

631 session, final_content, edit_content, _id 

632 ) 

633 final_content = super().check_conflict_on_edit( 

634 session, final_content, edit_content, _id 

635 ) 

636 # Update Helm/Juju Repo lists 

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

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

639 if proj != "ANY": 

640 for repo in self.db.get_list( 

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

642 ): 

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

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

645 for k in repos: 

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

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

648 final_content["_admin"][rlist] = [] 

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

650 return final_content 

651 

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

653 """ 

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

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

656 :param _id: internal _id 

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

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

659 """ 

660 if session["force"]: 

661 return 

662 # check if used by VNF 

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

664 if session["project_id"]: 

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

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

667 raise EngineException( 

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

669 http_code=HTTPStatus.CONFLICT, 

670 ) 

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

672 

673 

674class VcaTopic(CommonVimWimSdn): 

675 topic = "vca" 

676 topic_msg = "vca" 

677 schema_new = vca_new_schema 

678 schema_edit = vca_edit_schema 

679 multiproject = True 

680 password_to_encrypt = None 

681 

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

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

684 content["schema_version"] = schema_version = "1.11" 

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

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

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

688 ) 

689 return oid 

690 

691 def format_on_edit(self, final_content, edit_content): 

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

693 schema_version = final_content.get("schema_version") 

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

695 if key in edit_content: 

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

697 edit_content[key], 

698 schema_version=schema_version, 

699 salt=final_content["_id"], 

700 ) 

701 return oid 

702 

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

704 """ 

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

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

707 :param _id: internal _id 

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

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

710 """ 

711 if session["force"]: 

712 return 

713 # check if used by VNF 

714 filter_q = {"vca": _id} 

715 if session["project_id"]: 

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

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

718 raise EngineException( 

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

720 http_code=HTTPStatus.CONFLICT, 

721 ) 

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

723 

724 

725class K8sRepoTopic(CommonVimWimSdn): 

726 topic = "k8srepos" 

727 topic_msg = "k8srepo" 

728 schema_new = k8srepo_new_schema 

729 schema_edit = k8srepo_edit_schema 

730 multiproject = True 

731 password_to_encrypt = None 

732 config_to_encrypt = {} 

733 

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

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

736 # Update Helm/Juju Repo lists 

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

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

739 if proj != "ANY": 

740 self.db.set_list( 

741 "k8sclusters", 

742 { 

743 "_admin.projects_read": proj, 

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

745 }, 

746 {}, 

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

748 ) 

749 return oid 

750 

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

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

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

754 if oid: 

755 # Remove from Helm/Juju Repo lists 

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

757 self.db.set_list( 

758 "k8sclusters", 

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

760 {}, 

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

762 ) 

763 return oid 

764 

765 

766class OsmRepoTopic(BaseTopic): 

767 topic = "osmrepos" 

768 topic_msg = "osmrepos" 

769 schema_new = osmrepo_new_schema 

770 schema_edit = osmrepo_edit_schema 

771 multiproject = True 

772 # TODO: Implement user/password 

773 

774 

775class UserTopicAuth(UserTopic): 

776 # topic = "users" 

777 topic_msg = "users" 

778 schema_new = user_new_schema 

779 schema_edit = user_edit_schema 

780 

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

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

783 # self.auth = auth 

784 

785 def check_conflict_on_new(self, session, indata): 

786 """ 

787 Check that the data to be inserted is valid 

788 

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

790 :param indata: data to be inserted 

791 :return: None or raises EngineException 

792 """ 

793 username = indata.get("username") 

794 if is_valid_uuid(username): 

795 raise EngineException( 

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

797 HTTPStatus.UNPROCESSABLE_ENTITY, 

798 ) 

799 

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

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

802 raise EngineException( 

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

804 ) 

805 

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

807 # convert to new format project_role_mappings 

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

809 if not role: 

810 role = self.auth.get_role_list() 

811 if not role: 

812 raise AuthconnNotFoundException( 

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

814 ) 

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

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

817 indata["project_role_mappings"] = [] 

818 for project in indata["projects"]: 

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

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

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

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

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

824 # HTTPStatus.BAD_REQUEST) 

825 

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

827 """ 

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

829 

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

831 :param final_content: data once modified 

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

833 :param _id: internal _id 

834 :return: None or raises EngineException 

835 """ 

836 

837 if "username" in edit_content: 

838 username = edit_content.get("username") 

839 if is_valid_uuid(username): 

840 raise EngineException( 

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

842 HTTPStatus.UNPROCESSABLE_ENTITY, 

843 ) 

844 

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

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

847 raise EngineException( 

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

849 HTTPStatus.CONFLICT, 

850 ) 

851 

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

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

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

855 None, 

856 "system_admin", 

857 ): 

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

859 raise EngineException( 

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

861 http_code=HTTPStatus.FORBIDDEN, 

862 ) 

863 

864 return final_content 

865 

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

867 """ 

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

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

870 :param _id: internal _id 

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

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

873 """ 

874 if db_content["username"] == session["username"]: 

875 raise EngineException( 

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

877 ) 

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

879 

880 @staticmethod 

881 def format_on_show(content): 

882 """ 

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

884 metadata from the role definition. 

885 """ 

886 project_role_mappings = [] 

887 

888 if "projects" in content: 

889 for project in content["projects"]: 

890 for role in project["roles"]: 

891 project_role_mappings.append( 

892 { 

893 "project": project["_id"], 

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

895 "role": role["_id"], 

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

897 } 

898 ) 

899 del content["projects"] 

900 content["project_role_mappings"] = project_role_mappings 

901 

902 return content 

903 

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

905 """ 

906 Creates a new entry into the authentication backend. 

907 

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

909 

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

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

912 :param indata: data to be inserted 

913 :param kwargs: used to override the indata descriptor 

914 :param headers: http request headers 

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

916 """ 

917 try: 

918 content = BaseTopic._remove_envelop(indata) 

919 

920 # Override descriptor with query string kwargs 

921 BaseTopic._update_input_with_kwargs(content, kwargs) 

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

923 self.check_conflict_on_new(session, content) 

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

925 now = time() 

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

927 prms = [] 

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

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

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

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

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

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

934 if prl not in prms: 

935 prms.append(prl) 

936 content["project_role_mappings"] = prms 

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

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

939 

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

941 # del content["password"] 

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

943 return _id, None 

944 except ValidationError as e: 

945 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

946 

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

948 """ 

949 Get complete information on an topic 

950 

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

952 :param _id: server internal id or username 

953 :param filter_q: dict: query parameter 

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

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

956 """ 

957 # Allow _id to be a name or uuid 

958 filter_q = {"username": _id} 

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

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

961 if len(users) == 1: 

962 return users[0] 

963 elif len(users) > 1: 

964 raise EngineException( 

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

966 ) 

967 else: 

968 raise EngineException( 

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

970 ) 

971 

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

973 """ 

974 Updates an user entry. 

975 

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

977 :param _id: 

978 :param indata: data to be inserted 

979 :param kwargs: used to override the indata descriptor 

980 :param content: 

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

982 """ 

983 indata = self._remove_envelop(indata) 

984 

985 # Override descriptor with query string kwargs 

986 if kwargs: 

987 BaseTopic._update_input_with_kwargs(indata, kwargs) 

988 try: 

989 if not content: 

990 content = self.show(session, _id) 

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

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

993 # self.format_on_edit(content, indata) 

994 

995 if not ( 

996 "password" in indata 

997 or "username" in indata 

998 or indata.get("remove_project_role_mappings") 

999 or indata.get("add_project_role_mappings") 

1000 or indata.get("project_role_mappings") 

1001 or indata.get("projects") 

1002 or indata.get("add_projects") 

1003 or indata.get("unlock") 

1004 or indata.get("renew") 

1005 ): 

1006 return _id 

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

1008 indata.get("remove_project_role_mappings") 

1009 or indata.get("add_project_role_mappings") 

1010 ): 

1011 raise EngineException( 

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

1013 "' or 'remove_project_role_mappings'", 

1014 http_code=HTTPStatus.BAD_REQUEST, 

1015 ) 

1016 

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

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

1019 if not role: 

1020 role = self.auth.get_role_list() 

1021 if not role: 

1022 raise AuthconnNotFoundException( 

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

1024 content["username"] 

1025 ) 

1026 ) 

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

1028 if "add_project_role_mappings" not in indata: 

1029 indata["add_project_role_mappings"] = [] 

1030 if "remove_project_role_mappings" not in indata: 

1031 indata["remove_project_role_mappings"] = [] 

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

1033 # backward compatible 

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

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

1036 indata["remove_project_role_mappings"].append( 

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

1038 ) 

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

1040 indata["add_project_role_mappings"].append( 

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

1042 ) 

1043 del indata["projects"] 

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

1045 indata["add_project_role_mappings"].append( 

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

1047 ) 

1048 

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

1050 original_mapping = content["project_role_mappings"] 

1051 

1052 mappings_to_add = [] 

1053 mappings_to_remove = [] 

1054 

1055 # remove 

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

1057 for mapping in original_mapping: 

1058 if to_remove["project"] in ( 

1059 mapping["project"], 

1060 mapping["project_name"], 

1061 ): 

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

1063 mapping["role"], 

1064 mapping["role_name"], 

1065 ): 

1066 mappings_to_remove.append(mapping) 

1067 

1068 # add 

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

1070 for mapping in original_mapping: 

1071 if to_add["project"] in ( 

1072 mapping["project"], 

1073 mapping["project_name"], 

1074 ) and to_add["role"] in ( 

1075 mapping["role"], 

1076 mapping["role_name"], 

1077 ): 

1078 if mapping in mappings_to_remove: # do not remove 

1079 mappings_to_remove.remove(mapping) 

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

1081 else: 

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

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

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

1085 

1086 # set 

1087 if indata.get("project_role_mappings"): 

1088 for to_set in indata["project_role_mappings"]: 

1089 for mapping in original_mapping: 

1090 if to_set["project"] in ( 

1091 mapping["project"], 

1092 mapping["project_name"], 

1093 ) and to_set["role"] in ( 

1094 mapping["role"], 

1095 mapping["role_name"], 

1096 ): 

1097 if mapping in mappings_to_remove: # do not remove 

1098 mappings_to_remove.remove(mapping) 

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

1100 else: 

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

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

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

1104 for mapping in original_mapping: 

1105 for to_set in indata["project_role_mappings"]: 

1106 if to_set["project"] in ( 

1107 mapping["project"], 

1108 mapping["project_name"], 

1109 ) and to_set["role"] in ( 

1110 mapping["role"], 

1111 mapping["role_name"], 

1112 ): 

1113 break 

1114 else: 

1115 # delete 

1116 if mapping not in mappings_to_remove: # do not remove 

1117 mappings_to_remove.append(mapping) 

1118 

1119 self.auth.update_user( 

1120 { 

1121 "_id": _id, 

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

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

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

1125 "add_project_role_mappings": mappings_to_add, 

1126 "remove_project_role_mappings": mappings_to_remove, 

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

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

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

1130 } 

1131 ) 

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

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

1134 

1135 # return _id 

1136 except ValidationError as e: 

1137 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

1138 

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

1140 """ 

1141 Get a list of the topic that matches a filter 

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

1143 :param filter_q: filter of data to be applied 

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

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

1146 """ 

1147 user_list = self.auth.get_user_list(filter_q) 

1148 if not session["allow_show_user_project_role"]: 

1149 # Bug 853 - Default filtering 

1150 user_list = [ 

1151 usr for usr in user_list if usr["username"] == session["username"] 

1152 ] 

1153 return user_list 

1154 

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

1156 """ 

1157 Delete item by its internal _id 

1158 

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

1160 :param _id: server internal id 

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

1162 :param dry_run: make checking but do not delete 

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

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

1165 """ 

1166 # Allow _id to be a name or uuid 

1167 user = self.auth.get_user(_id) 

1168 uid = user["_id"] 

1169 self.check_conflict_on_del(session, uid, user) 

1170 if not dry_run: 

1171 v = self.auth.delete_user(uid) 

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

1173 return v 

1174 return None 

1175 

1176 

1177class ProjectTopicAuth(ProjectTopic): 

1178 # topic = "projects" 

1179 topic_msg = "project" 

1180 schema_new = project_new_schema 

1181 schema_edit = project_edit_schema 

1182 

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

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

1185 # self.auth = auth 

1186 

1187 def check_conflict_on_new(self, session, indata): 

1188 """ 

1189 Check that the data to be inserted is valid 

1190 

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

1192 :param indata: data to be inserted 

1193 :return: None or raises EngineException 

1194 """ 

1195 project_name = indata.get("name") 

1196 if is_valid_uuid(project_name): 

1197 raise EngineException( 

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

1199 HTTPStatus.UNPROCESSABLE_ENTITY, 

1200 ) 

1201 

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

1203 

1204 if project_list: 

1205 raise EngineException( 

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

1207 ) 

1208 

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

1210 """ 

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

1212 

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

1214 :param final_content: data once modified 

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

1216 :param _id: internal _id 

1217 :return: None or raises EngineException 

1218 """ 

1219 

1220 project_name = edit_content.get("name") 

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

1222 if is_valid_uuid(project_name): 

1223 raise EngineException( 

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

1225 HTTPStatus.UNPROCESSABLE_ENTITY, 

1226 ) 

1227 

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

1229 raise EngineException( 

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

1231 ) 

1232 

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

1234 if project_name and self.auth.get_project_list( 

1235 filter_q={"name": project_name} 

1236 ): 

1237 raise EngineException( 

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

1239 HTTPStatus.CONFLICT, 

1240 ) 

1241 return final_content 

1242 

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

1244 """ 

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

1246 

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

1248 :param _id: internal _id 

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

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

1251 """ 

1252 

1253 def check_rw_projects(topic, title, id_field): 

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

1255 if ( 

1256 _id 

1257 in desc["_admin"]["projects_read"] 

1258 + desc["_admin"]["projects_write"] 

1259 ): 

1260 raise EngineException( 

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

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

1263 ), 

1264 HTTPStatus.CONFLICT, 

1265 ) 

1266 

1267 if _id in session["project_id"]: 

1268 raise EngineException( 

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

1270 ) 

1271 

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

1273 raise EngineException( 

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

1275 ) 

1276 

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

1278 if not session["force"]: 

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

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

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

1282 raise EngineException( 

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

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

1285 ), 

1286 HTTPStatus.CONFLICT, 

1287 ) 

1288 

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

1290 if not session["force"]: 

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

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

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

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

1295 

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

1297 """ 

1298 Creates a new entry into the authentication backend. 

1299 

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

1301 

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

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

1304 :param indata: data to be inserted 

1305 :param kwargs: used to override the indata descriptor 

1306 :param headers: http request headers 

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

1308 """ 

1309 try: 

1310 content = BaseTopic._remove_envelop(indata) 

1311 

1312 # Override descriptor with query string kwargs 

1313 BaseTopic._update_input_with_kwargs(content, kwargs) 

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

1315 self.check_conflict_on_new(session, content) 

1316 self.format_on_new( 

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

1318 ) 

1319 _id = self.auth.create_project(content) 

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

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

1322 return _id, None 

1323 except ValidationError as e: 

1324 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

1325 

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

1327 """ 

1328 Get complete information on an topic 

1329 

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

1331 :param _id: server internal id 

1332 :param filter_q: dict: query parameter 

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

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

1335 """ 

1336 # Allow _id to be a name or uuid 

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

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

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

1340 if len(projects) == 1: 

1341 return projects[0] 

1342 elif len(projects) > 1: 

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

1344 else: 

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

1346 

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

1348 """ 

1349 Get a list of the topic that matches a filter 

1350 

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

1352 :param filter_q: filter of data to be applied 

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

1354 """ 

1355 project_list = self.auth.get_project_list(filter_q) 

1356 if not session["allow_show_user_project_role"]: 

1357 # Bug 853 - Default filtering 

1358 user = self.auth.get_user(session["username"]) 

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

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

1361 return project_list 

1362 

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

1364 """ 

1365 Delete item by its internal _id 

1366 

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

1368 :param _id: server internal id 

1369 :param dry_run: make checking but do not delete 

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

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

1372 """ 

1373 # Allow _id to be a name or uuid 

1374 proj = self.auth.get_project(_id) 

1375 pid = proj["_id"] 

1376 self.check_conflict_on_del(session, pid, proj) 

1377 if not dry_run: 

1378 v = self.auth.delete_project(pid) 

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

1380 return v 

1381 return None 

1382 

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

1384 """ 

1385 Updates a project entry. 

1386 

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

1388 :param _id: 

1389 :param indata: data to be inserted 

1390 :param kwargs: used to override the indata descriptor 

1391 :param content: 

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

1393 """ 

1394 indata = self._remove_envelop(indata) 

1395 

1396 # Override descriptor with query string kwargs 

1397 if kwargs: 

1398 BaseTopic._update_input_with_kwargs(indata, kwargs) 

1399 try: 

1400 if not content: 

1401 content = self.show(session, _id) 

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

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

1404 self.format_on_edit(content, indata) 

1405 content_original = copy.deepcopy(content) 

1406 deep_update_rfc7396(content, indata) 

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

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

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

1410 except ValidationError as e: 

1411 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

1412 

1413 

1414class RoleTopicAuth(BaseTopic): 

1415 topic = "roles" 

1416 topic_msg = None # "roles" 

1417 schema_new = roles_new_schema 

1418 schema_edit = roles_edit_schema 

1419 multiproject = False 

1420 

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

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

1423 # self.auth = auth 

1424 self.operations = auth.role_permissions 

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

1426 

1427 @staticmethod 

1428 def validate_role_definition(operations, role_definitions): 

1429 """ 

1430 Validates the role definition against the operations defined in 

1431 the resources to operations files. 

1432 

1433 :param operations: operations list 

1434 :param role_definitions: role definition to test 

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

1436 """ 

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

1438 return 

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

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

1441 if role_def in ignore_fields: 

1442 continue 

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

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

1445 

1446 match = next( 

1447 ( 

1448 op 

1449 for op in operations 

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

1451 ), 

1452 None, 

1453 ) 

1454 

1455 if not match: 

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

1457 

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

1459 """ 

1460 Validates input user content for a new entry. 

1461 

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

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

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

1465 """ 

1466 if self.schema_new: 

1467 validate_input(input, self.schema_new) 

1468 self.validate_role_definition(self.operations, input) 

1469 

1470 return input 

1471 

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

1473 """ 

1474 Validates input user content for updating an entry. 

1475 

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

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

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

1479 """ 

1480 if self.schema_edit: 

1481 validate_input(input, self.schema_edit) 

1482 self.validate_role_definition(self.operations, input) 

1483 

1484 return input 

1485 

1486 def check_conflict_on_new(self, session, indata): 

1487 """ 

1488 Check that the data to be inserted is valid 

1489 

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

1491 :param indata: data to be inserted 

1492 :return: None or raises EngineException 

1493 """ 

1494 # check name is not uuid 

1495 role_name = indata.get("name") 

1496 if is_valid_uuid(role_name): 

1497 raise EngineException( 

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

1499 HTTPStatus.UNPROCESSABLE_ENTITY, 

1500 ) 

1501 # check name not exists 

1502 name = indata["name"] 

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

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

1505 raise EngineException( 

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

1507 ) 

1508 

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

1510 """ 

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

1512 

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

1514 :param final_content: data once modified 

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

1516 :param _id: internal _id 

1517 :return: None or raises EngineException 

1518 """ 

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

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

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

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

1523 

1524 # check name is not uuid 

1525 role_name = edit_content.get("name") 

1526 if is_valid_uuid(role_name): 

1527 raise EngineException( 

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

1529 HTTPStatus.UNPROCESSABLE_ENTITY, 

1530 ) 

1531 

1532 # Check renaming of admin roles 

1533 role = self.auth.get_role(_id) 

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

1535 raise EngineException( 

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

1537 http_code=HTTPStatus.FORBIDDEN, 

1538 ) 

1539 

1540 # check name not exists 

1541 if "name" in edit_content: 

1542 role_name = edit_content["name"] 

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

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

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

1546 raise EngineException( 

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

1548 ) 

1549 

1550 return final_content 

1551 

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

1553 """ 

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

1555 

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

1557 :param _id: internal _id 

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

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

1560 """ 

1561 role = self.auth.get_role(_id) 

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

1563 raise EngineException( 

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

1565 http_code=HTTPStatus.FORBIDDEN, 

1566 ) 

1567 

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

1569 if not session["force"]: 

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

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

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

1573 raise EngineException( 

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

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

1576 ), 

1577 HTTPStatus.CONFLICT, 

1578 ) 

1579 

1580 @staticmethod 

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

1582 """ 

1583 Modifies content descriptor to include _admin 

1584 

1585 :param content: descriptor to be modified 

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

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

1588 :return: None, but content is modified 

1589 """ 

1590 now = time() 

1591 if "_admin" not in content: 

1592 content["_admin"] = {} 

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

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

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

1596 

1597 if "permissions" not in content: 

1598 content["permissions"] = {} 

1599 

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

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

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

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

1604 

1605 @staticmethod 

1606 def format_on_edit(final_content, edit_content): 

1607 """ 

1608 Modifies final_content descriptor to include the modified date. 

1609 

1610 :param final_content: final descriptor generated 

1611 :param edit_content: alterations to be include 

1612 :return: None, but final_content is modified 

1613 """ 

1614 if "_admin" in final_content: 

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

1616 

1617 if "permissions" not in final_content: 

1618 final_content["permissions"] = {} 

1619 

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

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

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

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

1624 return None 

1625 

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

1627 """ 

1628 Get complete information on an topic 

1629 

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

1631 :param _id: server internal id 

1632 :param filter_q: dict: query parameter 

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

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

1635 """ 

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

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

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

1639 if not roles: 

1640 raise AuthconnNotFoundException( 

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

1642 ) 

1643 elif len(roles) > 1: 

1644 raise AuthconnConflictException( 

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

1646 ) 

1647 return roles[0] 

1648 

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

1650 """ 

1651 Get a list of the topic that matches a filter 

1652 

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

1654 :param filter_q: filter of data to be applied 

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

1656 """ 

1657 role_list = self.auth.get_role_list(filter_q) 

1658 if not session["allow_show_user_project_role"]: 

1659 # Bug 853 - Default filtering 

1660 user = self.auth.get_user(session["username"]) 

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

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

1663 return role_list 

1664 

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

1666 """ 

1667 Creates a new entry into database. 

1668 

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

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

1671 :param indata: data to be inserted 

1672 :param kwargs: used to override the indata descriptor 

1673 :param headers: http request headers 

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

1675 """ 

1676 try: 

1677 content = self._remove_envelop(indata) 

1678 

1679 # Override descriptor with query string kwargs 

1680 self._update_input_with_kwargs(content, kwargs) 

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

1682 self.check_conflict_on_new(session, content) 

1683 self.format_on_new( 

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

1685 ) 

1686 # role_name = content["name"] 

1687 rid = self.auth.create_role(content) 

1688 content["_id"] = rid 

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

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

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

1692 return rid, None 

1693 except ValidationError as e: 

1694 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) 

1695 

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

1697 """ 

1698 Delete item by its internal _id 

1699 

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

1701 :param _id: server internal id 

1702 :param dry_run: make checking but do not delete 

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

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

1705 """ 

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

1707 roles = self.auth.get_role_list(filter_q) 

1708 if not roles: 

1709 raise AuthconnNotFoundException( 

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

1711 ) 

1712 elif len(roles) > 1: 

1713 raise AuthconnConflictException( 

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

1715 ) 

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

1717 self.check_conflict_on_del(session, rid, None) 

1718 # filter_q = {"_id": _id} 

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

1720 if not dry_run: 

1721 v = self.auth.delete_role(rid) 

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

1723 return v 

1724 return None 

1725 

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

1727 """ 

1728 Updates a role entry. 

1729 

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

1731 :param _id: 

1732 :param indata: data to be inserted 

1733 :param kwargs: used to override the indata descriptor 

1734 :param content: 

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

1736 """ 

1737 if kwargs: 

1738 self._update_input_with_kwargs(indata, kwargs) 

1739 try: 

1740 if not content: 

1741 content = self.show(session, _id) 

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

1743 deep_update_rfc7396(content, indata) 

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

1745 self.format_on_edit(content, indata) 

1746 self.auth.update_role(content) 

1747 except ValidationError as e: 

1748 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)