Skip to content

Commit 32204ae

Browse files
fix: non-owner roles can hit the Manage Form page (#1937)
1 parent 29456a9 commit 32204ae

4 files changed

Lines changed: 113 additions & 15 deletions

File tree

app/frontend/src/components/forms/manage/ManageForm.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ const canEditForm = computed(() =>
4242
permissions.value.includes(FormPermissions.FORM_UPDATE)
4343
);
4444
45+
// The version/design history list is only returned to designers (backend gates
46+
// it on design_create). Gate the panel on DESIGN_READ so non-designer roles
47+
// (e.g. team_manager) don't see a section rendered with missing version data.
48+
const canViewDesignHistory = computed(() =>
49+
permissions.value.includes(FormPermissions.DESIGN_READ)
50+
);
51+
4552
const combinedVersionAndDraftCount = computed(() => {
4653
return (
4754
(form.value?.versions ? form.value.versions.length : 0) +
@@ -342,7 +349,11 @@ defineExpose({
342349
</v-expansion-panels>
343350
344351
<!-- Form Design -->
345-
<v-expansion-panels v-model="versionsPanel" class="nrmc-expand-collapse">
352+
<v-expansion-panels
353+
v-if="canViewDesignHistory"
354+
v-model="versionsPanel"
355+
class="nrmc-expand-collapse"
356+
>
346357
<v-expansion-panel flat>
347358
<v-expansion-panel-title>
348359
<div

app/frontend/src/components/forms/manage/ManageLayout.vue

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import { useI18n } from 'vue-i18n';
55
66
import ManageForm from '~/components/forms/manage/ManageForm.vue';
77
import ManageFormActions from '~/components/forms/manage/ManageFormActions.vue';
8-
import { useNotificationStore } from '~/store/notification';
98
import { useFormStore } from '~/store/form';
109
import { useRecordsManagementStore } from '~/store/recordsManagement';
1110
import { FormPermissions } from '~/utils/constants';
1211
13-
const { locale, t } = useI18n({ useScope: 'global' });
12+
const { locale } = useI18n({ useScope: 'global' });
1413
1514
const properties = defineProps({
1615
f: {
@@ -21,7 +20,6 @@ const properties = defineProps({
2120
2221
const loading = ref(true);
2322
24-
const notificationStore = useNotificationStore();
2523
const recordsManagementStore = useRecordsManagementStore();
2624
2725
const { form, permissions, isRTL } = storeToRefs(useFormStore());
@@ -31,17 +29,15 @@ onMounted(async () => {
3129
3230
const formStore = useFormStore();
3331
34-
await formStore.fetchForm(properties.f);
35-
36-
if (formStore.form.versions) {
37-
await formStore.getFormPermissionsForUser(properties.f);
38-
} else {
39-
notificationStore.addNotification({
40-
text: t('trans.baseSecure.401UnAuthorizedErrMsg'),
41-
});
42-
}
43-
44-
await recordsManagementStore.getFormRetentionPolicy(properties.f);
32+
// Access to this page is already enforced by BaseSecure (IDP permission) and
33+
// the backend form_read middleware, so anyone who reaches here is authorized.
34+
// Load the user's form permissions unconditionally; the version list is only
35+
// returned to designers, so it must not be used as an authorization signal.
36+
await Promise.all([
37+
formStore.fetchForm(properties.f),
38+
formStore.getFormPermissionsForUser(properties.f),
39+
recordsManagementStore.getFormRetentionPolicy(properties.f),
40+
]);
4541
4642
if (permissions.value.includes(FormPermissions.DESIGN_READ))
4743
await formStore.fetchDrafts(properties.f);

app/frontend/tests/unit/components/forms/manage/ManageForm.spec.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,61 @@ describe('ManageForm.vue', () => {
123123
expect(wrapper.vm.currentVersion).toEqual('N/A');
124124
});
125125

126+
it('hides the Form Design History panel without DESIGN_READ (e.g. team_manager)', async () => {
127+
formStore.permissions = [
128+
FormPermissions.FORM_READ,
129+
FormPermissions.TEAM_UPDATE,
130+
];
131+
const wrapper = mount(ManageForm, {
132+
global: {
133+
plugins: [router, pinia],
134+
mocks: {
135+
$filters: {
136+
formatDate: vi.fn().mockReturnValue('formatted date'),
137+
},
138+
},
139+
provide: {
140+
formDesigner: false,
141+
draftId: '123-456',
142+
formId: '123-456',
143+
},
144+
stubs: STUBS,
145+
},
146+
});
147+
148+
await flushPromises();
149+
150+
expect(
151+
wrapper.find('[data-test="canExpandFormDesignHistoryPanel"]').exists()
152+
).toBe(false);
153+
});
154+
155+
it('shows the Form Design History panel with DESIGN_READ', async () => {
156+
formStore.permissions = [FormPermissions.DESIGN_READ];
157+
const wrapper = mount(ManageForm, {
158+
global: {
159+
plugins: [router, pinia],
160+
mocks: {
161+
$filters: {
162+
formatDate: vi.fn().mockReturnValue('formatted date'),
163+
},
164+
},
165+
provide: {
166+
formDesigner: false,
167+
draftId: '123-456',
168+
formId: '123-456',
169+
},
170+
stubs: STUBS,
171+
},
172+
});
173+
174+
await flushPromises();
175+
176+
expect(
177+
wrapper.find('[data-test="canExpandFormDesignHistoryPanel"]').exists()
178+
).toBe(true);
179+
});
180+
126181
it('currentVersion returns the current published version', async () => {
127182
formStore.form = ref({
128183
versions: [

app/frontend/tests/unit/components/forms/manage/ManageLayout.spec.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { beforeEach, expect, vi } from 'vitest';
77
import getRouter from '~/router';
88
import ManageLayout from '~/components/forms/manage/ManageLayout.vue';
99
import { useFormStore } from '~/store/form';
10+
import { useNotificationStore } from '~/store/notification';
1011
import { FormPermissions } from '~/utils/constants';
1112
import { ref } from 'vue';
1213
import { useAppStore } from '~/store/app';
@@ -127,4 +128,39 @@ describe('ManageLayout.vue', () => {
127128
expect(getFormPermissionsForUserSpy).toHaveBeenCalledTimes(1);
128129
expect(fetchDraftsSpy).toHaveBeenCalledTimes(1);
129130
});
131+
132+
// Regression: CCP-5326. The backend omits the `versions` list for non-designer
133+
// roles (e.g. team_manager, who lack design_create), so the fetched form has no
134+
// `versions` key. That must not be treated as "no permission" — the user is
135+
// already authorized by BaseSecure + backend form_read middleware to be here.
136+
it('loads permissions and shows no unauthorized alert when the form has no versions', async () => {
137+
formStore.fetchForm.mockImplementation(() => {
138+
// Mimic readForm returning a form payload without a `versions` key.
139+
formStore.form = { id: 'f', name: 'myForm' };
140+
});
141+
const getFormPermissionsForUserSpy = vi.spyOn(
142+
formStore,
143+
'getFormPermissionsForUser'
144+
);
145+
const notificationStore = useNotificationStore(pinia);
146+
const addNotificationSpy = vi.spyOn(notificationStore, 'addNotification');
147+
148+
mount(ManageLayout, {
149+
props: {
150+
f: 'f',
151+
},
152+
global: {
153+
plugins: [router, pinia],
154+
stubs: {
155+
ManageFormActions: true,
156+
ManageForm: true,
157+
},
158+
},
159+
});
160+
161+
await flushPromises();
162+
163+
expect(getFormPermissionsForUserSpy).toHaveBeenCalledTimes(1);
164+
expect(addNotificationSpy).not.toHaveBeenCalled();
165+
});
130166
});

0 commit comments

Comments
 (0)