mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat: enable token sign in with comma separated subs in myservers.config
This commit is contained in:
@@ -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",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -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": "",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -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": "",
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ export const initialState: SliceState = {
|
||||
refreshtoken: '',
|
||||
allowedOrigins: '',
|
||||
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
|
||||
ssoSubIds: ''
|
||||
},
|
||||
local: {},
|
||||
api: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user