mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-28 09:00:44 +00:00
Auth for secrets (#1369)
* added new api to show secrets so we can use that in permissions * updated frontend to use new secret show value api * cleaned up secret_show method --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
9458500b8a
commit
293aa867a1
@ -17,6 +17,10 @@ function log_info() {
|
|||||||
# run migrations
|
# run migrations
|
||||||
export FLASK_APP=/app/src/spiffworkflow_backend
|
export FLASK_APP=/app/src/spiffworkflow_backend
|
||||||
|
|
||||||
|
if [[ -z "${FLASK_DEBUG:-}" ]]; then
|
||||||
|
export FLASK_DEBUG=0
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${SPIFFWORKFLOW_BACKEND_WAIT_FOR_DB_TO_BE_READY:-}" == "true" ]]; then
|
if [[ "${SPIFFWORKFLOW_BACKEND_WAIT_FOR_DB_TO_BE_READY:-}" == "true" ]]; then
|
||||||
echo 'Waiting for db to be ready...'
|
echo 'Waiting for db to be ready...'
|
||||||
poetry run python ./bin/wait_for_db_to_be_ready.py
|
poetry run python ./bin/wait_for_db_to_be_ready.py
|
||||||
|
@ -68,6 +68,7 @@ if [[ -z "${SPIFFWORKFLOW_BACKEND_ENV:-}" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
export FLASK_SESSION_SECRET_KEY="e7711a3ba96c46c68e084a86952de16f"
|
export FLASK_SESSION_SECRET_KEY="e7711a3ba96c46c68e084a86952de16f"
|
||||||
|
export FLASK_DEBUG=1
|
||||||
|
|
||||||
if [[ -z "${SPIFFWORKFLOW_BACKEND_RUN_BACKGROUND_SCHEDULER_IN_CREATE_APP:-}" ]]; then
|
if [[ -z "${SPIFFWORKFLOW_BACKEND_RUN_BACKGROUND_SCHEDULER_IN_CREATE_APP:-}" ]]; then
|
||||||
SPIFFWORKFLOW_BACKEND_RUN_BACKGROUND_SCHEDULER_IN_CREATE_APP=true
|
SPIFFWORKFLOW_BACKEND_RUN_BACKGROUND_SCHEDULER_IN_CREATE_APP=true
|
||||||
|
@ -2954,7 +2954,7 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
get:
|
get:
|
||||||
operationId: spiffworkflow_backend.routes.secrets_controller.secret_show
|
operationId: spiffworkflow_backend.routes.secrets_controller.secret_show
|
||||||
summary: Return a secret value for a key
|
summary: Return info about a secret for a key. Does not include the value.
|
||||||
tags:
|
tags:
|
||||||
- Secrets
|
- Secrets
|
||||||
responses:
|
responses:
|
||||||
@ -2998,6 +2998,27 @@ paths:
|
|||||||
"404":
|
"404":
|
||||||
description: Secret does not exist
|
description: Secret does not exist
|
||||||
|
|
||||||
|
/secrets/show-value/{key}:
|
||||||
|
parameters:
|
||||||
|
- name: key
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The key we are using
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
get:
|
||||||
|
operationId: spiffworkflow_backend.routes.secrets_controller.secret_show_value
|
||||||
|
summary: Return a secret value for a key
|
||||||
|
tags:
|
||||||
|
- Secrets
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: We return a secret
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
|
||||||
/permissions-check:
|
/permissions-check:
|
||||||
post:
|
post:
|
||||||
operationId: spiffworkflow_backend.routes.process_api_blueprint.permissions_check
|
operationId: spiffworkflow_backend.routes.process_api_blueprint.permissions_check
|
||||||
|
@ -16,6 +16,11 @@ from spiffworkflow_backend.services.user_service import UserService
|
|||||||
|
|
||||||
def secret_show(key: str) -> Response:
|
def secret_show(key: str) -> Response:
|
||||||
secret = SecretService.get_secret(key)
|
secret = SecretService.get_secret(key)
|
||||||
|
return make_response(jsonify(secret), 200)
|
||||||
|
|
||||||
|
|
||||||
|
def secret_show_value(key: str) -> Response:
|
||||||
|
secret = SecretService.get_secret(key)
|
||||||
|
|
||||||
# normal serialization does not include the secret value, but this is the one endpoint where we want to return the goods
|
# normal serialization does not include the secret value, but this is the one endpoint where we want to return the goods
|
||||||
secret_as_dict = secret.serialized()
|
secret_as_dict = secret.serialized()
|
||||||
|
@ -40,7 +40,7 @@ class TestSecretsController(SecretServiceTestHelpers):
|
|||||||
assert SecretService._decrypt(secret["value"]) == self.test_value
|
assert SecretService._decrypt(secret["value"]) == self.test_value
|
||||||
assert secret["user_id"] == with_super_admin_user.id
|
assert secret["user_id"] == with_super_admin_user.id
|
||||||
|
|
||||||
def test_get_secret(
|
def test_get_secret_api(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
client: FlaskClient,
|
client: FlaskClient,
|
||||||
@ -56,6 +56,24 @@ class TestSecretsController(SecretServiceTestHelpers):
|
|||||||
assert secret_response
|
assert secret_response
|
||||||
assert secret_response.status_code == 200
|
assert secret_response.status_code == 200
|
||||||
assert secret_response.json
|
assert secret_response.json
|
||||||
|
assert "value" not in secret_response.json
|
||||||
|
|
||||||
|
def test_get_secret_value(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
|
) -> None:
|
||||||
|
"""Test get secret."""
|
||||||
|
self.add_test_secret(with_super_admin_user)
|
||||||
|
secret_response = client.get(
|
||||||
|
f"/v1.0/secrets/show-value/{self.test_key}",
|
||||||
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
|
)
|
||||||
|
assert secret_response
|
||||||
|
assert secret_response.status_code == 200
|
||||||
|
assert secret_response.json
|
||||||
assert SecretService._decrypt(secret_response.json["value"]) == self.test_value
|
assert SecretService._decrypt(secret_response.json["value"]) == self.test_value
|
||||||
|
|
||||||
def test_update_secret(
|
def test_update_secret(
|
||||||
|
@ -37,15 +37,18 @@ export const useUriListForPermissions = () => {
|
|||||||
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
|
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
|
||||||
processModelTestsPath: `/v1.0/process-model-tests/run/${params.process_model_id}`,
|
processModelTestsPath: `/v1.0/process-model-tests/run/${params.process_model_id}`,
|
||||||
secretListPath: `/v1.0/secrets`,
|
secretListPath: `/v1.0/secrets`,
|
||||||
|
secretShowPath: `/v1.0/secrets/${params.secret_identifier}`,
|
||||||
|
secretShowValuePath: `/v1.0/secrets/show-value/${params.secret_identifier}`,
|
||||||
userSearch: `/v1.0/users/search`,
|
userSearch: `/v1.0/users/search`,
|
||||||
userExists: `/v1.0/users/exists/by-username`,
|
userExists: `/v1.0/users/exists/by-username`,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
params.process_model_id,
|
params.secret_identifier,
|
||||||
params.file_name,
|
params.file_name,
|
||||||
|
params.page_identifier,
|
||||||
params.process_group_id,
|
params.process_group_id,
|
||||||
params.process_instance_id,
|
params.process_instance_id,
|
||||||
params.page_identifier,
|
params.process_model_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { targetUris };
|
return { targetUris };
|
||||||
|
@ -16,8 +16,8 @@ export interface ApiActions {
|
|||||||
export interface Secret {
|
export interface Secret {
|
||||||
id: number;
|
id: number;
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
|
||||||
creator_user_id: string;
|
creator_user_id: string;
|
||||||
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Onboarding {
|
export interface Onboarding {
|
||||||
|
@ -98,7 +98,7 @@ export default function Configuration({ extensionUxElements }: OwnProps) {
|
|||||||
<Route path="/" element={<SecretList />} />
|
<Route path="/" element={<SecretList />} />
|
||||||
<Route path="secrets" element={<SecretList />} />
|
<Route path="secrets" element={<SecretList />} />
|
||||||
<Route path="secrets/new" element={<SecretNew />} />
|
<Route path="secrets/new" element={<SecretNew />} />
|
||||||
<Route path="secrets/:key" element={<SecretShow />} />
|
<Route path="secrets/:secret_identifier" element={<SecretShow />} />
|
||||||
<Route path="authentications" element={<AuthenticationList />} />
|
<Route path="authentications" element={<AuthenticationList />} />
|
||||||
<Route
|
<Route
|
||||||
path="extension/:page_identifier"
|
path="extension/:page_identifier"
|
||||||
|
@ -3,9 +3,12 @@ import { useParams, useNavigate } from 'react-router-dom';
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Stack, Table, Button, TextInput } from '@carbon/react';
|
import { Stack, Table, Button, TextInput } from '@carbon/react';
|
||||||
import HttpService from '../services/HttpService';
|
import HttpService from '../services/HttpService';
|
||||||
import { Secret } from '../interfaces';
|
import { PermissionsToCheck, Secret } from '../interfaces';
|
||||||
import { Notification } from '../components/Notification';
|
import { Notification } from '../components/Notification';
|
||||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||||
|
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||||
|
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||||
|
import { Can } from '../contexts/Can';
|
||||||
|
|
||||||
export default function SecretShow() {
|
export default function SecretShow() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -16,12 +19,21 @@ export default function SecretShow() {
|
|||||||
const [showSuccessNotification, setShowSuccessNotification] =
|
const [showSuccessNotification, setShowSuccessNotification] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
|
||||||
|
const { targetUris } = useUriListForPermissions();
|
||||||
|
const permissionRequestData: PermissionsToCheck = {
|
||||||
|
[targetUris.secretShowPath]: ['PUT', 'DELETE', 'GET'],
|
||||||
|
[targetUris.secretShowValuePath]: ['GET'],
|
||||||
|
};
|
||||||
|
const { ability, permissionsLoaded } = usePermissionFetcher(
|
||||||
|
permissionRequestData
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: `/secrets/${params.key}`,
|
path: `/secrets/${params.secret_identifier}`,
|
||||||
successCallback: setSecret,
|
successCallback: setSecret,
|
||||||
});
|
});
|
||||||
}, [params.key]);
|
}, [params.secret_identifier]);
|
||||||
|
|
||||||
const handleSecretValueChange = (event: any) => {
|
const handleSecretValueChange = (event: any) => {
|
||||||
if (secret) {
|
if (secret) {
|
||||||
@ -67,26 +79,63 @@ export default function SecretShow() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (secret) {
|
const handleShowSecretValue = () => {
|
||||||
|
if (secret === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: `/secrets/show-value/${secret.key}`,
|
||||||
|
successCallback: (result: Secret) => {
|
||||||
|
setSecret(result);
|
||||||
|
setDisplaySecretValue(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (secret && permissionsLoaded) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showSuccessNotification && successNotificationComponent}
|
{showSuccessNotification && successNotificationComponent}
|
||||||
<h1>Secret Key: {secret.key}</h1>
|
<h1>Secret Key: {secret.key}</h1>
|
||||||
<Stack orientation="horizontal" gap={3}>
|
<Stack orientation="horizontal" gap={3}>
|
||||||
|
<Can I="DELETE" a={targetUris.secretShowPath} ability={ability}>
|
||||||
<ButtonWithConfirmation
|
<ButtonWithConfirmation
|
||||||
description="Delete Secret?"
|
description="Delete Secret?"
|
||||||
onConfirmation={deleteSecret}
|
onConfirmation={deleteSecret}
|
||||||
buttonLabel="Delete"
|
buttonLabel="Delete"
|
||||||
/>
|
/>
|
||||||
|
</Can>
|
||||||
|
<Can
|
||||||
|
I="GET"
|
||||||
|
a={targetUris.secretShowValuePath}
|
||||||
|
ability={ability}
|
||||||
|
passThrough
|
||||||
|
>
|
||||||
|
{(secretReadAllowed: boolean) => {
|
||||||
|
if (secretReadAllowed) {
|
||||||
|
return (
|
||||||
<Button
|
<Button
|
||||||
disabled={displaySecretValue}
|
disabled={displaySecretValue}
|
||||||
variant="warning"
|
variant="warning"
|
||||||
onClick={() => {
|
onClick={handleShowSecretValue}
|
||||||
setDisplaySecretValue(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Retrieve secret value
|
Retrieve secret value
|
||||||
</Button>
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Can I="PUT" a={targetUris.secretShowPath} ability={ability}>
|
||||||
|
<Button
|
||||||
|
disabled={displaySecretValue}
|
||||||
|
variant="warning"
|
||||||
|
onClick={() => setDisplaySecretValue(true)}
|
||||||
|
>
|
||||||
|
Edit secret value
|
||||||
|
</Button>
|
||||||
|
</Can>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Can>
|
||||||
</Stack>
|
</Stack>
|
||||||
<div>
|
<div>
|
||||||
<Table striped bordered>
|
<Table striped bordered>
|
||||||
@ -103,7 +152,7 @@ export default function SecretShow() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{params.key}</td>
|
<td>{params.secret_identifier}</td>
|
||||||
{displaySecretValue && (
|
{displaySecretValue && (
|
||||||
<>
|
<>
|
||||||
<td aria-label="Secret value">
|
<td aria-label="Secret value">
|
||||||
@ -112,14 +161,23 @@ export default function SecretShow() {
|
|||||||
name="secret_value"
|
name="secret_value"
|
||||||
value={secret.value}
|
value={secret.value}
|
||||||
onChange={handleSecretValueChange}
|
onChange={handleSecretValueChange}
|
||||||
|
disabled={
|
||||||
|
!ability.can('PUT', targetUris.secretShowPath)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<Can
|
||||||
|
I="PUT"
|
||||||
|
a={targetUris.secretShowPath}
|
||||||
|
ability={ability}
|
||||||
|
>
|
||||||
{displaySecretValue && (
|
{displaySecretValue && (
|
||||||
<Button variant="warning" onClick={updateSecretValue}>
|
<Button variant="warning" onClick={updateSecretValue}>
|
||||||
Update Value
|
Update Value
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</Can>
|
||||||
</td>
|
</td>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user