Skip to content

Commit 55aeed7

Browse files
authored
feat: CCP-4720: Implement public form submission privacy (#1892)
* CCP-4720: add form submission privacy options * CCP-4720: prevent error for auth user viewing private success page. * CCP-4720: SImplify - SonarQube flagged nesting
1 parent b41f3dd commit 55aeed7

30 files changed

Lines changed: 1616 additions & 165 deletions

File tree

.devcontainer/chefs_local/local.sample.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
},
6767
"encryption": {
6868
"proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7"
69-
}
69+
},
70+
"submissionTokenKey": "REPLACE_ME_WITH_32_PLUS_RANDOM_BYTES_e.g._openssl_rand_-hex_32"
7071
},
7172
"eventStreamService": {
7273
"servers": "localhost:4222,localhost:4223,localhost:4224",

.github/actions/deploy-to-environment/action.yaml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,21 @@ runs:
9191
9292
- name: Deploy App Secret
9393
shell: bash
94-
run: >-
95-
oc get --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} secret chefs-${{ inputs.job_name }}-secret || oc process --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f openshift/app.secret.yaml -p INSTANCE=${{ inputs.job_name }} | oc create --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f -
94+
env:
95+
NAMESPACE: ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }}
96+
JOB_NAME: ${{ inputs.job_name }}
97+
run: |
98+
set -euo pipefail
99+
100+
SECRET_NAME="chefs-${JOB_NAME}-secret"
101+
if ! oc get --namespace "${NAMESPACE}" secret "${SECRET_NAME}" >/dev/null 2>&1; then
102+
oc process --namespace "${NAMESPACE}" -f openshift/app.secret.yaml -p INSTANCE="${JOB_NAME}" | oc create --namespace "${NAMESPACE}" -f -
103+
fi
104+
105+
SUBMISSION_TOKEN_KEY="$(oc get --namespace "${NAMESPACE}" secret "${SECRET_NAME}" -o jsonpath='{.data.submissiontokenkey}' | base64 -d || true)"
106+
if [[ ${#SUBMISSION_TOKEN_KEY} -lt 32 ]]; then
107+
oc patch --namespace "${NAMESPACE}" secret "${SECRET_NAME}" --type merge -p "{\"stringData\":{\"submissiontokenkey\":\"$(openssl rand -hex 32)\"}}"
108+
fi
96109
97110
- name: Deploy app ConfigMaps
98111
shell: bash

app/config/custom-environment-variables.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@
6868
"port": "SERVER_PORT",
6969
"encryption": {
7070
"proxy": "SERVER_ENCRYPTION_PROXY"
71-
}
71+
},
72+
"submissionTokenKey": "SUBMISSION_TOKEN_KEY",
73+
"submissionTokenTtlMinutes": "SUBMISSION_TOKEN_TTL_MINUTES"
7274
},
7375
"eventStreamService": {
7476
"servers": "EVENTSTREAMSERVICE_SERVERS",

app/config/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
},
7373
"encryption": {
7474
"proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7"
75-
}
75+
},
76+
"submissionTokenTtlMinutes": "15"
7677
},
7778
"eventStreamService": {
7879
"servers": "localhost:4222,localhost:4223,localhost:4224",

app/config/test.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"oidc": {
3030
"clientSecret": "password"
3131
},
32-
"logLevel": "silent"
32+
"logLevel": "silent",
33+
"submissionTokenKey": "test-only-submission-token-key-32-bytes-min-do-not-use-anywhere-else"
3334
},
3435
"serviceClient": {
3536
"commonServices": {

app/frontend/src/components/designer/FormDesigner.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ async function schemaCreateNew() {
417417
enableSubmitterRevision: form.value.enableSubmitterRevision,
418418
showAssigneeInSubmissionsTable: form.value.showAssigneeInSubmissionsTable,
419419
showSubmissionConfirmation: form.value.showSubmissionConfirmation,
420+
enableSubmitterEmailReceipt: form.value.enableSubmitterEmailReceipt,
421+
enableSubmissionUrlSharing: form.value.enableSubmissionUrlSharing,
422+
hideSubmissionContentOnSuccess: form.value.hideSubmissionContentOnSuccess,
420423
submissionReceivedEmails: form.value.submissionReceivedEmails,
421424
submissionPackageSettings: form.value.submissionPackageSettings,
422425
reminder_enabled: false,

app/frontend/src/components/designer/FormViewer.vue

Lines changed: 104 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const { t, locale } = useI18n({ useScope: 'global' });
3434
3535
const router = useRouter();
3636
37-
const emit = defineEmits(['submission-updated']);
37+
const emit = defineEmits(['submission-updated', 'access-denied']);
3838
3939
const properties = defineProps({
4040
displayTitle: {
@@ -365,18 +365,34 @@ async function getFormData() {
365365
permissions.value = permRes.data[0] ? permRes.data[0].permissions : [];
366366
}
367367
} catch (error) {
368-
notificationStore.addNotification({
369-
text: t('trans.formViewer.getUsersSubmissionsErrMsg'),
370-
consoleError: t('trans.formViewer.getUsersSubmissionsConsoleErrMsg', {
371-
submissionId: properties.submissionId,
372-
error: error,
373-
}),
374-
});
368+
handleGetFormDataError(error);
375369
} finally {
376370
loadingSubmission.value = false;
377371
}
378372
}
379373
374+
// Sharing-off + 401 is the "forwarded success URL, viewer isn't on the form
375+
// team" case. Success.vue listens on `access-denied` and falls back to the
376+
// static confirmation block; suppressing the notification (and the follow-on
377+
// calls) avoids a burst of misleading errors for what is really a known
378+
// "you can't view this submission" state.
379+
function handleGetFormDataError(error) {
380+
if (
381+
error.response?.status === 401 &&
382+
formStore.form.enableSubmissionUrlSharing === false
383+
) {
384+
emit('access-denied');
385+
return;
386+
}
387+
notificationStore.addNotification({
388+
text: t('trans.formViewer.getUsersSubmissionsErrMsg'),
389+
consoleError: t('trans.formViewer.getUsersSubmissionsConsoleErrMsg', {
390+
submissionId: properties.submissionId,
391+
error: error,
392+
}),
393+
});
394+
}
395+
380396
async function setProxyHeaders() {
381397
try {
382398
let response = await formService.getProxyHeaders({
@@ -397,87 +413,92 @@ async function setProxyHeaders() {
397413
// Get the form definition/schema
398414
async function getFormSchema() {
399415
try {
400-
let response = undefined;
401416
if (properties.versionId) {
402-
versionIdToSubmitTo.value = properties.versionId;
403-
// If getting for a specific older version of the form
404-
response = await formService.readVersion(
405-
properties.formId,
406-
properties.versionId
407-
);
408-
if (!response.data || !response.data.schema) {
409-
throw new Error(
410-
t('trans.formViewer.readVersionErrMsg', {
411-
versionId: properties.versionId,
412-
})
413-
);
414-
}
415-
form.value = response.data;
416-
version.value = response.data.version;
417-
formSchema.value = response.data.schema;
418-
} else if (properties.draftId) {
419-
// If getting for a specific draft version of the form for preview
420-
response = await formService.readDraft(
421-
properties.formId,
422-
properties.draftId
423-
);
424-
if (!response.data || !response.data.schema) {
425-
throw new Error(
426-
t('trans.formViewer.readDraftErrMsg', {
427-
draftId: properties.draftId,
428-
})
429-
);
430-
}
431-
form.value = response.data;
432-
formSchema.value = response.data.schema;
433-
} else {
434-
// If getting the HEAD form version (IE making a new submission)
435-
response = await formService.readPublished(properties.formId);
436-
if (
437-
!response ||
438-
!response.data ||
439-
!response.data.versions ||
440-
!response.data.versions[0]
441-
) {
442-
router.push({
443-
name: 'Alert',
444-
query: {
445-
text: t('trans.formViewer.alertRouteMsg'),
446-
type: 'info',
447-
},
448-
});
449-
return;
450-
}
451-
form.value = response.data;
452-
version.value = response.data.versions[0].version;
453-
versionIdToSubmitTo.value = response.data.versions[0].id;
454-
formSchema.value = response.data.versions[0].schema;
455-
456-
if (response.data.schedule && response.data.schedule.expire) {
457-
let formScheduleStatus = response.data.schedule;
458-
isFormScheduleExpired.value = formScheduleStatus.expire;
459-
isLateSubmissionAllowed.value = formScheduleStatus.allowLateSubmissions;
460-
}
417+
await loadFormByVersion();
418+
return;
461419
}
462-
} catch (error) {
463-
if (authenticated.value) {
464-
// if 401 error, the user is not authorized to view the form
465-
if (error.response && error.response.status === 401) {
466-
isAuthorized.value = false;
467-
} else {
468-
// throw a generic error message
469-
notificationStore.addNotification({
470-
text: t('trans.formViewer.fecthingFormErrMsg'),
471-
consoleError: t('trans.formViewer.fecthingFormConsoleErrMsg', {
472-
versionId: properties.versionId,
473-
error: error,
474-
}),
475-
});
476-
}
420+
if (properties.draftId) {
421+
await loadFormByDraft();
422+
return;
477423
}
424+
await loadPublishedForm();
425+
} catch (error) {
426+
handleGetFormSchemaError(error);
478427
}
479428
}
480429
430+
async function loadFormByVersion() {
431+
versionIdToSubmitTo.value = properties.versionId;
432+
const response = await formService.readVersion(
433+
properties.formId,
434+
properties.versionId
435+
);
436+
if (!response.data || !response.data.schema) {
437+
throw new Error(
438+
t('trans.formViewer.readVersionErrMsg', {
439+
versionId: properties.versionId,
440+
})
441+
);
442+
}
443+
form.value = response.data;
444+
version.value = response.data.version;
445+
formSchema.value = response.data.schema;
446+
}
447+
448+
async function loadFormByDraft() {
449+
const response = await formService.readDraft(
450+
properties.formId,
451+
properties.draftId
452+
);
453+
if (!response.data || !response.data.schema) {
454+
throw new Error(
455+
t('trans.formViewer.readDraftErrMsg', {
456+
draftId: properties.draftId,
457+
})
458+
);
459+
}
460+
form.value = response.data;
461+
formSchema.value = response.data.schema;
462+
}
463+
464+
async function loadPublishedForm() {
465+
const response = await formService.readPublished(properties.formId);
466+
if (!response?.data?.versions?.[0]) {
467+
router.push({
468+
name: 'Alert',
469+
query: {
470+
text: t('trans.formViewer.alertRouteMsg'),
471+
type: 'info',
472+
},
473+
});
474+
return;
475+
}
476+
form.value = response.data;
477+
version.value = response.data.versions[0].version;
478+
versionIdToSubmitTo.value = response.data.versions[0].id;
479+
formSchema.value = response.data.versions[0].schema;
480+
if (response.data.schedule?.expire) {
481+
isFormScheduleExpired.value = response.data.schedule.expire;
482+
isLateSubmissionAllowed.value = response.data.schedule.allowLateSubmissions;
483+
}
484+
}
485+
486+
function handleGetFormSchemaError(error) {
487+
// Silent for anonymous viewers (public forms rendered without auth).
488+
if (!authenticated.value) return;
489+
if (error.response?.status === 401) {
490+
isAuthorized.value = false;
491+
return;
492+
}
493+
notificationStore.addNotification({
494+
text: t('trans.formViewer.fecthingFormErrMsg'),
495+
consoleError: t('trans.formViewer.fecthingFormConsoleErrMsg', {
496+
versionId: properties.versionId,
497+
error: error,
498+
}),
499+
});
500+
}
501+
481502
function isProcessingMultiUpload(e) {
482503
block.value = e;
483504
}

0 commit comments

Comments
 (0)