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:
jasquat 2024-04-09 17:44:47 +00:00 committed by GitHub
parent 9458500b8a
commit 293aa867a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 139 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
</> </>
)} )}