secrets api

This commit is contained in:
mike cullerton 2022-09-22 16:29:08 -04:00
parent 77259f07f5
commit 65d163ed39
5 changed files with 189 additions and 83 deletions

View File

@ -8,6 +8,8 @@ servers:
- url: http://localhost:5000/v1.0 - url: http://localhost:5000/v1.0
security: security:
- jwt: ["secret"] - jwt: ["secret"]
# - oAuth2AuthCode:
# - read_email
paths: paths:
/login: /login:
@ -83,21 +85,8 @@ paths:
"200": "200":
description: Logout Authenticated User description: Logout Authenticated User
/login_swagger: /login_api:
parameters: 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 - name: redirect_url
in: query in: query
required: false required: false
@ -105,13 +94,38 @@ paths:
type: string type: string
get: get:
security: [] security: []
operationId: spiffworkflow_backend.routes.user.api_login operationId: spiffworkflow_backend.routes.user.login_api
summary: Authenticate user for API access summary: Authenticate user for API access
tags: tags:
- Authentication - Authentication
responses: responses:
"304": "304":
description: Redirection to the hosted frontend with an auth_token header. 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: /status:
get: get:
@ -208,7 +222,7 @@ paths:
description: The process group was deleted. description: The process group was deleted.
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_update operationId: spiffworkflow_backend.routes.process_api_blueprint.process_group_update
summary: Upates a single process group summary: Updates a single process group
tags: tags:
- Process Groups - Process Groups
requestBody: requestBody:
@ -942,11 +956,6 @@ paths:
- Messages - Messages
operationId: spiffworkflow_backend.routes.process_api_blueprint.message_instance_list operationId: spiffworkflow_backend.routes.process_api_blueprint.message_instance_list
summary: Get a list of message instances summary: Get a list of message instances
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
responses: responses:
"200": "200":
description: One task description: One task
@ -1099,42 +1108,40 @@ paths:
"404": "404":
description: Secret does not exist description: Secret does not exist
/secrets/{service}/{client}/allowed_process_paths: /secrets/allowed_process_paths:
parameters: post:
- name: service operationId: spiffworkflow_backend.routes.process_api_blueprint.add_allowed_process_path
in: path summary: Create an allowed process to a secret
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
tags: tags:
- Secrets - Secrets
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/SecretAllowedProcessPath"
responses: responses:
"200": "201":
description: Allowed process paths returned description: Allowed process created successfully
content: content:
application/json: application/json:
schema: schema:
type: array $ref: "#/components/schemas/SecretAllowedProcessPath"
items: /secrets/allowed_process_paths/{allowed_process_path_id}:
$ref: "#/components/schemas/SecretAllowedProcessPath" parameters:
# delete: - name: allowed_process_path_id
# operationId: spiffworkflow_backend.routes.process_api_blueprint.delete_allowed_process_path 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: components:
securitySchemes: securitySchemes:
@ -1144,6 +1151,18 @@ components:
bearerFormat: JWT bearerFormat: JWT
x-bearerInfoFunc: spiffworkflow_backend.routes.user.verify_token x-bearerInfoFunc: spiffworkflow_backend.routes.user.verify_token
x-scopeValidateFunc: spiffworkflow_backend.routes.user.validate_scope 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: schemas:
OkTrue: OkTrue:
properties: properties:
@ -1864,16 +1883,16 @@ components:
example: ["a_file.txt", "b_file.txt"] example: ["a_file.txt", "b_file.txt"]
Secret: Secret:
properties: properties:
value:
description: The value associated with the key
type: string
example: my_super_secret_value
nullable: false
key: key:
description: The key of the secret we want to use description: The key of the secret we want to use
type: string type: string
example: my_secret_key example: my_secret_key
nullable: false nullable: false
value:
description: The value associated with the key
type: string
example: my_super_secret_value
nullable: false
creator_user_id: creator_user_id:
description: The id of the logged in user that created this secret description: The id of the logged in user that created this secret
type: number type: number
@ -1922,10 +1941,12 @@ components:
description: The id of the allowed process path description: The id of the allowed process path
type: number type: number
example: 1 example: 1
nullable: true
secret_id: secret_id:
description: The id of the secret associated with this allowed process path description: The id of the secret associated with this allowed process path
type: number type: number
example: 2 example: 2
allowed_process_path: allowed_relative_path:
description: The allowed process path description: The allowed process path
type: string
example: /group_one/group_two/model_a example: /group_one/group_two/model_a

View File

@ -46,3 +46,12 @@ class SecretAllowedProcessPathModel(SpiffworkflowBaseDBModel):
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
secret_id: int = db.Column(ForeignKey(SecretModel.id), nullable=False) # type: ignore secret_id: int = db.Column(ForeignKey(SecretModel.id), nullable=False) # type: ignore
allowed_relative_path: str = db.Column(db.String(500), nullable=False) 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"]

View File

@ -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 ProcessModelInfo
from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema 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.secret_model import SecretModelSchema
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
@ -1105,15 +1106,9 @@ def add_secret(body: Dict) -> Response:
) )
def update_secret( def update_secret(key: str, body: dict) -> Response:
service: str,
client: str,
secret: Optional[str] = None,
creator_user_id: Optional[int] = None,
allowed_process: Optional[str] = None,
) -> None:
"""Update secret.""" """Update secret."""
... SecretService().update_secret(key, body['value'], body['creator_user_id'])
def delete_secret(key: str) -> None: def delete_secret(key: str) -> None:
@ -1122,6 +1117,15 @@ def delete_secret(key: str) -> None:
SecretService.delete_secret(key, current_user.id) 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.""" """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)

View File

@ -1,6 +1,8 @@
"""Secret_service.""" """Secret_service."""
import logging
from typing import Optional from typing import Optional
from flask import current_app
from flask_bpmn.api.api_error import ApiError from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from sqlalchemy.exc import IntegrityError 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 SecretAllowedProcessPathModel
from spiffworkflow_backend.models.secret_model import SecretModel 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: class SecretService:
"""SecretService.""" """SecretService."""
@ -22,8 +33,8 @@ class SecretService:
"""Decrypt key.""" """Decrypt key."""
... ...
@staticmethod
def add_secret( def add_secret(
self,
key: str, key: str,
value: str, value: str,
creator_user_id: int, creator_user_id: int,
@ -114,10 +125,10 @@ class SecretService:
@staticmethod @staticmethod
def add_allowed_process( def add_allowed_process(
key: str, user_id: str, allowed_relative_path: str secret_id: str, user_id: str, allowed_relative_path: str
) -> SecretAllowedProcessPathModel: ) -> SecretAllowedProcessPathModel:
"""Add_allowed_process.""" """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:
if secret_model.creator_user_id == user_id: if secret_model.creator_user_id == user_id:
secret_process_model = SecretAllowedProcessPathModel( secret_process_model = SecretAllowedProcessPathModel(
@ -142,7 +153,7 @@ class SecretService:
# db.session.rollback() # db.session.rollback()
raise ApiError( raise ApiError(
code="add_allowed_process_error", 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"with path: {allowed_relative_path}. "
f"Original error is {e}", f"Original error is {e}",
) from e ) from e
@ -150,13 +161,13 @@ class SecretService:
else: else:
raise ApiError( raise ApiError(
code="add_allowed_process_error", 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, status_code=401,
) )
else: else:
raise ApiError( raise ApiError(
code="add_allowed_process_error", 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, status_code=404,
) )
@ -170,6 +181,7 @@ class SecretService:
secret = SecretModel.query.filter( secret = SecretModel.query.filter(
SecretModel.id == allowed_process.secret_id SecretModel.id == allowed_process.secret_id
).first() ).first()
assert secret
if secret.creator_user_id == user_id: if secret.creator_user_id == user_id:
db.session.delete(allowed_process) db.session.delete(allowed_process)
try: try:

View File

@ -66,7 +66,7 @@ class SecretServiceTestHelpers(BaseTest):
test_secret = self.add_test_secret(user) test_secret = self.add_test_secret(user)
allowed_process_model = SecretService().add_allowed_process( allowed_process_model = SecretService().add_allowed_process(
key=test_secret.key, secret_id=test_secret.id,
user_id=user.id, user_id=user.id,
allowed_relative_path=process_model_relative_path, allowed_relative_path=process_model_relative_path,
) )
@ -199,7 +199,7 @@ class TestSecretService(SecretServiceTestHelpers):
process_model_info process_model_info
) )
allowed_process_model = SecretService().add_allowed_process( allowed_process_model = SecretService().add_allowed_process(
key=test_secret.key, secret_id=test_secret.id,
user_id=user.id, user_id=user.id,
allowed_relative_path=process_model_relative_path, allowed_relative_path=process_model_relative_path,
) )
@ -230,7 +230,7 @@ class TestSecretService(SecretServiceTestHelpers):
process_model_info process_model_info
) )
SecretService().add_allowed_process( SecretService().add_allowed_process(
key=test_secret.key, secret_id=test_secret.id,
user_id=user.id, user_id=user.id,
allowed_relative_path=process_model_relative_path, allowed_relative_path=process_model_relative_path,
) )
@ -239,7 +239,7 @@ class TestSecretService(SecretServiceTestHelpers):
with pytest.raises(ApiError) as ae: with pytest.raises(ApiError) as ae:
SecretService().add_allowed_process( SecretService().add_allowed_process(
key=test_secret.key, secret_id=test_secret.id,
user_id=user.id, user_id=user.id,
allowed_relative_path=process_model_relative_path, allowed_relative_path=process_model_relative_path,
) )
@ -259,13 +259,13 @@ class TestSecretService(SecretServiceTestHelpers):
test_secret = self.add_test_secret(user) test_secret = self.add_test_secret(user)
with pytest.raises(ApiError) as ae: with pytest.raises(ApiError) as ae:
SecretService().add_allowed_process( SecretService().add_allowed_process(
key=test_secret.key, secret_id=test_secret.id,
user_id=user.id + 1, user_id=user.id + 1,
allowed_relative_path=process_model_relative_path, allowed_relative_path=process_model_relative_path,
) )
assert ( assert (
ae.value.message 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( def test_secret_add_allowed_process_bad_secret_fails(
@ -281,7 +281,7 @@ class TestSecretService(SecretServiceTestHelpers):
with pytest.raises(ApiError) as ae: with pytest.raises(ApiError) as ae:
SecretService().add_allowed_process( SecretService().add_allowed_process(
key=test_secret.key + "x", secret_id=test_secret.id + 1,
user_id=user.id, user_id=user.id,
allowed_relative_path=process_model_relative_path, allowed_relative_path=process_model_relative_path,
) )
@ -359,7 +359,6 @@ class TestSecretServiceApi(SecretServiceTestHelpers):
assert secret["key"] == self.test_key assert secret["key"] == self.test_key
assert secret["value"] == self.test_value assert secret["value"] == self.test_value
assert secret["creator_user_id"] == user.id assert secret["creator_user_id"] == user.id
print("test_add_secret")
def test_get_secret( def test_get_secret(
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None 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
assert secret_response.status_code == 200 assert secret_response.status_code == 200
assert secret_response.json == self.test_value 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( def test_delete_secret(
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None 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 assert secret_response.status_code == 204
secret = SecretService.get_secret(self.test_key) secret = SecretService.get_secret(self.test_key)
assert secret is None assert secret is None
print("test_delete_secret")
def test_delete_secret_bad_user( def test_delete_secret_bad_user(
self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None 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 assert secret_response.status_code == 404
print("test_delete_secret_bad_key") 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