From e9bd18a4091ee3408cae74fd11dceb1cd6b81e5b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 18 Jan 2025 11:06:12 -0500 Subject: [PATCH] feat: enable token sign in with comma separated subs in myservers.config --- .../__test__/common/allowed-origins.test.ts | 54 ++++---- .../files/config-file-normalizer.test.ts | 130 +++++++++--------- .../modules/__snapshots__/config.test.ts.snap | 40 ++++++ api/src/__test__/store/modules/config.test.ts | 43 +----- api/src/store/modules/config.ts | 1 + api/src/types/my-servers-config.ts | 5 +- api/src/unraid-api/cli/restart.command.ts | 49 +++---- .../unraid-api/cli/validate-token.command.ts | 50 ++++--- 8 files changed, 191 insertions(+), 181 deletions(-) create mode 100644 api/src/__test__/store/modules/__snapshots__/config.test.ts.snap diff --git a/api/src/__test__/common/allowed-origins.test.ts b/api/src/__test__/common/allowed-origins.test.ts index 07de93fa9..04e45b88b 100644 --- a/api/src/__test__/common/allowed-origins.test.ts +++ b/api/src/__test__/common/allowed-origins.test.ts @@ -18,31 +18,31 @@ test('Returns allowed origins', async () => { // Get allowed origins expect(getAllowedOrigins()).toMatchInlineSnapshot(` - [ - "/var/run/unraid-notifications.sock", - "/var/run/unraid-php.sock", - "/var/run/unraid-cli.sock", - "http://localhost:8080", - "https://localhost:4443", - "https://tower.local:4443", - "https://192.168.1.150:4443", - "https://tower:4443", - "https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443", - "https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443", - "https://10-252-0-1.hash.myunraid.net:4443", - "https://10-252-1-1.hash.myunraid.net:4443", - "https://10-253-3-1.hash.myunraid.net:4443", - "https://10-253-4-1.hash.myunraid.net:4443", - "https://10-253-5-1.hash.myunraid.net:4443", - "https://10-100-0-1.hash.myunraid.net:4443", - "https://10-100-0-2.hash.myunraid.net:4443", - "https://10-123-1-2.hash.myunraid.net:4443", - "https://221-123-121-112.hash.myunraid.net:4443", - "https://google.com", - "https://test.com", - "https://connect.myunraid.net", - "https://connect-staging.myunraid.net", - "https://dev-my.myunraid.net:4000", - ] - `); + [ + "/var/run/unraid-notifications.sock", + "/var/run/unraid-php.sock", + "/var/run/unraid-cli.sock", + "http://localhost:8080", + "https://localhost:4443", + "https://tower.local:4443", + "https://192.168.1.150:4443", + "https://tower:4443", + "https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443", + "https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443", + "https://10-252-0-1.hash.myunraid.net:4443", + "https://10-252-1-1.hash.myunraid.net:4443", + "https://10-253-3-1.hash.myunraid.net:4443", + "https://10-253-4-1.hash.myunraid.net:4443", + "https://10-253-5-1.hash.myunraid.net:4443", + "https://10-100-0-1.hash.myunraid.net:4443", + "https://10-100-0-2.hash.myunraid.net:4443", + "https://10-123-1-2.hash.myunraid.net:4443", + "https://221-123-121-112.hash.myunraid.net:4443", + "https://google.com", + "https://test.com", + "https://connect.myunraid.net", + "https://connect-staging.myunraid.net", + "https://dev-my.myunraid.net:4000", + ] + `); }); diff --git a/api/src/__test__/core/utils/files/config-file-normalizer.test.ts b/api/src/__test__/core/utils/files/config-file-normalizer.test.ts index d4b911f0d..dfffef72c 100644 --- a/api/src/__test__/core/utils/files/config-file-normalizer.test.ts +++ b/api/src/__test__/core/utils/files/config-file-normalizer.test.ts @@ -29,6 +29,7 @@ test('it creates a FLASH config with NO OPTIONAL values', () => { "localApiKey": "", "refreshtoken": "", "regWizTime": "", + "ssoSubIds": "", "upnpEnabled": "", "username": "", "wanaccess": "", @@ -69,6 +70,7 @@ test('it creates a MEMORY config with NO OPTIONAL values', () => { "localApiKey": "", "refreshtoken": "", "regWizTime": "", + "ssoSubIds": "", "upnpEnabled": "", "username": "", "wanaccess": "", @@ -93,35 +95,36 @@ test('it creates a FLASH config with OPTIONAL values', () => { basicConfig.connectionStatus.upnpStatus = 'Turned On'; const config = getWriteableConfig(basicConfig, 'flash'); expect(config).toMatchInlineSnapshot(` - { - "api": { - "extraOrigins": "myextra.origins", - "version": "", - }, - "local": {}, - "notifier": { - "apikey": "", - }, - "remote": { - "accesstoken": "", - "apikey": "", - "avatar": "", - "dynamicRemoteAccessType": "DISABLED", - "email": "", - "idtoken": "", - "localApiKey": "", - "refreshtoken": "", - "regWizTime": "", - "upnpEnabled": "yes", - "username": "", - "wanaccess": "", - "wanport": "", - }, - "upc": { - "apikey": "", - }, - } - `); + { + "api": { + "extraOrigins": "myextra.origins", + "version": "", + }, + "local": {}, + "notifier": { + "apikey": "", + }, + "remote": { + "accesstoken": "", + "apikey": "", + "avatar": "", + "dynamicRemoteAccessType": "DISABLED", + "email": "", + "idtoken": "", + "localApiKey": "", + "refreshtoken": "", + "regWizTime": "", + "ssoSubIds": "", + "upnpEnabled": "yes", + "username": "", + "wanaccess": "", + "wanport": "", + }, + "upc": { + "apikey": "", + }, + } + `); }); test('it creates a MEMORY config with OPTIONAL values', () => { @@ -135,38 +138,39 @@ test('it creates a MEMORY config with OPTIONAL values', () => { basicConfig.connectionStatus.upnpStatus = 'Turned On'; const config = getWriteableConfig(basicConfig, 'memory'); expect(config).toMatchInlineSnapshot(` - { - "api": { - "extraOrigins": "myextra.origins", - "version": "", - }, - "connectionStatus": { - "minigraph": "PRE_INIT", - "upnpStatus": "Turned On", - }, - "local": {}, - "notifier": { - "apikey": "", - }, - "remote": { - "accesstoken": "", - "allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000", - "apikey": "", - "avatar": "", - "dynamicRemoteAccessType": "DISABLED", - "email": "", - "idtoken": "", - "localApiKey": "", - "refreshtoken": "", - "regWizTime": "", - "upnpEnabled": "yes", - "username": "", - "wanaccess": "", - "wanport": "", - }, - "upc": { - "apikey": "", - }, - } - `); + { + "api": { + "extraOrigins": "myextra.origins", + "version": "", + }, + "connectionStatus": { + "minigraph": "PRE_INIT", + "upnpStatus": "Turned On", + }, + "local": {}, + "notifier": { + "apikey": "", + }, + "remote": { + "accesstoken": "", + "allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000", + "apikey": "", + "avatar": "", + "dynamicRemoteAccessType": "DISABLED", + "email": "", + "idtoken": "", + "localApiKey": "", + "refreshtoken": "", + "regWizTime": "", + "ssoSubIds": "", + "upnpEnabled": "yes", + "username": "", + "wanaccess": "", + "wanport": "", + }, + "upc": { + "apikey": "", + }, + } + `); }); diff --git a/api/src/__test__/store/modules/__snapshots__/config.test.ts.snap b/api/src/__test__/store/modules/__snapshots__/config.test.ts.snap new file mode 100644 index 000000000..6875a6292 --- /dev/null +++ b/api/src/__test__/store/modules/__snapshots__/config.test.ts.snap @@ -0,0 +1,40 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Before init returns default values for all fields 1`] = ` +{ + "api": { + "extraOrigins": "", + "version": "", + }, + "connectionStatus": { + "minigraph": "PRE_INIT", + "upnpStatus": "", + }, + "local": {}, + "nodeEnv": "test", + "notifier": { + "apikey": "", + }, + "remote": { + "accesstoken": "", + "allowedOrigins": "", + "apikey": "", + "avatar": "", + "dynamicRemoteAccessType": "DISABLED", + "email": "", + "idtoken": "", + "localApiKey": "", + "refreshtoken": "", + "regWizTime": "", + "ssoSubIds": "", + "upnpEnabled": "", + "username": "", + "wanaccess": "", + "wanport": "", + }, + "status": "UNLOADED", + "upc": { + "apikey": "", + }, +} +`; diff --git a/api/src/__test__/store/modules/config.test.ts b/api/src/__test__/store/modules/config.test.ts index b1ac6c4d0..dd9824179 100644 --- a/api/src/__test__/store/modules/config.test.ts +++ b/api/src/__test__/store/modules/config.test.ts @@ -1,46 +1,11 @@ import { expect, test } from 'vitest'; import { store } from '@app/store'; +import { MyServersConfigMemory } from '@app/types/my-servers-config'; test('Before init returns default values for all fields', async () => { const state = store.getState().config; - expect(state).toMatchInlineSnapshot(` - { - "api": { - "extraOrigins": "", - "version": "", - }, - "connectionStatus": { - "minigraph": "PRE_INIT", - "upnpStatus": "", - }, - "local": {}, - "nodeEnv": "test", - "notifier": { - "apikey": "", - }, - "remote": { - "accesstoken": "", - "allowedOrigins": "", - "apikey": "", - "avatar": "", - "dynamicRemoteAccessType": "DISABLED", - "email": "", - "idtoken": "", - "localApiKey": "", - "refreshtoken": "", - "regWizTime": "", - "upnpEnabled": "", - "username": "", - "wanaccess": "", - "wanport": "", - }, - "status": "UNLOADED", - "upc": { - "apikey": "", - }, - } - `); + expect(state).toMatchSnapshot(); }, 10_000); test('After init returns values from cfg file for all fields', async () => { @@ -77,6 +42,7 @@ test('After init returns values from cfg file for all fields', async () => { localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________', refreshtoken: '', regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0', + ssoSubIds: '', upnpEnabled: 'no', username: 'zspearmint', wanaccess: 'yes', @@ -130,6 +96,7 @@ test('updateUserConfig merges in changes to current state', async () => { localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________', refreshtoken: '', regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0', + ssoSubIds: '', upnpEnabled: 'no', username: 'zspearmint', wanaccess: 'yes', @@ -139,6 +106,6 @@ test('updateUserConfig merges in changes to current state', async () => { upc: { apikey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810', }, - }) + } as MyServersConfigMemory) ); }); diff --git a/api/src/store/modules/config.ts b/api/src/store/modules/config.ts index fe61d9b33..57dea7d0c 100644 --- a/api/src/store/modules/config.ts +++ b/api/src/store/modules/config.ts @@ -51,6 +51,7 @@ export const initialState: SliceState = { refreshtoken: '', allowedOrigins: '', dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED, + ssoSubIds: '' }, local: {}, api: { diff --git a/api/src/types/my-servers-config.ts b/api/src/types/my-servers-config.ts index 1ac28c7a9..60dbf1d56 100644 --- a/api/src/types/my-servers-config.ts +++ b/api/src/types/my-servers-config.ts @@ -26,16 +26,19 @@ const RemoteConfigSchema = z.object({ idtoken: z.string(), refreshtoken: z.string(), dynamicRemoteAccessType: z.nativeEnum(DynamicRemoteAccessType), + ssoSubIds: z.string(), }); const UpcConfigSchema = z.object({ apikey: z.string(), }); +const LocalConfigSchema = z.object({}); + // Base config schema export const MyServersConfigSchema = z.object({ api: ApiConfigSchema, - local: z.object({}), // Empty object + local: LocalConfigSchema, notifier: NotifierConfigSchema, remote: RemoteConfigSchema, upc: UpcConfigSchema, diff --git a/api/src/unraid-api/cli/restart.command.ts b/api/src/unraid-api/cli/restart.command.ts index c1631157d..ca7e7b31c 100644 --- a/api/src/unraid-api/cli/restart.command.ts +++ b/api/src/unraid-api/cli/restart.command.ts @@ -1,38 +1,27 @@ -import { execSync } from 'child_process'; -import { join } from 'path'; - - - +import { execa } from 'execa'; import { Command, CommandRunner } from 'nest-commander'; - - import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts'; - - - - +import { LogService } from '@app/unraid-api/cli/log.service'; /** * Stop a running API process and then start it again. */ -@Command({ name: 'restart', description: 'Restart / Start the Unraid API'}) +@Command({ name: 'restart', description: 'Restart / Start the Unraid API' }) export class RestartCommand extends CommandRunner { - async run(_): Promise { - console.log( - 'Dirname is ', - import.meta.dirname, - ' command is ', - `${PM2_PATH} restart ${ECOSYSTEM_PATH} --update-env` - ); - execSync( - `${PM2_PATH} restart ${ECOSYSTEM_PATH} --update-env`, - { - env: process.env, - stdio: 'pipe', - cwd: process.cwd(), - } - ); - } - -} \ No newline at end of file + constructor(private readonly logger: LogService) { + super(); + } + + async run(_): Promise { + const { stderr, stdout } = await execa(PM2_PATH, ['restart', ECOSYSTEM_PATH]); + if (stderr) { + this.logger.error(stderr); + process.exit(1); + } + if (stdout) { + this.logger.info(stdout); + } + process.exit(0); + } +} diff --git a/api/src/unraid-api/cli/validate-token.command.ts b/api/src/unraid-api/cli/validate-token.command.ts index 76ecac4b5..68ad9d877 100644 --- a/api/src/unraid-api/cli/validate-token.command.ts +++ b/api/src/unraid-api/cli/validate-token.command.ts @@ -7,12 +7,6 @@ import { store } from '@app/store'; import { loadConfigFile } from '@app/store/modules/config'; import { LogService } from '@app/unraid-api/cli/log.service'; -const createJsonErrorString = (errorMessage: string) => - JSON.stringify({ - error: errorMessage, - valid: false, - }); - @Command({ name: 'validate-token', description: 'Returns JSON: { error: string | null, valid: boolean }', @@ -26,6 +20,17 @@ export class ValidateTokenCommand extends CommandRunner { this.JWKSOffline = createLocalJWKSet(JWKS_LOCAL_PAYLOAD); this.JWKSOnline = createRemoteJWKSet(new URL(JWKS_REMOTE_LINK)); } + + private createErrorAndExit = (errorMessage: string) => { + this.logger.error( + JSON.stringify({ + error: errorMessage, + valid: false, + }) + ); + process.exit(1); + }; + async run(passedParams: string[]): Promise { if (passedParams.length !== 1) { this.logger.error('Please pass token argument only'); @@ -50,31 +55,32 @@ export class ValidateTokenCommand extends CommandRunner { if (caughtError) { if (caughtError instanceof Error) { - this.logger.error( - createJsonErrorString(`Caught error validating jwt token: ${caughtError.message}`) - ); + this.createErrorAndExit(`Caught error validating jwt token: ${caughtError.message}`); } else { - this.logger.error(createJsonErrorString('Caught error validating jwt token')); + this.createErrorAndExit('Caught unknown error validating jwt token'); } } if (tokenPayload === null) { - this.logger.error(createJsonErrorString('No data in JWT to use for user validation')); + this.createErrorAndExit('No data in JWT to use for user validation'); } - const username = tokenPayload!.username ?? tokenPayload!['cognito:username']; + const username = tokenPayload?.sub; + + if (!username) { + return this.createErrorAndExit('No ID found in token'); + } const configFile = await store.dispatch(loadConfigFile()).unwrap(); - if (!configFile.remote?.accesstoken) { - this.logger.error(createJsonErrorString('No local user token set to compare to')); - } - - const existingUserPayload = decodeJwt(configFile.remote?.accesstoken); - if (username === existingUserPayload.username) { - this.logger.info(JSON.stringify({ error: null, valid: true })); - } else { - this.logger.error( - createJsonErrorString('Username on token does not match logged in user name') + if (!configFile.remote?.ssoSubIds) { + this.createErrorAndExit( + 'No local user token set to compare to - please set any valid SSO IDs you would like to sign in with' ); } + const possibleUserIds = configFile.remote.ssoSubIds.split(','); + if (possibleUserIds.includes(username)) { + this.logger.info(JSON.stringify({ error: null, valid: true, username })); + } else { + this.createErrorAndExit('Username on token does not match'); + } } }