From 4fa8967cbf23a1f4411ca4bcdc68913fe72770c8 Mon Sep 17 00:00:00 2001 From: Konrad Mohrfeldt <km@roko.li> Date: Fri, 24 Jan 2025 14:11:15 +0100 Subject: [PATCH] feat: validate actual permission strings This changes enables us to catch typos as well as missing or updated steering permissions in the dashboard code. --- .../generic/APermissionGuard.spec.ts | 9 +- src/components/generic/APermissionGuard.vue | 3 +- src/components/shows/TimeSlotRow.vue | 59 +++-- src/steering-types.ts | 233 +++++++++++++++++- src/types.ts | 2 + 5 files changed, 272 insertions(+), 34 deletions(-) diff --git a/src/components/generic/APermissionGuard.spec.ts b/src/components/generic/APermissionGuard.spec.ts index 2acaeeb4..d1a3c8f1 100644 --- a/src/components/generic/APermissionGuard.spec.ts +++ b/src/components/generic/APermissionGuard.spec.ts @@ -6,6 +6,7 @@ import { ref, h, nextTick } from 'vue' import { SteeringUser, useHasUserPermission } from '@/stores/auth' import APermissionGuard from './APermissionGuard.vue' +import { AnyPermission } from '@/types' function createGuard( content: @@ -14,7 +15,7 @@ function createGuard( disabled?: boolean areAllChildGuardsHidden: boolean }) => ReturnType<typeof h>), - props: { showPermissions?: string[]; editPermissions?: string[] }, + props: { showPermissions?: AnyPermission[]; editPermissions?: AnyPermission[] }, pinia: TestingPinia, ) { return mount(APermissionGuard, { @@ -89,6 +90,7 @@ describe('APermissionGuard', () => { const pinia = createPiniaWithFakeUserAuthStore() const guard = createGuard( 'Is disabled: {{ params.disabled ? "Yes" : "No" }}', + // @ts-expect-error my-edit-permission is not a real permission { editPermissions: ['my-edit-permission'] }, pinia, ) @@ -99,6 +101,7 @@ describe('APermissionGuard', () => { const pinia = createPiniaWithFakeUserAuthStore(['my-edit-permission']) const guard = createGuard( 'Is disabled: {{ "disabled" in params || params.disabled ? "Yes" : "No" }}', + // @ts-expect-error my-edit-permission is not a real permission { editPermissions: ['my-edit-permission'] }, pinia, ) @@ -107,12 +110,14 @@ describe('APermissionGuard', () => { test('Does not render content if show permissions are not granted.', () => { const pinia = createPiniaWithFakeUserAuthStore() + // @ts-expect-error my-show-permission is not a real permission const guard = createGuard('my content', { showPermissions: ['my-show-permission'] }, pinia) expect(guard.text()).toEqual('') }) test('Does render content if show permissions are granted.', () => { const pinia = createPiniaWithFakeUserAuthStore(['my-show-permission']) + // @ts-expect-error my-show-permission is not a real permission const guard = createGuard('my content', { showPermissions: ['my-show-permission'] }, pinia) expect(guard.text()).toEqual('my content') }) @@ -122,6 +127,7 @@ describe('APermissionGuard', () => { const guard = createGuard( ({ areAllChildGuardsHidden }) => { return h('div', [ + // @ts-expect-error my-show-permission is not a real permission h(APermissionGuard, { showPermissions: 'my-other-permission' }, () => ['test: ']), areAllChildGuardsHidden ? 'has-no-visible-guards' : 'has-visible-guards', ]) @@ -138,6 +144,7 @@ describe('APermissionGuard', () => { const guard = createGuard( ({ areAllChildGuardsHidden }) => { return h('div', [ + // @ts-expect-error my-show-permission is not a real permission h(APermissionGuard, { showPermissions: 'my-show-permission' }, () => ['test: ']), areAllChildGuardsHidden ? 'has-no-visible-guards' : 'has-visible-guards', ]) diff --git a/src/components/generic/APermissionGuard.vue b/src/components/generic/APermissionGuard.vue index 89c95552..00d0ddee 100644 --- a/src/components/generic/APermissionGuard.vue +++ b/src/components/generic/APermissionGuard.vue @@ -5,8 +5,9 @@ <script lang="ts"> import type { InjectionKey, Ref } from 'vue' import type { Authorizer } from '@/stores/auth' +import { AnyPermission } from '@/types' -type Permissions = Authorizer | string | string[] +type Permissions = Authorizer | AnyPermission | AnyPermission[] export type PermissionGuardProps = { showPermissions?: Permissions editPermissions?: Permissions diff --git a/src/components/shows/TimeSlotRow.vue b/src/components/shows/TimeSlotRow.vue index 11b261a1..55ac9bce 100644 --- a/src/components/shows/TimeSlotRow.vue +++ b/src/components/shows/TimeSlotRow.vue @@ -9,37 +9,35 @@ :is-repetition="Boolean(timeslot.repetitionOfId)" /> - <APermissionGuard show-permissions="program.edit__timeslot__episode"> - <button - type="button" - class="btn btn-default" - :class="{ - 'tw-aspect-square btn-sm': timeslot.episodeId, - }" - :popovertarget="episodePopoverId" - popovertargetaction="toggle" - :style="{ 'anchor-name': `--${episodePopoverId}` }" - > - <icon-pepicons-pencil-dots-y v-if="timeslot.episodeId" /> - <span :class="{ 'tw-sr-only': timeslot.episodeId }"> - {{ t('episode.labels.assign') }} - </span> - </button> + <button + type="button" + class="btn btn-default" + :class="{ + 'tw-aspect-square btn-sm': timeslot.episodeId, + }" + :popovertarget="episodePopoverId" + popovertargetaction="toggle" + :style="{ 'anchor-name': `--${episodePopoverId}` }" + > + <icon-pepicons-pencil-dots-y v-if="timeslot.episodeId" /> + <span :class="{ 'tw-sr-only': timeslot.episodeId }"> + {{ t('episode.labels.assign') }} + </span> + </button> - <AEpisodeAssignmentManager - :id="episodePopoverId" - v-model="episodeId.value" - :show-id="timeslot.showId" - class="anchored" - :class="{ - 'on-bottom-right': timeslot.episodeId, - 'on-bottom-left tw-mt-2': !timeslot.episodeId, - }" - :style="{ - 'position-anchor': `--${episodePopoverId}`, - }" - /> - </APermissionGuard> + <AEpisodeAssignmentManager + :id="episodePopoverId" + v-model="episodeId.value" + :show-id="timeslot.showId" + class="anchored" + :class="{ + 'on-bottom-right': timeslot.episodeId, + 'on-bottom-left tw-mt-2': !timeslot.episodeId, + }" + :style="{ + 'position-anchor': `--${episodePopoverId}`, + }" + /> </div> </td> <td> @@ -85,7 +83,6 @@ import { calculateDurationSeconds, secondsToDurationString } from '@/util' import { useEpisodeStore, useMediaStore, useTimeSlotStore } from '@/stores' import { useMediaState } from '@/stores/media' import AStatus from '@/components/generic/AStatus.vue' -import APermissionGuard from '@/components/generic/APermissionGuard.vue' import AEpisodeAssignmentManager from '@/components/episode/AEpisodeAssignmentManager.vue' import { useAPIObjectFieldCopy } from '@/form' import AEpisodeLink from '@/components/episode/AEpisodeLink.vue' diff --git a/src/steering-types.ts b/src/steering-types.ts index a156ebf4..a1c44636 100644 --- a/src/steering-types.ts +++ b/src/steering-types.ts @@ -1,6 +1,6 @@ /* eslint-disable */ /* - * This file was auto-generated by `make update-types` at 2025-01-23 19:23:45.859Z. + * This file was auto-generated by `make update-types` at 2025-01-24 12:56:47.495Z. * DO NOT make changes to this file. */ @@ -1738,6 +1738,237 @@ export interface components { permissions: readonly string[] profileIds?: number[] } + /** + * @description Permissions used by steering. + * @enum {string} + */ + AnyPermission: + | 'auth.add_group' + | 'auth.add_permission' + | 'auth.add_user' + | 'auth.change_group' + | 'auth.change_permission' + | 'auth.change_user' + | 'auth.delete_group' + | 'auth.delete_permission' + | 'auth.delete_user' + | 'auth.view_group' + | 'auth.view_permission' + | 'auth.view_user' + | 'program.add__file' + | 'program.add__import' + | 'program.add__line' + | 'program.add__m3ufile' + | 'program.add__stream' + | 'program.add_category' + | 'program.add_cba' + | 'program.add_contentlicense' + | 'program.add_episode' + | 'program.add_episodelink' + | 'program.add_fundingcategory' + | 'program.add_host' + | 'program.add_hostlink' + | 'program.add_image' + | 'program.add_language' + | 'program.add_license' + | 'program.add_licensetype' + | 'program.add_linktype' + | 'program.add_media' + | 'program.add_mediasource' + | 'program.add_musicfocus' + | 'program.add_note' + | 'program.add_notelink' + | 'program.add_playlist' + | 'program.add_playlistentry' + | 'program.add_profile' + | 'program.add_profilelink' + | 'program.add_radiosettings' + | 'program.add_rrule' + | 'program.add_schedule' + | 'program.add_show' + | 'program.add_showlink' + | 'program.add_timeslot' + | 'program.add_topic' + | 'program.add_type' + | 'program.add_userprofile' + | 'program.change_category' + | 'program.change_cba' + | 'program.change_contentlicense' + | 'program.change_episode' + | 'program.change_episodelink' + | 'program.change_fundingcategory' + | 'program.change_host' + | 'program.change_hostlink' + | 'program.change_image' + | 'program.change_language' + | 'program.change_license' + | 'program.change_licensetype' + | 'program.change_linktype' + | 'program.change_media' + | 'program.change_mediasource' + | 'program.change_musicfocus' + | 'program.change_note' + | 'program.change_notelink' + | 'program.change_playlist' + | 'program.change_playlistentry' + | 'program.change_profile' + | 'program.change_profilelink' + | 'program.change_radiosettings' + | 'program.change_rrule' + | 'program.change_schedule' + | 'program.change_show' + | 'program.change_showlink' + | 'program.change_timeslot' + | 'program.change_topic' + | 'program.change_type' + | 'program.change_userprofile' + | 'program.create_cba' + | 'program.create_episode' + | 'program.create_media' + | 'program.create_note' + | 'program.create_playlist' + | 'program.delete_category' + | 'program.delete_cba' + | 'program.delete_contentlicense' + | 'program.delete_episode' + | 'program.delete_episodelink' + | 'program.delete_fundingcategory' + | 'program.delete_host' + | 'program.delete_hostlink' + | 'program.delete_image' + | 'program.delete_language' + | 'program.delete_license' + | 'program.delete_licensetype' + | 'program.delete_linktype' + | 'program.delete_media' + | 'program.delete_mediasource' + | 'program.delete_musicfocus' + | 'program.delete_note' + | 'program.delete_notelink' + | 'program.delete_playlist' + | 'program.delete_playlistentry' + | 'program.delete_profile' + | 'program.delete_profilelink' + | 'program.delete_radiosettings' + | 'program.delete_rrule' + | 'program.delete_schedule' + | 'program.delete_show' + | 'program.delete_showlink' + | 'program.delete_timeslot' + | 'program.delete_topic' + | 'program.delete_type' + | 'program.delete_userprofile' + | 'program.destroy_media' + | 'program.destroy_playlist' + | 'program.display__show__internal_note' + | 'program.edit__episode__cba_id' + | 'program.edit__episode__content' + | 'program.edit__episode__contributors' + | 'program.edit__episode__image' + | 'program.edit__episode__languages' + | 'program.edit__episode__links' + | 'program.edit__episode__media' + | 'program.edit__episode__media_description' + | 'program.edit__episode__memo' + | 'program.edit__episode__playlist' + | 'program.edit__episode__summary' + | 'program.edit__episode__tags' + | 'program.edit__episode__title' + | 'program.edit__episode__topics' + | 'program.edit__host__biography' + | 'program.edit__host__email' + | 'program.edit__host__image' + | 'program.edit__host__links' + | 'program.edit__host__name' + | 'program.edit__host__owners' + | 'program.edit__note__cba_id' + | 'program.edit__note__content' + | 'program.edit__note__contributors' + | 'program.edit__note__image' + | 'program.edit__note__language' + | 'program.edit__note__languages' + | 'program.edit__note__links' + | 'program.edit__note__playlist' + | 'program.edit__note__summary' + | 'program.edit__note__tags' + | 'program.edit__note__title' + | 'program.edit__note__topic' + | 'program.edit__note__topics' + | 'program.edit__profile__biography' + | 'program.edit__profile__email' + | 'program.edit__profile__image' + | 'program.edit__profile__links' + | 'program.edit__profile__name' + | 'program.edit__profile__owners' + | 'program.edit__schedule__default_media_id' + | 'program.edit__schedule__default_playlist_id' + | 'program.edit__show__categories' + | 'program.edit__show__cba_series_id' + | 'program.edit__show__default_media_id' + | 'program.edit__show__default_playlist' + | 'program.edit__show__default_playlist_id' + | 'program.edit__show__description' + | 'program.edit__show__email' + | 'program.edit__show__funding_categories' + | 'program.edit__show__hosts' + | 'program.edit__show__image' + | 'program.edit__show__internal_note' + | 'program.edit__show__is_active' + | 'program.edit__show__languages' + | 'program.edit__show__links' + | 'program.edit__show__logo' + | 'program.edit__show__music_focuses' + | 'program.edit__show__name' + | 'program.edit__show__owners' + | 'program.edit__show__predecessor' + | 'program.edit__show__short_description' + | 'program.edit__show__slug' + | 'program.edit__show__topics' + | 'program.edit__show__type' + | 'program.edit__timeslot__memo' + | 'program.edit__timeslot__playlist' + | 'program.edit__timeslot__playlist_id' + | 'program.edit__timeslot__repetition_of' + | 'program.update_cba' + | 'program.update_episode' + | 'program.update_host' + | 'program.update_media' + | 'program.update_note' + | 'program.update_playlist' + | 'program.update_profile' + | 'program.update_show' + | 'program.update_timeslot' + | 'program.view_category' + | 'program.view_cba' + | 'program.view_contentlicense' + | 'program.view_episode' + | 'program.view_episodelink' + | 'program.view_fundingcategory' + | 'program.view_host' + | 'program.view_hostlink' + | 'program.view_image' + | 'program.view_language' + | 'program.view_license' + | 'program.view_licensetype' + | 'program.view_linktype' + | 'program.view_media' + | 'program.view_mediasource' + | 'program.view_musicfocus' + | 'program.view_note' + | 'program.view_notelink' + | 'program.view_playlist' + | 'program.view_playlistentry' + | 'program.view_profile' + | 'program.view_profilelink' + | 'program.view_radiosettings' + | 'program.view_rrule' + | 'program.view_schedule' + | 'program.view_show' + | 'program.view_showlink' + | 'program.view_timeslot' + | 'program.view_topic' + | 'program.view_type' + | 'program.view_userprofile' } responses: never parameters: never diff --git a/src/types.ts b/src/types.ts index 0f11fa54..fe8c68e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,6 +35,8 @@ export type ImageFrameSpec = { type ReadonlyAttrs = 'id' | 'createdAt' | 'createdBy' | 'updatedAt' | 'updatedBy' +export type AnyPermission = steeringComponents['schemas']['AnyPermission'] + type _Episode = steeringComponents['schemas']['Episode'] type _EpisodeReadonlyAttrs = ReadonlyAttrs | 'timeslotIds' export type Episode = Required<_Episode> -- GitLab