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
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",
]
`);
});

View File

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

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 { 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)
);
});

View File

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

View File

@@ -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,

View File

@@ -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<void> {
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(),
}
);
}
}
constructor(private readonly logger: LogService) {
super();
}
async run(_): Promise<void> {
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);
}
}

View File

@@ -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<void> {
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');
}
}
}