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
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

View File

@ -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"]

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 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)

View File

@ -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:

View File

@ -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