diff --git a/api/dev/Unraid.net/myservers.cfg b/api/dev/Unraid.net/myservers.cfg index 0e907c896..5493663d3 100644 --- a/api/dev/Unraid.net/myservers.cfg +++ b/api/dev/Unraid.net/myservers.cfg @@ -9,6 +9,7 @@ wanaccess="yes" wanport="8443" upnpEnabled="no" apikey="_______________________BIG_API_KEY_HERE_________________________" +localApiKey="426b62b4d51e441fa97a93dfa1259920390a6eb61bd8675db0caa18dd0e414e9" email="test@example.com" username="zspearmint" avatar="https://via.placeholder.com/200" diff --git a/api/dev/keys/10f356da-1e9e-43b8-9028-a26a645539a6.json b/api/dev/keys/10f356da-1e9e-43b8-9028-a26a645539a6.json index 8eb8ab7d9..b9ce6ca0d 100644 --- a/api/dev/keys/10f356da-1e9e-43b8-9028-a26a645539a6.json +++ b/api/dev/keys/10f356da-1e9e-43b8-9028-a26a645539a6.json @@ -3,6 +3,6 @@ "key": "73717ca0-8c15-40b9-bcca-8d85656d1438", "name": "Test API Key", "description": "Testing API key creation", - "roles": ["guest", "upc"], + "roles": ["guest", "connect"], "createdAt": "2024-10-29T19:59:12.569Z" } diff --git a/api/dev/keys/d166bf8b-3615-444a-8932-c460b2132ba3.json b/api/dev/keys/d166bf8b-3615-444a-8932-c460b2132ba3.json new file mode 100644 index 000000000..0ce830d3a --- /dev/null +++ b/api/dev/keys/d166bf8b-3615-444a-8932-c460b2132ba3.json @@ -0,0 +1,10 @@ +{ + "createdAt": "2024-12-19T16:49:56.848Z", + "description": "API key for Connect user", + "id": "d166bf8b-3615-444a-8932-c460b2132ba3", + "key": "3a4e2332891e879d2ac8c3f25ef03a7b54f70b62cd6c5a08a86189cdd19ba203", + "name": "Connect", + "roles": [ + "admin" + ] +} \ No newline at end of file diff --git a/api/dev/states/myservers.cfg b/api/dev/states/myservers.cfg index a505dc733..48dd6c0dc 100644 --- a/api/dev/states/myservers.cfg +++ b/api/dev/states/myservers.cfg @@ -9,6 +9,7 @@ wanaccess="yes" wanport="8443" upnpEnabled="no" apikey="_______________________BIG_API_KEY_HERE_________________________" +localApiKey="3a4e2332891e879d2ac8c3f25ef03a7b54f70b62cd6c5a08a86189cdd19ba203" email="test@example.com" username="zspearmint" avatar="https://via.placeholder.com/200" diff --git a/api/src/graphql/generated/api/types.ts b/api/src/graphql/generated/api/types.ts index 30095d50b..8aac86bb1 100644 --- a/api/src/graphql/generated/api/types.ts +++ b/api/src/graphql/generated/api/types.ts @@ -1187,12 +1187,12 @@ export type RemoveRoleFromApiKeyInput = { /** Available resources for permissions */ export enum Resource { - API_KEY = 'api_key', + APIKEY = 'apikey', ARRAY = 'array', CLOUD = 'cloud', CONFIG = 'config', CONNECT = 'connect', - CRASH_REPORTING_ENABLED = 'crash_reporting_enabled', + CONNECT__REMOTE_ACCESS = 'connect__remote_access', CUSTOMIZATIONS = 'customizations', DASHBOARD = 'dashboard', DISK = 'disk', @@ -1220,10 +1220,8 @@ export enum Resource { /** Available roles for API keys and users */ export enum Role { ADMIN = 'admin', - GUEST = 'guest', - MY_SERVERS = 'my_servers', - NOTIFIER = 'notifier', - UPC = 'upc' + CONNECT = 'connect', + GUEST = 'guest' } export type Server = { diff --git a/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts b/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts index bbf4340a6..206d89f30 100644 --- a/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts +++ b/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts @@ -1,7 +1,6 @@ import { decodeJwt } from 'jose'; import type { ConnectSignInInput } from '@app/graphql/generated/api/types'; -import { Role } from '@app/graphql/generated/api/types'; import { getters, store } from '@app/store/index'; import { loginUser } from '@app/store/modules/config'; import { FileLoadStatus } from '@app/store/types'; @@ -30,11 +29,7 @@ export const connectSignIn = async (input: ConnectSignInInput): Promise if (localApiKeyFromConfig == '') { const apiKeyService = new ApiKeyService(); // Create local API key - const localApiKey = await apiKeyService.create( - `LOCAL_KEY_${userInfo.preferred_username.toUpperCase()}`, - `Local API key for Connect user ${userInfo.email}`, - [Role.ADMIN] - ); + const localApiKey = await apiKeyService.createLocalConnectApiKey(); if (!localApiKey?.key) { throw new Error('Failed to create local API key'); @@ -60,4 +55,4 @@ export const connectSignIn = async (input: ConnectSignInInput): Promise } else { return false; } -}; +}; \ No newline at end of file diff --git a/api/src/graphql/schema/types/auth/auth.graphql b/api/src/graphql/schema/types/auth/auth.graphql index 19f129e9c..93d7e0251 100644 --- a/api/src/graphql/schema/types/auth/auth.graphql +++ b/api/src/graphql/schema/types/auth/auth.graphql @@ -1,48 +1,3 @@ -""" -Available resources for permissions -""" -enum Resource { - api_key - cloud - config - crash_reporting_enabled - customizations - disk - display - flash - info - logs - online - os - owner - permission - registration - servers - share - vars - connect - notifications - array - dashboard - docker - network - services - vms - me - welcome -} - -""" -Available roles for API keys and users -""" -enum Role { - admin - upc - my_servers - notifier - guest -} - type ApiKey { id: ID! name: String! diff --git a/api/src/graphql/schema/types/auth/roles.graphql b/api/src/graphql/schema/types/auth/roles.graphql new file mode 100644 index 000000000..57cc67f8e --- /dev/null +++ b/api/src/graphql/schema/types/auth/roles.graphql @@ -0,0 +1,42 @@ +""" +Available resources for permissions +""" +enum Resource { + apikey + array + cloud + config + connect + connect__remote_access + customizations + dashboard + disk + display + docker + flash + info + logs + me + network + notifications + online + os + owner + permission + registration + servers + services + share + vars + vms + welcome +} + +""" +Available roles for API keys and users +""" +enum Role { + admin + connect + guest +} diff --git a/api/src/index.ts b/api/src/index.ts index 20ca9a6f8..477be1533 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -31,6 +31,7 @@ import { setupRegistrationKeyWatch } from '@app/store/watch/registration-watch'; import { StateManager } from '@app/store/watch/state-watch'; import { setupVarRunWatch } from '@app/store/watch/var-run-watch'; import { bootstrapNestServer } from '@app/unraid-api/main'; +import { createLocalApiKeyForConnectIfNecessary } from '@app/mothership/utils/create-local-connect-api-key'; import { setupNewMothershipSubscription } from './mothership/subscribe-to-mothership'; @@ -87,6 +88,8 @@ try { // Start listening to dynamix config file changes setupDynamixConfigWatch(); + await createLocalApiKeyForConnectIfNecessary(); + // Disabled until we need the access token to work // TokenRefresh.init(); diff --git a/api/src/mothership/utils/create-local-connect-api-key.ts b/api/src/mothership/utils/create-local-connect-api-key.ts new file mode 100644 index 000000000..3dfb6a7c5 --- /dev/null +++ b/api/src/mothership/utils/create-local-connect-api-key.ts @@ -0,0 +1,34 @@ +import { minigraphLogger } from '@app/core/log'; +import { getters, store } from '@app/store/index'; +import { updateUserConfig } from '@app/store/modules/config'; +import { FileLoadStatus } from '@app/store/types'; +import { ApiKeyService } from '@app/unraid-api/auth/api-key.service'; + +export const createLocalApiKeyForConnectIfNecessary = async () => { + if (getters.config().status !== FileLoadStatus.LOADED) { + minigraphLogger.error('Config file not loaded, cannot create local API key'); + return; + } + + const { remote } = getters.config(); + const service = new ApiKeyService(); + // If the remote API Key is set and the local key is either not set or not found on disk, create a key + if (remote.apikey && (!remote.localApiKey || !(await service.findById(remote.localApiKey)))) { + minigraphLogger.debug('Creating local API key for Connect'); + // Create local API key + const apiKeyService = new ApiKeyService(); + const localApiKey = await apiKeyService.createLocalConnectApiKey(); + + if (localApiKey?.key) { + store.dispatch( + updateUserConfig({ + remote: { + localApiKey: localApiKey.key, + }, + }) + ); + } else { + throw new Error('Failed to create local API key - no key returned'); + } + } +}; diff --git a/api/src/store/listeners/listener-middleware.ts b/api/src/store/listeners/listener-middleware.ts index a4bdc3ac1..229046928 100644 --- a/api/src/store/listeners/listener-middleware.ts +++ b/api/src/store/listeners/listener-middleware.ts @@ -1,3 +1,5 @@ +import 'reflect-metadata'; + import type { TypedAddListener, TypedStartListening } from '@reduxjs/toolkit'; import { addListener, createListenerMiddleware } from '@reduxjs/toolkit'; @@ -6,17 +8,12 @@ import { enableArrayEventListener } from '@app/store/listeners/array-event-liste import { enableConfigFileListener } from '@app/store/listeners/config-listener'; import { enableDynamicRemoteAccessListener } from '@app/store/listeners/dynamic-remote-access-listener'; import { enableMothershipJobsListener } from '@app/store/listeners/mothership-subscription-listener'; +import { enableNotificationPathListener } from '@app/store/listeners/notification-path-listener'; import { enableServerStateListener } from '@app/store/listeners/server-state-listener'; import { enableUpnpListener } from '@app/store/listeners/upnp-listener'; import { enableVersionListener } from '@app/store/listeners/version-listener'; import { enableWanAccessChangeListener } from '@app/store/listeners/wan-access-change-listener'; -import 'reflect-metadata'; - -import { enableNotificationPathListener } from '@app/store/listeners/notification-path-listener'; - -import { enableLocalApiKeyListener } from './local-api-key-listener'; - export const listenerMiddleware = createListenerMiddleware(); export type AppStartListening = TypedStartListening; @@ -29,7 +26,6 @@ export const addAppListener = addListener as TypedAddListener { // Begin listening for events - enableLocalApiKeyListener(); enableMothershipJobsListener(); enableConfigFileListener('flash')(); enableConfigFileListener('memory')(); diff --git a/api/src/store/listeners/local-api-key-listener.ts b/api/src/store/listeners/local-api-key-listener.ts deleted file mode 100644 index 9349f72d4..000000000 --- a/api/src/store/listeners/local-api-key-listener.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { logger } from '@app/core/log'; -import { Role } from '@app/graphql/generated/api/types'; -import { getters } from '@app/store/index'; -import { startAppListening } from '@app/store/listeners/listener-middleware'; -import { updateUserConfig } from '@app/store/modules/config'; -import { FileLoadStatus } from '@app/store/types'; -import { ApiKeyService } from '@app/unraid-api/auth/api-key.service'; - -export const enableLocalApiKeyListener = () => - startAppListening({ - predicate(_, currentState) { - return ( - currentState.config.status === FileLoadStatus.LOADED && - currentState.config.remote.apikey !== '' && - currentState.config.remote.localApiKey === '' - ); - }, - async effect(_, { dispatch }) { - try { - const { remote } = getters.config(); - const { apikey, username } = remote; - // Validate the API key with the key server - const apiKeyService = new ApiKeyService(); - // Create local API key - const localApiKey = await apiKeyService.create( - `LOCAL_KEY_${(username as string).toUpperCase()}`, - `Local API key for Connect user ${username}`, - [Role.ADMIN] - ); - - if (localApiKey?.key) { - dispatch( - updateUserConfig({ - remote: { - localApiKey: localApiKey.key, - }, - }) - ); - } else { - throw new Error('Failed to create local API key - no key returned'); - } - } catch (error) { - logger.error('Failed to create local API key', error); - } - }, - }); diff --git a/api/src/unraid-api/auth/api-key.service.ts b/api/src/unraid-api/auth/api-key.service.ts index 37b473f90..de4c4580c 100644 --- a/api/src/unraid-api/auth/api-key.service.ts +++ b/api/src/unraid-api/auth/api-key.service.ts @@ -3,20 +3,29 @@ import crypto from 'crypto'; import { readdir, readFile, writeFile } from 'fs/promises'; import { join } from 'path'; + + import { ensureDir } from 'fs-extra'; import { GraphQLError } from 'graphql'; import { v4 as uuidv4 } from 'uuid'; import { ZodError } from 'zod'; + + import { ApiKeySchema, ApiKeyWithSecretSchema } from '@app/graphql/generated/api/operations'; import { ApiKey, ApiKeyWithSecret, Role, UserAccount } from '@app/graphql/generated/api/types'; import { getters } from '@app/store'; + + + + @Injectable() export class ApiKeyService implements OnModuleInit { private readonly logger = new Logger(ApiKeyService.name); protected readonly basePath: string; protected readonly keyFile: (id: string) => string; + protected memoryApiKeys = new Map(); private static readonly validRoles: Set = new Set(Object.values(Role)); constructor() { @@ -34,6 +43,8 @@ export class ApiKeyService implements OnModuleInit { throw new GraphQLError('Failed to initialize API key storage'); } this.logger.verbose(`Using API key base path: ${this.basePath}`); + + // @todo setup file watch to reload keys } async onModuleInit() { @@ -41,13 +52,18 @@ export class ApiKeyService implements OnModuleInit { } private sanitizeName(name: string): string { - return name.replace(/[^a-zA-Z0-9-_]/g, '_').toUpperCase(); + if (/^[\p{L}\p{N} ]+$/u.test(name)) { + return name; + } else { + throw new GraphQLError('API key name must be alphanumeric and can only contain spaces'); + } } async create( name: string, description: string | undefined, - roles: Role[] + roles: Role[], + overwrite: boolean = false ): Promise { const trimmedName = name?.trim(); const sanitizedName = this.sanitizeName(trimmedName); @@ -63,19 +79,24 @@ export class ApiKeyService implements OnModuleInit { if (roles.some((role) => !ApiKeyService.validRoles.has(role))) { throw new GraphQLError('Invalid role specified'); } - - const apiKey: ApiKeyWithSecret = { + const apiKey: Partial = (await this.findByField('name', sanitizedName)) ?? { id: uuidv4(), key: this.generateApiKey(), name: sanitizedName, - description, - roles, - createdAt: new Date().toISOString(), }; - await this.saveApiKey(apiKey); + if (!overwrite && apiKey.createdAt) { + throw new GraphQLError('API key name already exists, use overwrite flag to update'); + } - return apiKey; + apiKey.description = description; + apiKey.roles = roles; + // Update createdAt date + apiKey.createdAt = new Date().toISOString(); + + await this.saveApiKey(apiKey as ApiKeyWithSecret); + + return apiKey as ApiKeyWithSecret; } async findAll(): Promise { @@ -162,12 +183,11 @@ export class ApiKeyService implements OnModuleInit { } } - async findByKey(key: string): Promise { - if (!key) return null; + async findByField(field: keyof ApiKeyWithSecret, value: string): Promise { + if (!value) return null; try { const files = await readdir(this.basePath); - const keyBuffer1 = Buffer.from(key); for (const file of files) { if (!file.endsWith('.json')) continue; @@ -187,14 +207,14 @@ export class ApiKeyService implements OnModuleInit { } const apiKey = ApiKeyWithSecretSchema().parse(parsedContent); - const keyBuffer2 = Buffer.from(apiKey.key); - if ( - keyBuffer1.length === keyBuffer2.length && - crypto.timingSafeEqual(keyBuffer1, keyBuffer2) - ) { + if (field === 'key') { + if (crypto.timingSafeEqual(Buffer.from(apiKey[field]), Buffer.from(value))) { + apiKey.roles = apiKey.roles.map((role) => role || Role.GUEST); + return apiKey; + } + } else if (apiKey[field] === value) { apiKey.roles = apiKey.roles.map((role) => role || Role.GUEST); - return apiKey; } } catch (error) { @@ -203,7 +223,6 @@ export class ApiKeyService implements OnModuleInit { } this.logger.error(`Error processing API key file ${file}: ${error}`); - throw new GraphQLError('Authentication system error'); } } @@ -218,6 +237,10 @@ export class ApiKeyService implements OnModuleInit { } } + async findByKey(key: string): Promise { + return this.findByField('key', key); + } + async findOneByKey(apiKey: string): Promise { try { const key = await this.findByKey(apiKey); @@ -247,11 +270,22 @@ export class ApiKeyService implements OnModuleInit { return crypto.randomBytes(32).toString('hex'); } + public async createLocalConnectApiKey(): Promise { + return await this.create('Connect', 'API key for Connect user', [Role.ADMIN], true); + } + public async saveApiKey(apiKey: ApiKeyWithSecret): Promise { try { const validatedApiKey = ApiKeyWithSecretSchema().parse(apiKey); - await writeFile(this.keyFile(validatedApiKey.id), JSON.stringify(validatedApiKey, null, 2)); + const sortedApiKey = Object.keys(validatedApiKey) + .sort() + .reduce((acc, key) => { + acc[key] = validatedApiKey[key]; + return acc; + }, {} as ApiKeyWithSecret); + + await writeFile(this.keyFile(validatedApiKey.id), JSON.stringify(sortedApiKey, null, 2)); } catch (error: unknown) { if (error instanceof ZodError) { this.logger.error('Invalid API key structure', error.errors); @@ -270,4 +304,4 @@ export class ApiKeyService implements OnModuleInit { keyFile: this.keyFile, }; } -} +} \ No newline at end of file diff --git a/api/src/unraid-api/auth/casbin/policy.ts b/api/src/unraid-api/auth/casbin/policy.ts index bc6831bd2..d2a0e6c77 100644 --- a/api/src/unraid-api/auth/casbin/policy.ts +++ b/api/src/unraid-api/auth/casbin/policy.ts @@ -6,63 +6,14 @@ export const BASE_POLICY = ` # Admin permissions p, ${Role.ADMIN}, *, *, * -# UPC permissions for API keys -p, ${Role.UPC}, ${Resource.API_KEY}, ${AuthAction.CREATE_ANY} -p, ${Role.UPC}, ${Resource.API_KEY}, ${AuthAction.UPDATE_ANY} - -# UPC permissions -p, ${Role.UPC}, ${Resource.CLOUD}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.CONFIG}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, crash-reporting-enabled, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.CUSTOMIZATIONS}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.DISK}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.DISPLAY}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.FLASH}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.INFO}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.LOGS}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.OS}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.OWNER}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.REGISTRATION}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.SERVERS}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.VARS}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.CONFIG}, ${AuthAction.UPDATE_ANY} -p, ${Role.UPC}, ${Resource.CONNECT}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.CONNECT}, ${AuthAction.UPDATE_ANY} -p, ${Role.UPC}, ${Resource.CONNECT}, ${AuthAction.UPDATE_OWN} -p, ${Role.UPC}, ${Resource.NOTIFICATIONS}, ${AuthAction.READ_ANY} -p, ${Role.UPC}, ${Resource.NOTIFICATIONS}, ${AuthAction.UPDATE_ANY} - -# My Servers permissions -p, ${Role.MY_SERVERS}, ${Resource.ARRAY}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.CONFIG}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.CONNECT}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, connect/dynamic-remote-access, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, connect/dynamic-remote-access, ${AuthAction.UPDATE_ANY} -p, ${Role.MY_SERVERS}, ${Resource.CUSTOMIZATIONS}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.DASHBOARD}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.DISPLAY}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, docker/container, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.DOCKER}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.INFO}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.LOGS}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.NETWORK}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.NOTIFICATIONS}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.SERVICES}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.VARS}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, ${Resource.VMS}, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, vms/domain, ${AuthAction.READ_ANY} -p, ${Role.MY_SERVERS}, unraid-version, ${AuthAction.READ_ANY} - -# Notifier permissions -p, ${Role.NOTIFIER}, ${Resource.NOTIFICATIONS}, ${AuthAction.CREATE_OWN} +# Connect Permissions +p, ${Role.CONNECT}, *, ${AuthAction.READ_ANY} +p, ${Role.CONNECT}, ${Resource.CONNECT__REMOTE_ACCESS}, ${AuthAction.UPDATE_ANY} # Guest permissions p, ${Role.GUEST}, ${Resource.ME}, ${AuthAction.READ_ANY} -p, ${Role.GUEST}, ${Resource.WELCOME}, ${AuthAction.READ_ANY} # Role inheritance g, ${Role.ADMIN}, ${Role.GUEST} -g, ${Role.UPC}, ${Role.GUEST} -g, ${Role.MY_SERVERS}, ${Role.GUEST} -g, ${Role.NOTIFIER}, ${Role.GUEST} +g, ${Role.CONNECT}, ${Role.GUEST} `; diff --git a/api/src/unraid-api/graph/connect/connect.resolver.ts b/api/src/unraid-api/graph/connect/connect.resolver.ts index db8b4d413..fc1e2b642 100644 --- a/api/src/unraid-api/graph/connect/connect.resolver.ts +++ b/api/src/unraid-api/graph/connect/connect.resolver.ts @@ -8,7 +8,7 @@ import type { DynamicRemoteAccessStatus, EnableDynamicRemoteAccessInput, } from '@app/graphql/generated/api/types'; -import { ConnectResolvers, DynamicRemoteAccessType } from '@app/graphql/generated/api/types'; +import { ConnectResolvers, DynamicRemoteAccessType, Resource } from '@app/graphql/generated/api/types'; import { RemoteAccessController } from '@app/remoteAccess/remote-access-controller'; import { store } from '@app/store/index'; import { setAllowedRemoteAccessUrl } from '@app/store/modules/dynamic-remote-access'; @@ -20,7 +20,7 @@ export class ConnectResolver implements ConnectResolvers { @Query('connect') @UsePermissions({ action: AuthActionVerb.READ, - resource: 'connect/dynamic-remote-access', + resource: Resource.CONNECT, possession: AuthPossession.ANY, }) public connect() { @@ -46,7 +46,7 @@ export class ConnectResolver implements ConnectResolvers { @Mutation() @UsePermissions({ action: AuthActionVerb.UPDATE, - resource: 'connect/dynamic-remote-access', + resource: Resource.CONNECT__REMOTE_ACCESS, possession: AuthPossession.ANY, }) public async enableDynamicRemoteAccess(