feat: enable token sign in with comma separated subs in myservers.config

This commit is contained in:
Eli Bosley
2025-01-18 11:06:12 -05:00
parent 02c197f244
commit e9bd18a409
8 changed files with 191 additions and 181 deletions

View File

@@ -18,31 +18,31 @@ test('Returns allowed origins', async () => {
// Get allowed origins // Get allowed origins
expect(getAllowedOrigins()).toMatchInlineSnapshot(` expect(getAllowedOrigins()).toMatchInlineSnapshot(`
[ [
"/var/run/unraid-notifications.sock", "/var/run/unraid-notifications.sock",
"/var/run/unraid-php.sock", "/var/run/unraid-php.sock",
"/var/run/unraid-cli.sock", "/var/run/unraid-cli.sock",
"http://localhost:8080", "http://localhost:8080",
"https://localhost:4443", "https://localhost:4443",
"https://tower.local:4443", "https://tower.local:4443",
"https://192.168.1.150:4443", "https://192.168.1.150:4443",
"https://tower:4443", "https://tower:4443",
"https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443", "https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443",
"https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443", "https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443",
"https://10-252-0-1.hash.myunraid.net:4443", "https://10-252-0-1.hash.myunraid.net:4443",
"https://10-252-1-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-3-1.hash.myunraid.net:4443",
"https://10-253-4-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-253-5-1.hash.myunraid.net:4443",
"https://10-100-0-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-100-0-2.hash.myunraid.net:4443",
"https://10-123-1-2.hash.myunraid.net:4443", "https://10-123-1-2.hash.myunraid.net:4443",
"https://221-123-121-112.hash.myunraid.net:4443", "https://221-123-121-112.hash.myunraid.net:4443",
"https://google.com", "https://google.com",
"https://test.com", "https://test.com",
"https://connect.myunraid.net", "https://connect.myunraid.net",
"https://connect-staging.myunraid.net", "https://connect-staging.myunraid.net",
"https://dev-my.myunraid.net:4000", "https://dev-my.myunraid.net:4000",
] ]
`); `);
}); });

View File

@@ -29,6 +29,7 @@ test('it creates a FLASH config with NO OPTIONAL values', () => {
"localApiKey": "", "localApiKey": "",
"refreshtoken": "", "refreshtoken": "",
"regWizTime": "", "regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "", "upnpEnabled": "",
"username": "", "username": "",
"wanaccess": "", "wanaccess": "",
@@ -69,6 +70,7 @@ test('it creates a MEMORY config with NO OPTIONAL values', () => {
"localApiKey": "", "localApiKey": "",
"refreshtoken": "", "refreshtoken": "",
"regWizTime": "", "regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "", "upnpEnabled": "",
"username": "", "username": "",
"wanaccess": "", "wanaccess": "",
@@ -93,35 +95,36 @@ test('it creates a FLASH config with OPTIONAL values', () => {
basicConfig.connectionStatus.upnpStatus = 'Turned On'; basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'flash'); const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(` expect(config).toMatchInlineSnapshot(`
{ {
"api": { "api": {
"extraOrigins": "myextra.origins", "extraOrigins": "myextra.origins",
"version": "", "version": "",
}, },
"local": {}, "local": {},
"notifier": { "notifier": {
"apikey": "", "apikey": "",
}, },
"remote": { "remote": {
"accesstoken": "", "accesstoken": "",
"apikey": "", "apikey": "",
"avatar": "", "avatar": "",
"dynamicRemoteAccessType": "DISABLED", "dynamicRemoteAccessType": "DISABLED",
"email": "", "email": "",
"idtoken": "", "idtoken": "",
"localApiKey": "", "localApiKey": "",
"refreshtoken": "", "refreshtoken": "",
"regWizTime": "", "regWizTime": "",
"upnpEnabled": "yes", "ssoSubIds": "",
"username": "", "upnpEnabled": "yes",
"wanaccess": "", "username": "",
"wanport": "", "wanaccess": "",
}, "wanport": "",
"upc": { },
"apikey": "", "upc": {
}, "apikey": "",
} },
`); }
`);
}); });
test('it creates a MEMORY config with OPTIONAL values', () => { 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'; basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'memory'); const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(` expect(config).toMatchInlineSnapshot(`
{ {
"api": { "api": {
"extraOrigins": "myextra.origins", "extraOrigins": "myextra.origins",
"version": "", "version": "",
}, },
"connectionStatus": { "connectionStatus": {
"minigraph": "PRE_INIT", "minigraph": "PRE_INIT",
"upnpStatus": "Turned On", "upnpStatus": "Turned On",
}, },
"local": {}, "local": {},
"notifier": { "notifier": {
"apikey": "", "apikey": "",
}, },
"remote": { "remote": {
"accesstoken": "", "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", "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": "", "apikey": "",
"avatar": "", "avatar": "",
"dynamicRemoteAccessType": "DISABLED", "dynamicRemoteAccessType": "DISABLED",
"email": "", "email": "",
"idtoken": "", "idtoken": "",
"localApiKey": "", "localApiKey": "",
"refreshtoken": "", "refreshtoken": "",
"regWizTime": "", "regWizTime": "",
"upnpEnabled": "yes", "ssoSubIds": "",
"username": "", "upnpEnabled": "yes",
"wanaccess": "", "username": "",
"wanport": "", "wanaccess": "",
}, "wanport": "",
"upc": { },
"apikey": "", "upc": {
}, "apikey": "",
} },
`); }
`);
}); });

View File

@@ -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": "",
},
}
`;

View File

@@ -1,46 +1,11 @@
import { expect, test } from 'vitest'; import { expect, test } from 'vitest';
import { store } from '@app/store'; import { store } from '@app/store';
import { MyServersConfigMemory } from '@app/types/my-servers-config';
test('Before init returns default values for all fields', async () => { test('Before init returns default values for all fields', async () => {
const state = store.getState().config; const state = store.getState().config;
expect(state).toMatchInlineSnapshot(` expect(state).toMatchSnapshot();
{
"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": "",
},
}
`);
}, 10_000); }, 10_000);
test('After init returns values from cfg file for all fields', async () => { 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_________________________', localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________',
refreshtoken: '', refreshtoken: '',
regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0', regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0',
ssoSubIds: '',
upnpEnabled: 'no', upnpEnabled: 'no',
username: 'zspearmint', username: 'zspearmint',
wanaccess: 'yes', wanaccess: 'yes',
@@ -130,6 +96,7 @@ test('updateUserConfig merges in changes to current state', async () => {
localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________', localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________',
refreshtoken: '', refreshtoken: '',
regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0', regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0',
ssoSubIds: '',
upnpEnabled: 'no', upnpEnabled: 'no',
username: 'zspearmint', username: 'zspearmint',
wanaccess: 'yes', wanaccess: 'yes',
@@ -139,6 +106,6 @@ test('updateUserConfig merges in changes to current state', async () => {
upc: { upc: {
apikey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810', apikey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
}, },
}) } as MyServersConfigMemory)
); );
}); });

View File

@@ -51,6 +51,7 @@ export const initialState: SliceState = {
refreshtoken: '', refreshtoken: '',
allowedOrigins: '', allowedOrigins: '',
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED, dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
ssoSubIds: ''
}, },
local: {}, local: {},
api: { api: {

View File

@@ -26,16 +26,19 @@ const RemoteConfigSchema = z.object({
idtoken: z.string(), idtoken: z.string(),
refreshtoken: z.string(), refreshtoken: z.string(),
dynamicRemoteAccessType: z.nativeEnum(DynamicRemoteAccessType), dynamicRemoteAccessType: z.nativeEnum(DynamicRemoteAccessType),
ssoSubIds: z.string(),
}); });
const UpcConfigSchema = z.object({ const UpcConfigSchema = z.object({
apikey: z.string(), apikey: z.string(),
}); });
const LocalConfigSchema = z.object({});
// Base config schema // Base config schema
export const MyServersConfigSchema = z.object({ export const MyServersConfigSchema = z.object({
api: ApiConfigSchema, api: ApiConfigSchema,
local: z.object({}), // Empty object local: LocalConfigSchema,
notifier: NotifierConfigSchema, notifier: NotifierConfigSchema,
remote: RemoteConfigSchema, remote: RemoteConfigSchema,
upc: UpcConfigSchema, upc: UpcConfigSchema,

View File

@@ -1,38 +1,27 @@
import { execSync } from 'child_process'; import { execa } from 'execa';
import { join } from 'path';
import { Command, CommandRunner } from 'nest-commander'; import { Command, CommandRunner } from 'nest-commander';
import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts'; 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. * 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 { export class RestartCommand extends CommandRunner {
async run(_): Promise<void> { constructor(private readonly logger: LogService) {
console.log( super();
'Dirname is ', }
import.meta.dirname,
' command is ', async run(_): Promise<void> {
`${PM2_PATH} restart ${ECOSYSTEM_PATH} --update-env` const { stderr, stdout } = await execa(PM2_PATH, ['restart', ECOSYSTEM_PATH]);
); if (stderr) {
execSync( this.logger.error(stderr);
`${PM2_PATH} restart ${ECOSYSTEM_PATH} --update-env`, process.exit(1);
{ }
env: process.env, if (stdout) {
stdio: 'pipe', this.logger.info(stdout);
cwd: process.cwd(), }
} process.exit(0);
); }
} }
}

View File

@@ -7,12 +7,6 @@ import { store } from '@app/store';
import { loadConfigFile } from '@app/store/modules/config'; import { loadConfigFile } from '@app/store/modules/config';
import { LogService } from '@app/unraid-api/cli/log.service'; import { LogService } from '@app/unraid-api/cli/log.service';
const createJsonErrorString = (errorMessage: string) =>
JSON.stringify({
error: errorMessage,
valid: false,
});
@Command({ @Command({
name: 'validate-token', name: 'validate-token',
description: 'Returns JSON: { error: string | null, valid: boolean }', description: 'Returns JSON: { error: string | null, valid: boolean }',
@@ -26,6 +20,17 @@ export class ValidateTokenCommand extends CommandRunner {
this.JWKSOffline = createLocalJWKSet(JWKS_LOCAL_PAYLOAD); this.JWKSOffline = createLocalJWKSet(JWKS_LOCAL_PAYLOAD);
this.JWKSOnline = createRemoteJWKSet(new URL(JWKS_REMOTE_LINK)); 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<void> { async run(passedParams: string[]): Promise<void> {
if (passedParams.length !== 1) { if (passedParams.length !== 1) {
this.logger.error('Please pass token argument only'); this.logger.error('Please pass token argument only');
@@ -50,31 +55,32 @@ export class ValidateTokenCommand extends CommandRunner {
if (caughtError) { if (caughtError) {
if (caughtError instanceof Error) { if (caughtError instanceof Error) {
this.logger.error( this.createErrorAndExit(`Caught error validating jwt token: ${caughtError.message}`);
createJsonErrorString(`Caught error validating jwt token: ${caughtError.message}`)
);
} else { } else {
this.logger.error(createJsonErrorString('Caught error validating jwt token')); this.createErrorAndExit('Caught unknown error validating jwt token');
} }
} }
if (tokenPayload === null) { 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(); const configFile = await store.dispatch(loadConfigFile()).unwrap();
if (!configFile.remote?.accesstoken) { if (!configFile.remote?.ssoSubIds) {
this.logger.error(createJsonErrorString('No local user token set to compare to')); this.createErrorAndExit(
} 'No local user token set to compare to - please set any valid SSO IDs you would like to sign in with'
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')
); );
} }
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');
}
} }
} }