From b4b5b1991b3d3058be5cc6a969ad2e1ce5e63940 Mon Sep 17 00:00:00 2001 From: Peter Beverloo Date: Thu, 18 Jul 2024 20:47:42 +0100 Subject: [PATCH] Migrate more privileges to be permissions --- app/admin/AdminSidebarClient.tsx | 2 +- app/admin/TopLevelLayout.tsx | 32 ++++++++---- app/admin/page.tsx | 7 ++- app/admin/system/ai/page.tsx | 2 +- app/admin/system/ai/prompt/[prompt]/page.tsx | 2 +- app/admin/system/debug/page.tsx | 2 +- app/admin/system/displays/page.tsx | 1 - app/admin/system/feedback/page.tsx | 3 +- app/admin/system/integrations/page.tsx | 3 +- app/admin/system/scheduler/[id]/page.tsx | 2 +- app/admin/system/scheduler/page.tsx | 2 +- app/admin/system/settings/page.tsx | 3 +- app/api/admin/scheduler/[[...id]]/route.ts | 2 +- app/api/admin/scheduler/scheduleTask.ts | 2 +- app/api/admin/serviceHealth.ts | 2 +- app/api/admin/updateSettings.ts | 2 +- app/api/admin/vertexAi.ts | 2 +- app/api/ai/generatePrompt.ts | 6 +-- app/api/ai/updateSettings.ts | 2 +- app/api/event/schedule/submitFeedback.ts | 3 +- app/feedback/page.tsx | 3 +- app/lib/auth/Access.ts | 53 ++++++++++++++++---- app/lib/auth/AuthenticationContext.test.ts | 4 +- app/lib/auth/Privileges.ts | 6 --- 24 files changed, 91 insertions(+), 57 deletions(-) diff --git a/app/admin/AdminSidebarClient.tsx b/app/admin/AdminSidebarClient.tsx index e5848eb7..bb78cf2e 100644 --- a/app/admin/AdminSidebarClient.tsx +++ b/app/admin/AdminSidebarClient.tsx @@ -126,7 +126,7 @@ export interface AdminSidebarMenuSubMenuItem { /** * Whether the menu should be open by default. Defaults to false. */ - defaultOpen?: boolean | Privilege; + defaultOpen?: boolean; /** * Child menu items that should be shown as part of this entry. diff --git a/app/admin/TopLevelLayout.tsx b/app/admin/TopLevelLayout.tsx index ef491695..b1eee4ff 100644 --- a/app/admin/TopLevelLayout.tsx +++ b/app/admin/TopLevelLayout.tsx @@ -24,6 +24,8 @@ import { type AdminSidebarMenuEntry, AdminSidebar } from './AdminSidebar'; import { Privilege } from '@lib/auth/Privileges'; import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; +import { kAnyEvent, kAnyTeam } from '@lib/auth/AccessControl'; + export default async function TopLevelLayout(props: React.PropsWithChildren) { const { access, user } = await requireAuthenticationContext(); @@ -37,7 +39,7 @@ export default async function TopLevelLayout(props: React.PropsWithChildren) { { icon: , label: 'Content', - privilege: Privilege.SystemAdministrator, + permission: 'system.content', url: '/admin/content', }, { @@ -49,7 +51,13 @@ export default async function TopLevelLayout(props: React.PropsWithChildren) { { icon: , label: 'Events', - privilege: Privilege.EventAdministrator, + permission: { + permission: 'event.visible', + options: { + event: kAnyEvent, + team: kAnyTeam, + }, + }, url: '/admin/events', }, { @@ -63,6 +71,8 @@ export default async function TopLevelLayout(props: React.PropsWithChildren) { { icon: , label: 'Communication', + // permission: system.feedback + // permission: system.internals privilege: [ Privilege.SystemOutboxAccess, Privilege.SystemSubscriptionManagement, @@ -72,7 +82,7 @@ export default async function TopLevelLayout(props: React.PropsWithChildren) { { icon: , label: 'Feedback', - privilege: Privilege.SystemAdministrator, + permission: 'system.feedback', url: '/admin/system/feedback', }, { @@ -90,7 +100,7 @@ export default async function TopLevelLayout(props: React.PropsWithChildren) { { icon: , label: 'Webhooks', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals', url: '/admin/system/webhooks', }, ], @@ -99,11 +109,13 @@ export default async function TopLevelLayout(props: React.PropsWithChildren) { icon: , label: 'System', permission: [ - 'system.ai', 'system.displays', + 'system.internals.ai', + 'system.internals.scheduler', + 'system.internals.settings', { permission: 'system.logs', operation: 'read' }, ], - defaultOpen: Privilege.SystemAdministrator, + defaultOpen: access.can('system.internals'), menu: [ { icon: , @@ -114,13 +126,13 @@ export default async function TopLevelLayout(props: React.PropsWithChildren) { { icon: , label: 'Generative AI', - permission: 'system.ai', + permission: 'system.internals.ai', url: '/admin/system/ai', }, { icon: , label: 'Integrations', - privilege: Privilege.Administrator, + permission: 'system.internals.settings', url: '/admin/system/integrations', }, { @@ -135,13 +147,13 @@ export default async function TopLevelLayout(props: React.PropsWithChildren) { { icon: , label: 'Scheduler', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.scheduler', url: '/admin/system/scheduler', }, { icon: , label: 'Settings', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.settings', url: '/admin/system/settings', } ] diff --git a/app/admin/page.tsx b/app/admin/page.tsx index b84c02a0..b9791f13 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -9,7 +9,6 @@ import type { SchedulerStatus } from './dashboard/SchedulerCard'; import type { User } from '@lib/auth/User'; import { default as TopLevelLayout } from './TopLevelLayout'; import { Dashboard } from './dashboard/Dashboard'; -import { Privilege, can } from '@lib/auth/Privileges'; import { RegistrationStatus } from '@lib/database/Types'; import { Temporal } from '@lib/Temporal'; import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; @@ -82,7 +81,7 @@ async function fetchBirthdays(user: User) { * give an overview of what's going on. Exact cards depend on the user's access level. */ export default async function AdminPage() { - const { events, user } = await requireAuthenticationContext({ check: 'admin' }); + const { access, events, user } = await requireAuthenticationContext({ check: 'admin' }); // TODO: Filter for participating events in `fetchBirthdays` const { currentBirthdays, upcomingBirthdays } = await fetchBirthdays(user); @@ -90,7 +89,7 @@ export default async function AdminPage() { const connectionPool = getConnectionPool(); let databaseStatus: DatabaseStatus | undefined; - if (can(user, Privilege.SystemAdministrator) && connectionPool) { + if (access.can('system.internals') && connectionPool) { databaseStatus = { connections: { active: connectionPool.activeConnections(), @@ -102,7 +101,7 @@ export default async function AdminPage() { } let schedulerStatus: SchedulerStatus | undefined; - if (can(user, Privilege.SystemAdministrator)) { + if (access.can('system.internals.scheduler')) { let timeSinceLastExecutionMs: number | undefined = undefined; if (globalScheduler.lastExecution !== undefined) { const diffNs = process.hrtime.bigint() - globalScheduler.lastExecution; diff --git a/app/admin/system/ai/page.tsx b/app/admin/system/ai/page.tsx index 80566dfe..aeea94b9 100644 --- a/app/admin/system/ai/page.tsx +++ b/app/admin/system/ai/page.tsx @@ -18,7 +18,7 @@ import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; export default async function AiPage() { await requireAuthenticationContext({ check: 'admin', - permission: 'system.ai', + permission: 'system.internals.ai', }); // Settings to load for this page, shared across the different displays. diff --git a/app/admin/system/ai/prompt/[prompt]/page.tsx b/app/admin/system/ai/prompt/[prompt]/page.tsx index a333f61d..3f6cfb94 100644 --- a/app/admin/system/ai/prompt/[prompt]/page.tsx +++ b/app/admin/system/ai/prompt/[prompt]/page.tsx @@ -19,7 +19,7 @@ import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; export default async function AiPromptExplorer(props: NextPageParams<'prompt'>) { await requireAuthenticationContext({ check: 'admin', - permission: 'system.ai', + permission: 'system.internals.ai', }); const personality = await readSetting('gen-ai-personality') ?? ''; diff --git a/app/admin/system/debug/page.tsx b/app/admin/system/debug/page.tsx index e617aaf6..0ba5db6b 100644 --- a/app/admin/system/debug/page.tsx +++ b/app/admin/system/debug/page.tsx @@ -67,7 +67,7 @@ async function debugAction(formData: unknown) { export default async function DebugPage() { await requireAuthenticationContext({ check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals', }); const debugValues: Record = {}; diff --git a/app/admin/system/displays/page.tsx b/app/admin/system/displays/page.tsx index 8fc94ad4..7f2231f2 100644 --- a/app/admin/system/displays/page.tsx +++ b/app/admin/system/displays/page.tsx @@ -6,7 +6,6 @@ import type { Metadata } from 'next'; import type { DisplayTableEventOption, DisplayTableLocationOption } from './DisplaysTable'; import { DisplaysTable } from './DisplaysTable'; import { HelpRequestTable } from './HelpRequestTable'; -import { Privilege } from '@lib/auth/Privileges'; import { Section } from '@app/admin/components/Section'; import { SectionIntroduction } from '@app/admin/components/SectionIntroduction'; import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; diff --git a/app/admin/system/feedback/page.tsx b/app/admin/system/feedback/page.tsx index c4e3d988..fe209c05 100644 --- a/app/admin/system/feedback/page.tsx +++ b/app/admin/system/feedback/page.tsx @@ -3,7 +3,6 @@ import type { Metadata } from 'next'; -import { Privilege } from '@lib/auth/Privileges'; import { Section } from '../../components/Section'; import { SectionIntroduction } from '../../components/SectionIntroduction'; import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; @@ -16,7 +15,7 @@ import { FeedbackDataTable } from './FeedbackDataTable'; export default async function FeedbackPage() { await requireAuthenticationContext({ check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.feedback', }); // TODO: Consider allowing a response to be composed automagically. diff --git a/app/admin/system/integrations/page.tsx b/app/admin/system/integrations/page.tsx index fd0f003f..35b100b8 100644 --- a/app/admin/system/integrations/page.tsx +++ b/app/admin/system/integrations/page.tsx @@ -7,7 +7,6 @@ import { AnimeCon, type AnimeConSettings } from './AnimeCon'; import { Email, type EmailSettings } from './Email'; import { Google, type GoogleSettings } from './Google'; import { StatusHeader } from './StatusHeader'; -import { Privilege } from '@lib/auth/Privileges'; import { Twilio } from './Twilio'; import { VertexAI, type VertexAISettings } from './VertexAI'; import { VertexSupportedModels } from '@lib/integrations/vertexai/VertexSupportedModels'; @@ -22,7 +21,7 @@ import type { TwilioSettings } from '@lib/integrations/twilio/TwilioClient'; export default async function IntegrationsPage() { await requireAuthenticationContext({ check: 'admin', - privilege: Privilege.Administrator, + permission: 'system.internals.settings', }); const settings = await readSettings([ diff --git a/app/admin/system/scheduler/[id]/page.tsx b/app/admin/system/scheduler/[id]/page.tsx index 3cfd981b..54237b3c 100644 --- a/app/admin/system/scheduler/[id]/page.tsx +++ b/app/admin/system/scheduler/[id]/page.tsx @@ -32,7 +32,7 @@ export default async function TaskPage(props: NextPageParams<'id'>) { await requireAuthenticationContext({ check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.scheduler', }); const task = await db.selectFrom(tTasks) diff --git a/app/admin/system/scheduler/page.tsx b/app/admin/system/scheduler/page.tsx index 6b5c9953..74ca3189 100644 --- a/app/admin/system/scheduler/page.tsx +++ b/app/admin/system/scheduler/page.tsx @@ -15,7 +15,7 @@ import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; export default async function SchedulerPage() { await requireAuthenticationContext({ check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.scheduler', }); return ( diff --git a/app/admin/system/settings/page.tsx b/app/admin/system/settings/page.tsx index 5aa7d295..7326ab66 100644 --- a/app/admin/system/settings/page.tsx +++ b/app/admin/system/settings/page.tsx @@ -5,7 +5,6 @@ import type { Metadata } from 'next'; import Alert from '@mui/material/Alert'; -import { Privilege } from '@lib/auth/Privileges'; import { Section } from '@app/admin/components/Section'; import { SettingSection, type ConfigurableSetting } from './SettingSection'; import { readSettings, type Setting } from '@lib/Settings'; @@ -19,7 +18,7 @@ import { SettingUtilitiesSection } from './SettingUtilitiesSection'; export default async function IntegrationsPage() { await requireAuthenticationContext({ check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.settings', }); // The configuration for settings contains the individual settings, the data types, and the diff --git a/app/api/admin/scheduler/[[...id]]/route.ts b/app/api/admin/scheduler/[[...id]]/route.ts index c8444387..79a301fd 100644 --- a/app/api/admin/scheduler/[[...id]]/route.ts +++ b/app/api/admin/scheduler/[[...id]]/route.ts @@ -82,7 +82,7 @@ export const { GET } = createDataTableApi(kSchedulerRowModel, kSchedulerContext, async accessCheck(context, action, props) { executeAccessCheck(props.authenticationContext, { check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.scheduler', }); }, diff --git a/app/api/admin/scheduler/scheduleTask.ts b/app/api/admin/scheduler/scheduleTask.ts index 7c8cfa0d..b3f27406 100644 --- a/app/api/admin/scheduler/scheduleTask.ts +++ b/app/api/admin/scheduler/scheduleTask.ts @@ -67,7 +67,7 @@ type Response = ApiResponse; export async function scheduleTask(request: Request, props: ActionProps): Promise { executeAccessCheck(props.authenticationContext, { check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.scheduler', }); if ('taskId' in request) { diff --git a/app/api/admin/serviceHealth.ts b/app/api/admin/serviceHealth.ts index 0878a834..f94ea2d8 100644 --- a/app/api/admin/serviceHealth.ts +++ b/app/api/admin/serviceHealth.ts @@ -174,7 +174,7 @@ async function runVertexAIHealthCheck(): Promise { export async function serviceHealth(request: Request, props: ActionProps): Promise { executeAccessCheck(props.authenticationContext, { check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals', }); switch (request.service) { diff --git a/app/api/admin/updateSettings.ts b/app/api/admin/updateSettings.ts index c1dc3c02..ec2ffcb6 100644 --- a/app/api/admin/updateSettings.ts +++ b/app/api/admin/updateSettings.ts @@ -50,7 +50,7 @@ type Response = ApiResponse; export async function updateSettings(request: Request, props: ActionProps): Promise { executeAccessCheck(props.authenticationContext, { check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.settings', }); const settings: { [k: string]: boolean | number | string } = { /* will be composed */ }; diff --git a/app/api/admin/vertexAi.ts b/app/api/admin/vertexAi.ts index bef13030..c57751e3 100644 --- a/app/api/admin/vertexAi.ts +++ b/app/api/admin/vertexAi.ts @@ -58,7 +58,7 @@ type Response = ApiResponse; export async function vertexAi(request: Request, props: ActionProps): Promise { executeAccessCheck(props.authenticationContext, { check: 'admin', - privilege: Privilege.SystemAdministrator, + permission: 'system.internals.settings', }); const { settings } = request; diff --git a/app/api/ai/generatePrompt.ts b/app/api/ai/generatePrompt.ts index efbcffde..46bc3408 100644 --- a/app/api/ai/generatePrompt.ts +++ b/app/api/ai/generatePrompt.ts @@ -160,7 +160,7 @@ export async function generatePrompt(request: Request, props: ActionProps): Prom case 'approve-volunteer': executeAccessCheck(props.authenticationContext, { check: 'admin', - permission: or('system.ai', { + permission: or('system.internals.ai', { permission: 'event.applications', operation: 'update', options: { @@ -190,7 +190,7 @@ export async function generatePrompt(request: Request, props: ActionProps): Prom case 'reject-volunteer': executeAccessCheck(props.authenticationContext, { check: 'admin', - permission: or('system.ai', { + permission: or('system.internals.ai', { permission: 'event.applications', operation: 'update', options: { @@ -209,7 +209,7 @@ export async function generatePrompt(request: Request, props: ActionProps): Prom // Install personality and prompt overrides when provided. This feature is only accessible // through the Generative AI Explorer pages, and relies on a special permission. - if (request.overrides && props.access.can('system.ai')) + if (request.overrides && props.access.can('system.internals.ai')) generator.setOverrides(request.overrides.personality, request.overrides.prompt); const { context, prompt, subject } = await generator.build(request.language); diff --git a/app/api/ai/updateSettings.ts b/app/api/ai/updateSettings.ts index ccc8d892..346879d6 100644 --- a/app/api/ai/updateSettings.ts +++ b/app/api/ai/updateSettings.ts @@ -50,7 +50,7 @@ type Response = ApiResponse; export async function updateSettings(request: Request, props: ActionProps): Promise { executeAccessCheck(props.authenticationContext, { check: 'admin', - permission: 'system.ai', + permission: 'system.internals.ai', }); if (request.personality) { diff --git a/app/api/event/schedule/submitFeedback.ts b/app/api/event/schedule/submitFeedback.ts index 89d58cc1..ab2a3f59 100644 --- a/app/api/event/schedule/submitFeedback.ts +++ b/app/api/event/schedule/submitFeedback.ts @@ -8,7 +8,6 @@ import type { ActionProps } from '../../Action'; import type { ApiDefinition, ApiRequest, ApiResponse } from '../../Types'; import { Log, LogType } from '@lib/Log'; import { LogSeverity } from '@lib/database/Types'; -import { Privilege, can } from '@lib/auth/Privileges'; import db, { tFeedback } from '@lib/database'; /** @@ -66,7 +65,7 @@ export async function submitFeedback(request: Request, props: ActionProps): Prom let userId: number | undefined = props.user.userId; let feedbackName: string | undefined; - if (can(props.user, Privilege.Feedback) && !!request.overrides) { + if (props.access.can('system.feedback') && !!request.overrides) { if (!!request.overrides.userId || !!request.overrides.name) { userId = request.overrides.userId ?? undefined; feedbackName = request.overrides.name ?? undefined; diff --git a/app/feedback/page.tsx b/app/feedback/page.tsx index 1f270d57..9f479e41 100644 --- a/app/feedback/page.tsx +++ b/app/feedback/page.tsx @@ -5,7 +5,6 @@ import { notFound } from 'next/navigation'; import { ExportLayout } from '@app/exports/[slug]/ExportLayout'; import { FeedbackForm } from './FeedbackForm'; -import { Privilege } from '@lib/auth/Privileges'; import { Temporal } from '@lib/Temporal'; import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; @@ -18,7 +17,7 @@ import { getStaticContent } from '@lib/Content'; * soliciting feedback from volunteers. The page will automatically determine the right event. */ export default async function FeedbackPage() { - await requireAuthenticationContext({ privilege: Privilege.Feedback }); + await requireAuthenticationContext({ permission: 'system.feedback' }); const currentTime = Temporal.Now.zonedDateTimeISO('utc'); const eventSelectionTime = currentTime.subtract({ days: 30 }); diff --git a/app/lib/auth/Access.ts b/app/lib/auth/Access.ts index f775aad2..2c269d26 100644 --- a/app/lib/auth/Access.ts +++ b/app/lib/auth/Access.ts @@ -82,15 +82,6 @@ export const kPermissions = { // System-associated permissions // --------------------------------------------------------------------------------------------- - 'system.ai': { - name: 'Generative AI access', - description: - 'This permission controls whether the volunteer has access to Generative AI-related ' + - 'tooling, such as configuration and debugging pages. This does not include generated ' + - 'e-mail messages, which are granted based on feature availability.', - type: 'boolean', - }, - 'system.content': { name: 'Global content access', description: @@ -108,6 +99,50 @@ export const kPermissions = { type: 'boolean', }, + 'system.feedback': { + name: 'Feedback access', + description: + 'Volunteers have the ability to submit feedback through the portals, as well as ' + + 'through the feedback sub-app. This permission controls whether they have access to ' + + 'read all the feedback, possibly attributed.', + type: 'boolean', + }, + + 'system.internals': { + name: 'Internal system capabilities', + description: + 'This permission contains a set of individual permissions for features that will ' + + 'generally not be useful to regular volunteers, as they are part of internal system ' + + 'configuration or debugging capabilities.', + type: 'boolean', + }, + + 'system.internals.ai': { + name: 'Generative AI-related tooling', + description: + 'This permission controls whether the volunteer has access to Generative AI-related ' + + 'tooling, such as configuration and debugging pages. This does not include generated ' + + 'e-mail messages, which are granted based on feature availability.', + type: 'boolean', + }, + + 'system.internals.scheduler': { + name: 'System Scheduler status', + description: + 'The system scheduler is responsible for background operations such as sending ' + + 'messages and fetching program updates. This permission controls access to the ' + + 'status and overview pages of the scheduler.', + type: 'boolean', + }, + + 'system.internals.settings': { + name: 'System Settings', + description: + 'This permission grants access to the Volunteer Manager settings that allow detailed ' + + 'behaviour of the system to be adjusted without needing code changes.', + type: 'boolean', + }, + 'system.logs': { name: 'Volunteer Manager logs', description: diff --git a/app/lib/auth/AuthenticationContext.test.ts b/app/lib/auth/AuthenticationContext.test.ts index 215d85e4..7d8ead73 100644 --- a/app/lib/auth/AuthenticationContext.test.ts +++ b/app/lib/auth/AuthenticationContext.test.ts @@ -205,7 +205,7 @@ describe('AuthenticationContext', () => { executeAccessCheck(authenticationContext, { privilege: 0n as any as Privilege }); executeAccessCheck(authenticationContext, { privilege: Privilege.EventHotelManagement }); executeAccessCheck(authenticationContext, { - privilege: or(Privilege.EventHotelManagement, Privilege.Statistics), + privilege: or(Privilege.EventHotelManagement, Privilege.Refunds), }); // Fail: @@ -218,7 +218,7 @@ describe('AuthenticationContext', () => { try { executeAccessCheck(authenticationContext, { - privilege: and(Privilege.EventHotelManagement, Privilege.Statistics), + privilege: and(Privilege.EventHotelManagement, Privilege.Refunds), }); fail('executeAccessCheck was expected to throw'); } catch (error: any) { diff --git a/app/lib/auth/Privileges.ts b/app/lib/auth/Privileges.ts index c19577bc..2961a5f0 100644 --- a/app/lib/auth/Privileges.ts +++ b/app/lib/auth/Privileges.ts @@ -11,9 +11,7 @@ import type { User } from './User'; */ export enum Privilege { Administrator = 1 << 0, - Feedback = 1 << 1, // TODO: Assign to its own privilege Refunds = 1 << 23, - Statistics = 1 << 1, // Privileges regarding access in the administrative area. EventAdministrator = 1 << 7, @@ -64,9 +62,7 @@ const PrivilegeExpansion: { [key in Privilege]?: Privilege[] } = { Privilege.EventAdministrator, Privilege.SystemAdministrator, Privilege.VolunteerAdministrator, - Privilege.Feedback, Privilege.Refunds, - Privilege.Statistics, ], [Privilege.EventAdministrator]: [ @@ -117,7 +113,6 @@ export function expand(privileges: Privileges): Privileges { */ export const PrivilegeGroups: { [key in Privilege]: string } = { [Privilege.Administrator]: 'Special access', - [Privilege.Feedback]: 'Special access', [Privilege.Refunds]: 'Special access', //[Privilege.Statistics]: 'Special access', @@ -146,7 +141,6 @@ export const PrivilegeGroups: { [key in Privilege]: string } = { */ export const PrivilegeNames: { [key in Privilege]: string } = { [Privilege.Administrator]: 'Administrator', - [Privilege.Feedback]: 'Feedback tool', [Privilege.Refunds]: 'Refund requests', //[Privilege.Statistics]: 'Statistics',