diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index 834dbfa9..db5439a1 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -8,6 +8,8 @@ servers: - url: http://localhost:5000/v1.0 security: - jwt: ["secret"] +# - oAuth2AuthCode: +# - read_email paths: /login: @@ -83,21 +85,8 @@ paths: "200": description: Logout Authenticated User - /login_swagger: + /login_api: parameters: - - name: uid - in: query - required: true - description: The user we are authenticating - schema: - type: string - - name: password - in: query - required: true - description: The password for the user - schema: - type: string - format: password - name: redirect_url in: query required: false @@ -105,13 +94,38 @@ paths: type: string get: security: [] - operationId: spiffworkflow_backend.routes.user.api_login + operationId: spiffworkflow_backend.routes.user.login_api summary: Authenticate user for API access tags: - Authentication responses: "304": description: Redirection to the hosted frontend with an auth_token header. + /login_api_return: + parameters: + - name: code + in: query + required: true + schema: + type: string + - name: state + in: query + required: true + schema: + type: string + - name: session_state + in: query + required: false + schema: + type: string + get: + security: [] + operationId: spiffworkflow_backend.routes.user.login_api_return + tags: + - Authentication + responses: + "200": + description: Test Return Response /status: get: @@ -208,7 +222,7 @@ paths: description: The process group was deleted. put: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_update - summary: Upates a single process group + summary: Updates a single process group tags: - Process Groups requestBody: @@ -942,11 +956,6 @@ paths: - Messages operationId: spiffworkflow_backend.routes.process_api_blueprint.message_instance_list summary: Get a list of message instances - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Workflow" responses: "200": description: One task @@ -1099,42 +1108,40 @@ paths: "404": description: Secret does not exist - /secrets/{service}/{client}/allowed_process_paths: - parameters: - - name: service - in: path - required: true - description: The external service we are using - schema: - type: string - - name: client - in: path - required: true - description: The client identifier of the external service we are using - schema: - type: string - - name: allowed_process_path - in: query - required: false - description: The path to an allowed Process Group or Process Model - schema: - type: string - get: - operationId: spiffworkflow_backend.routes.process_api_blueprint.get_allowed_process_paths - summary: Returns the allowed process paths for a secret + /secrets/allowed_process_paths: + post: + operationId: spiffworkflow_backend.routes.process_api_blueprint.add_allowed_process_path + summary: Create an allowed process to a secret tags: - Secrets + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SecretAllowedProcessPath" responses: - "200": - description: Allowed process paths returned + "201": + description: Allowed process created successfully content: application/json: schema: - type: array - items: - $ref: "#/components/schemas/SecretAllowedProcessPath" -# delete: -# operationId: spiffworkflow_backend.routes.process_api_blueprint.delete_allowed_process_path + $ref: "#/components/schemas/SecretAllowedProcessPath" + /secrets/allowed_process_paths/{allowed_process_path_id}: + parameters: + - name: allowed_process_path_id + in: path + required: true + description: The id of the allowed process path to delete + schema: + type: integer + delete: + operationId: spiffworkflow_backend.routes.process_api_blueprint.delete_allowed_process_path + summary: Delete an existing allowed process for a secret + tags: + - Secrets + responses: + "204": + description: The allowed process is deleted. components: securitySchemes: @@ -1144,6 +1151,18 @@ components: bearerFormat: JWT x-bearerInfoFunc: spiffworkflow_backend.routes.user.verify_token x-scopeValidateFunc: spiffworkflow_backend.routes.user.validate_scope + + oAuth2AuthCode: + type: oauth2 + description: authenticate with openid server + flows: + authorizationCode: + authorizationUrl: /v1.0/login_api + tokenUrl: /v1.0/login_return + scopes: + read_email: read email + x-tokenInfoFunc: spiffworkflow_backend.routes.user.get_scope + schemas: OkTrue: properties: @@ -1864,16 +1883,16 @@ components: example: ["a_file.txt", "b_file.txt"] Secret: properties: - value: - description: The value associated with the key - type: string - example: my_super_secret_value - nullable: false key: description: The key of the secret we want to use type: string example: my_secret_key nullable: false + value: + description: The value associated with the key + type: string + example: my_super_secret_value + nullable: false creator_user_id: description: The id of the logged in user that created this secret type: number @@ -1922,10 +1941,12 @@ components: description: The id of the allowed process path type: number example: 1 + nullable: true secret_id: description: The id of the secret associated with this allowed process path type: number example: 2 - allowed_process_path: + allowed_relative_path: description: The allowed process path + type: string example: /group_one/group_two/model_a diff --git a/src/spiffworkflow_backend/models/secret_model.py b/src/spiffworkflow_backend/models/secret_model.py index 2c972c1b..d87821ae 100644 --- a/src/spiffworkflow_backend/models/secret_model.py +++ b/src/spiffworkflow_backend/models/secret_model.py @@ -46,3 +46,12 @@ class SecretAllowedProcessPathModel(SpiffworkflowBaseDBModel): id: int = db.Column(db.Integer, primary_key=True) secret_id: int = db.Column(ForeignKey(SecretModel.id), nullable=False) # type: ignore allowed_relative_path: str = db.Column(db.String(500), nullable=False) + + +class SecretAllowedProcessSchema(Schema): + + class Meta: + """Meta.""" + + model = SecretAllowedProcessPathModel + fields = ["secret_id", "allowed_relative_path"] diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index 987e342a..618a6ae4 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -43,6 +43,7 @@ from spiffworkflow_backend.models.process_instance_report import ( ) from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema +from spiffworkflow_backend.models.secret_model import SecretAllowedProcessSchema from spiffworkflow_backend.models.secret_model import SecretModelSchema from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel from spiffworkflow_backend.models.user import UserModel @@ -1105,15 +1106,9 @@ def add_secret(body: Dict) -> Response: ) -def update_secret( - service: str, - client: str, - secret: Optional[str] = None, - creator_user_id: Optional[int] = None, - allowed_process: Optional[str] = None, -) -> None: +def update_secret(key: str, body: dict) -> Response: """Update secret.""" - ... + SecretService().update_secret(key, body['value'], body['creator_user_id']) def delete_secret(key: str) -> None: @@ -1122,6 +1117,15 @@ def delete_secret(key: str) -> None: SecretService.delete_secret(key, current_user.id) -def get_allowed_process_paths(service: str, client: str) -> Any: +def add_allowed_process_path(body: dict) -> Any: """Get allowed process paths.""" - ... + allowed_process_path = SecretService.add_allowed_process( + body['secret_id'], g.user.id, body["allowed_relative_path"] + ) + return Response(json.dumps(SecretAllowedProcessSchema().dump(allowed_process_path)), + status=201, mimetype="application/json") + + +def delete_allowed_process_path(allowed_process_path_id: int) -> Any: + """Get allowed process paths.""" + SecretService().delete_allowed_process(allowed_process_path_id, g.user.id) diff --git a/src/spiffworkflow_backend/services/secret_service.py b/src/spiffworkflow_backend/services/secret_service.py index d6fd253c..b55914c2 100644 --- a/src/spiffworkflow_backend/services/secret_service.py +++ b/src/spiffworkflow_backend/services/secret_service.py @@ -1,6 +1,8 @@ """Secret_service.""" +import logging from typing import Optional +from flask import current_app from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db from sqlalchemy.exc import IntegrityError @@ -8,6 +10,15 @@ from sqlalchemy.exc import IntegrityError from spiffworkflow_backend.models.secret_model import SecretAllowedProcessPathModel from spiffworkflow_backend.models.secret_model import SecretModel +# from cryptography.fernet import Fernet +# +# +# class EncryptionService: +# key = Fernet.generate_key() # this is your "password" +# cipher_suite = Fernet(key) +# encoded_text = cipher_suite.encrypt(b"Hello stackoverflow!") +# decoded_text = cipher_suite.decrypt(encoded_text) + class SecretService: """SecretService.""" @@ -22,8 +33,8 @@ class SecretService: """Decrypt key.""" ... + @staticmethod def add_secret( - self, key: str, value: str, creator_user_id: int, @@ -114,10 +125,10 @@ class SecretService: @staticmethod def add_allowed_process( - key: str, user_id: str, allowed_relative_path: str + secret_id: str, user_id: str, allowed_relative_path: str ) -> SecretAllowedProcessPathModel: """Add_allowed_process.""" - secret_model = SecretModel.query.filter(SecretModel.key == key).first() + secret_model = SecretModel.query.filter(SecretModel.id == secret_id).first() if secret_model: if secret_model.creator_user_id == user_id: secret_process_model = SecretAllowedProcessPathModel( @@ -142,7 +153,7 @@ class SecretService: # db.session.rollback() raise ApiError( code="add_allowed_process_error", - message=f"Could not create an allowed process for secret with key: {key} " + message=f"Could not create an allowed process for secret with key: {secret_model.key} " f"with path: {allowed_relative_path}. " f"Original error is {e}", ) from e @@ -150,13 +161,13 @@ class SecretService: else: raise ApiError( code="add_allowed_process_error", - message=f"User: {user_id} cannot modify the secret with key : {key}", + message=f"User: {user_id} cannot modify the secret with key : {secret_model.key}", status_code=401, ) else: raise ApiError( code="add_allowed_process_error", - message=f"Cannot add allowed process to secret with key: {key}. Resource does not exist.", + message=f"Cannot add allowed process to secret with key: {secret_id}. Resource does not exist.", status_code=404, ) @@ -170,6 +181,7 @@ class SecretService: secret = SecretModel.query.filter( SecretModel.id == allowed_process.secret_id ).first() + assert secret if secret.creator_user_id == user_id: db.session.delete(allowed_process) try: diff --git a/tests/spiffworkflow_backend/integration/test_secret_service.py b/tests/spiffworkflow_backend/integration/test_secret_service.py index 42851149..98aaa252 100644 --- a/tests/spiffworkflow_backend/integration/test_secret_service.py +++ b/tests/spiffworkflow_backend/integration/test_secret_service.py @@ -66,7 +66,7 @@ class SecretServiceTestHelpers(BaseTest): test_secret = self.add_test_secret(user) allowed_process_model = SecretService().add_allowed_process( - key=test_secret.key, + secret_id=test_secret.id, user_id=user.id, allowed_relative_path=process_model_relative_path, ) @@ -199,7 +199,7 @@ class TestSecretService(SecretServiceTestHelpers): process_model_info ) allowed_process_model = SecretService().add_allowed_process( - key=test_secret.key, + secret_id=test_secret.id, user_id=user.id, allowed_relative_path=process_model_relative_path, ) @@ -230,7 +230,7 @@ class TestSecretService(SecretServiceTestHelpers): process_model_info ) SecretService().add_allowed_process( - key=test_secret.key, + secret_id=test_secret.id, user_id=user.id, allowed_relative_path=process_model_relative_path, ) @@ -239,7 +239,7 @@ class TestSecretService(SecretServiceTestHelpers): with pytest.raises(ApiError) as ae: SecretService().add_allowed_process( - key=test_secret.key, + secret_id=test_secret.id, user_id=user.id, allowed_relative_path=process_model_relative_path, ) @@ -259,13 +259,13 @@ class TestSecretService(SecretServiceTestHelpers): test_secret = self.add_test_secret(user) with pytest.raises(ApiError) as ae: SecretService().add_allowed_process( - key=test_secret.key, + secret_id=test_secret.id, user_id=user.id + 1, allowed_relative_path=process_model_relative_path, ) assert ( ae.value.message - == f"User: {user.id+1} cannot modify the secret with key : test_key" + == f"User: {user.id+1} cannot modify the secret with key : {self.test_key}" ) def test_secret_add_allowed_process_bad_secret_fails( @@ -281,7 +281,7 @@ class TestSecretService(SecretServiceTestHelpers): with pytest.raises(ApiError) as ae: SecretService().add_allowed_process( - key=test_secret.key + "x", + secret_id=test_secret.id + 1, user_id=user.id, allowed_relative_path=process_model_relative_path, ) @@ -359,7 +359,6 @@ class TestSecretServiceApi(SecretServiceTestHelpers): assert secret["key"] == self.test_key assert secret["value"] == self.test_value assert secret["creator_user_id"] == user.id - print("test_add_secret") def test_get_secret( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None @@ -374,7 +373,25 @@ class TestSecretServiceApi(SecretServiceTestHelpers): assert secret_response assert secret_response.status_code == 200 assert secret_response.json == self.test_value - print("test_get_secret") + + def test_update_secret(self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None) -> None: + user = self.find_or_create_user() + self.add_test_secret(user) + secret = SecretService.get_secret(self.test_key) + assert secret == self.test_value + secret_model = SecretModel(key=self.test_key, + value="new_secret_value", + creator_user_id=user.id) + response = client.put( + f"/v1.0/secrets/{self.test_key}", + headers=self.logged_in_headers(user), + content_type="application/json", + data=json.dumps(SecretModelSchema().dump(secret_model)), + ) + assert response.status_code == 204 + + secret_model = SecretModel.query.filter(SecretModel.key == self.test_key).first() + assert secret_model.value == "new_secret_value" def test_delete_secret( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None @@ -392,7 +409,6 @@ class TestSecretServiceApi(SecretServiceTestHelpers): assert secret_response.status_code == 204 secret = SecretService.get_secret(self.test_key) assert secret is None - print("test_delete_secret") def test_delete_secret_bad_user( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None @@ -418,3 +434,47 @@ class TestSecretServiceApi(SecretServiceTestHelpers): ) assert secret_response.status_code == 404 print("test_delete_secret_bad_key") + + def test_add_secret_allowed_process(self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None) -> None: + """Test add secret allowed process""" + user = self.find_or_create_user() + test_secret = self.add_test_secret(user) + process_model_info = self.add_test_process(client, user) + process_model_relative_path = FileSystemService.process_model_relative_path( + process_model_info + ) + data = {"secret_id": test_secret.id, + "allowed_relative_path": process_model_relative_path + } + response: TestResponse = client.post( + "/v1.0/secrets/allowed_process_paths", + headers=self.logged_in_headers(user), + content_type='application/json', + data=json.dumps(data) + ) + assert response.status_code == 201 + allowed_processes = SecretAllowedProcessPathModel.query.all() + assert len(allowed_processes) == 1 + assert allowed_processes[0].allowed_relative_path == process_model_relative_path + assert allowed_processes[0].secret_id == test_secret.id + + def test_delete_secret_allowed_process(self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None) -> None: + """Test delete secret allowed process""" + user = self.find_or_create_user() + test_secret = self.add_test_secret(user) + process_model_info = self.add_test_process(client, user) + process_model_relative_path = FileSystemService.process_model_relative_path( + process_model_info + ) + allowed_process = SecretService.add_allowed_process(test_secret.id, user.id, process_model_relative_path) + allowed_processes = SecretAllowedProcessPathModel.query.all() + assert len(allowed_processes) == 1 + assert allowed_processes[0].secret_id == test_secret.id + assert allowed_processes[0].allowed_relative_path == process_model_relative_path + response = client.delete( + f"/v1.0/secrets/allowed_process_paths/{allowed_process.id}", + headers=self.logged_in_headers(user), + ) + assert response.status_code == 204 + allowed_processes = SecretAllowedProcessPathModel.query.all() + assert len(allowed_processes) == 0