diff --git a/api/package-lock.json b/api/package-lock.json index bb5e75db9..d213d79ff 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -65,6 +65,7 @@ "mustache": "^4.2.0", "nanobus": "^4.5.0", "nest-access-control": "^3.1.0", + "nest-authz": "^2.11.0", "nestjs-pino": "^4.0.0", "node-cache": "^5.1.2", "node-window-polyfill": "^1.0.2", @@ -6604,6 +6605,11 @@ "fastq": "^1.17.1" } }, + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" + }, "node_modules/awilix": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/awilix/-/awilix-9.0.0.tgz", @@ -7150,6 +7156,63 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/casbin": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/casbin/-/casbin-5.30.0.tgz", + "integrity": "sha512-GDc8sImStd+ddBVBfLpe5fJPBWRjeEaz7fkiAGuw0+LTHF2TVvVsMALIMOx+ofzQhm+EHCH7mfiJsrS1Kgef2w==", + "dependencies": { + "await-lock": "^2.0.1", + "buffer": "^6.0.3", + "csv-parse": "^5.3.5", + "expression-eval": "^5.0.0", + "minimatch": "^7.4.2" + } + }, + "node_modules/casbin/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/casbin/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/casbin/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/caseless": { "version": "0.12.0", "license": "Apache-2.0" @@ -8240,6 +8303,11 @@ "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" }, + "node_modules/csv-parse": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.6.tgz", + "integrity": "sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==" + }, "node_modules/cz-conventional-changelog": { "version": "3.3.0", "dev": true, @@ -9823,6 +9891,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/expression-eval": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expression-eval/-/expression-eval-5.0.1.tgz", + "integrity": "sha512-7SL4miKp19lI834/F6y156xlNg+i9Q41tteuGNCq9C06S78f1bm3BXuvf0+QpQxv369Pv/P2R7Hb17hzxLpbDA==", + "deprecated": "The expression-eval npm package is no longer maintained. The package was originally published as part of a now-completed personal project, and I do not have incentives to continue maintenance.", + "dependencies": { + "jsep": "^0.3.0" + } + }, "node_modules/extend": { "version": "3.0.2", "license": "MIT" @@ -12242,6 +12319,14 @@ "version": "0.1.1", "license": "MIT" }, + "node_modules/jsep": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.3.5.tgz", + "integrity": "sha512-AoRLBDc6JNnKjNcmonituEABS5bcfqDhQAWWXNTFrqu6nVXBpBAGfcoTGZMFlIrh9FjmE1CQyX9CTNwZrXMMDA==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "license": "MIT", @@ -13538,6 +13623,23 @@ } } }, + "node_modules/nest-authz": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nest-authz/-/nest-authz-2.11.0.tgz", + "integrity": "sha512-S5bBUwvz0V2z8+GftccLDJXRRAs4nKsaztHm3JUmi7e4SQvCvyBCLdBJY8f3/kxzpM09Ht1d0Jrpq8SenR0EZg==", + "dependencies": { + "casbin": "^5.30.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "@nestjs/common": "^9.0.3 || ^10.0.0", + "@nestjs/core": "^9.0.3 || ^10.0.0", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.5.6" + } + }, "node_modules/nestjs-pino": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/nestjs-pino/-/nestjs-pino-4.0.0.tgz", @@ -23747,6 +23849,11 @@ "fastq": "^1.17.1" } }, + "await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" + }, "awilix": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/awilix/-/awilix-9.0.0.tgz", @@ -24105,6 +24212,45 @@ "upper-case-first": "^2.0.2" } }, + "casbin": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/casbin/-/casbin-5.30.0.tgz", + "integrity": "sha512-GDc8sImStd+ddBVBfLpe5fJPBWRjeEaz7fkiAGuw0+LTHF2TVvVsMALIMOx+ofzQhm+EHCH7mfiJsrS1Kgef2w==", + "requires": { + "await-lock": "^2.0.1", + "buffer": "^6.0.3", + "csv-parse": "^5.3.5", + "expression-eval": "^5.0.0", + "minimatch": "^7.4.2" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "caseless": { "version": "0.12.0" }, @@ -24850,6 +24996,11 @@ "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" }, + "csv-parse": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.6.tgz", + "integrity": "sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==" + }, "cz-conventional-changelog": { "version": "3.3.0", "dev": true, @@ -25922,6 +26073,14 @@ } } }, + "expression-eval": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expression-eval/-/expression-eval-5.0.1.tgz", + "integrity": "sha512-7SL4miKp19lI834/F6y156xlNg+i9Q41tteuGNCq9C06S78f1bm3BXuvf0+QpQxv369Pv/P2R7Hb17hzxLpbDA==", + "requires": { + "jsep": "^0.3.0" + } + }, "extend": { "version": "3.0.2" }, @@ -27560,6 +27719,11 @@ "jsbn": { "version": "0.1.1" }, + "jsep": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.3.5.tgz", + "integrity": "sha512-AoRLBDc6JNnKjNcmonituEABS5bcfqDhQAWWXNTFrqu6nVXBpBAGfcoTGZMFlIrh9FjmE1CQyX9CTNwZrXMMDA==" + }, "jsesc": { "version": "2.5.2" }, @@ -28431,6 +28595,14 @@ "tslib": "^2.6.2" } }, + "nest-authz": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nest-authz/-/nest-authz-2.11.0.tgz", + "integrity": "sha512-S5bBUwvz0V2z8+GftccLDJXRRAs4nKsaztHm3JUmi7e4SQvCvyBCLdBJY8f3/kxzpM09Ht1d0Jrpq8SenR0EZg==", + "requires": { + "casbin": "^5.30.0" + } + }, "nestjs-pino": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/nestjs-pino/-/nestjs-pino-4.0.0.tgz", diff --git a/api/package.json b/api/package.json index bef117867..51d4f8f06 100644 --- a/api/package.json +++ b/api/package.json @@ -116,6 +116,7 @@ "mustache": "^4.2.0", "nanobus": "^4.5.0", "nest-access-control": "^3.1.0", + "nest-authz": "^2.11.0", "nestjs-pino": "^4.0.0", "node-cache": "^5.1.2", "node-window-polyfill": "^1.0.2", diff --git a/api/src/core/utils/server-identifier.ts b/api/src/core/utils/server-identifier.ts index e8140fe17..2606b3c09 100644 --- a/api/src/core/utils/server-identifier.ts +++ b/api/src/core/utils/server-identifier.ts @@ -1,6 +1,10 @@ import { getters } from '@app/store/index'; import crypto from 'crypto'; -export const getServerIdentifier = (domain: string | null = null): string => { - const config = getters.config(); - return crypto.createHash('sha256').update(`${domain ? domain : ''}-${config.api.version}-${config.remote.apikey ?? config.upc.apikey}`).digest('hex'); +import { hostname } from 'os'; +export const getServerIdentifier = (): string => { + const flashGuid = getters.emhttp()?.var?.flashGuid ?? 'FLASH_GUID_NOT_FOUND'; + return crypto + .createHash('sha256') + .update(`${flashGuid}-${hostname()}`) + .digest('hex'); }; diff --git a/api/src/unraid-api/graph/connect/connect.resolver.ts b/api/src/unraid-api/graph/connect/connect.resolver.ts index 411da0a40..7f891a33a 100644 --- a/api/src/unraid-api/graph/connect/connect.resolver.ts +++ b/api/src/unraid-api/graph/connect/connect.resolver.ts @@ -13,7 +13,6 @@ import { import { setAllowedRemoteAccessUrl, } from '@app/store/modules/dynamic-remote-access'; -import { getServerIdentifier } from '@app/core/utils/server-identifier'; @Resolver('Connect') export class ConnectResolver implements ConnectResolvers { @@ -31,7 +30,7 @@ export class ConnectResolver implements ConnectResolvers { @ResolveField() public id() { - return getServerIdentifier('connect'); + return 'connect' } @ResolveField() diff --git a/api/src/unraid-api/graph/id-prefix-plugin.ts b/api/src/unraid-api/graph/id-prefix-plugin.ts index 72be25498..5dbe7f559 100644 --- a/api/src/unraid-api/graph/id-prefix-plugin.ts +++ b/api/src/unraid-api/graph/id-prefix-plugin.ts @@ -1,9 +1,12 @@ import { ApolloServerPlugin } from "@apollo/server"; import { getServerIdentifier } from "@app/core/utils/server-identifier"; -const serverId = getServerIdentifier(); - -const updateId = (obj: any) => { +/** + * Modify all ID fields in the GQL response object to include a prefix + * @param obj GQL response object, to be modified in place + */ +const updateId = (obj: Record) => { + const serverId = getServerIdentifier(); const stack = [obj]; let iterations = 0; // Prevent infinite loops @@ -17,7 +20,7 @@ const updateId = (obj: any) => { for (const value of Object.values(current)) { if (value && typeof value === 'object') { - stack.push(value); + stack.push(value as Record); } } } diff --git a/api/src/unraid-api/graph/network/network.resolver.ts b/api/src/unraid-api/graph/network/network.resolver.ts index 23cfd4293..4c06a891a 100644 --- a/api/src/unraid-api/graph/network/network.resolver.ts +++ b/api/src/unraid-api/graph/network/network.resolver.ts @@ -1,4 +1,3 @@ -import { getServerIdentifier } from '@app/core/utils/server-identifier'; import { AccessUrl, Network } from '@app/graphql/generated/api/types'; import { getServerIps } from '@app/graphql/resolvers/subscription/network'; import { Query, ResolveField, Resolver } from '@nestjs/graphql'; @@ -16,7 +15,7 @@ export class NetworkResolver { @Query('network') public async network(): Promise { return { - id: getServerIdentifier('network'), + id: 'network' }; } diff --git a/api/src/unraid-api/graph/resolvers/config/config.resolver.ts b/api/src/unraid-api/graph/resolvers/config/config.resolver.ts index a201d0b9c..9fcd24020 100644 --- a/api/src/unraid-api/graph/resolvers/config/config.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/config/config.resolver.ts @@ -1,5 +1,4 @@ import { getAllowedOrigins } from '@app/common/allowed-origins'; -import { getServerIdentifier } from '@app/core/utils/server-identifier'; import { type AllowedOriginInput, Config, ConfigErrorState } from '@app/graphql/generated/api/types'; import { getters, store } from '@app/store/index'; import { updateAllowedOrigins } from '@app/store/modules/config'; @@ -17,7 +16,7 @@ export class ConfigResolver { public async config(): Promise { const emhttp = getters.emhttp(); return { - id: getServerIdentifier('config'), + id: 'config', valid: emhttp.var.configValid, error: emhttp.var.configValid ? null diff --git a/api/src/unraid-api/graph/resolvers/display/display.resolver.ts b/api/src/unraid-api/graph/resolvers/display/display.resolver.ts index b0ea1c53e..6af15212d 100644 --- a/api/src/unraid-api/graph/resolvers/display/display.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/display/display.resolver.ts @@ -1,5 +1,4 @@ import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; -import { getServerIdentifier } from '@app/core/utils/server-identifier'; import { type Display } from '@app/graphql/generated/api/types'; import { getters } from '@app/store/index'; import { Query, Resolver, Subscription } from '@nestjs/graphql'; @@ -71,7 +70,7 @@ export class DisplayResolver { const dynamixBasePath = getters.paths()['dynamix-base']; const configFilePath = join(dynamixBasePath, 'case-model.cfg'); const result = { - id: getServerIdentifier('display'), + id: 'display' } // If the config file doesn't exist then it's a new OS install diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts index 306389418..86b8560f4 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts @@ -1,5 +1,4 @@ import { getDockerContainers } from '@app/core/modules/index'; -import { getServerIdentifier } from '@app/core/utils/server-identifier'; import { Query, ResolveField, Resolver } from '@nestjs/graphql'; import { UseRoles } from 'nest-access-control'; @@ -13,7 +12,7 @@ export class DockerResolver { @Query() public docker() { return { - id: getServerIdentifier('docker'), + id: 'docker', }; } diff --git a/api/src/unraid-api/graph/resolvers/info/info.resolver.ts b/api/src/unraid-api/graph/resolvers/info/info.resolver.ts index 5e8fac56a..1152387a4 100644 --- a/api/src/unraid-api/graph/resolvers/info/info.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/info/info.resolver.ts @@ -1,6 +1,5 @@ import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; import { getMachineId } from '@app/core/utils/misc/get-machine-id'; -import { getServerIdentifier } from '@app/core/utils/server-identifier'; import { generateApps, generateCpu, @@ -24,7 +23,7 @@ export class InfoResolver { }) public async info() { return { - id: getServerIdentifier('info') + id: 'info' }; } diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts index 28b94556e..5d900a38f 100644 --- a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts @@ -1,4 +1,3 @@ -import { getServerIdentifier } from '@app/core/utils/server-identifier'; import { getters } from '@app/store/index'; import { Query, Resolver } from '@nestjs/graphql'; import { UseRoles } from 'nest-access-control'; @@ -13,7 +12,7 @@ export class VarsResolver { }) public async vars() { return { - id: getServerIdentifier('vars'), + id: 'vars', ...getters.emhttp().var ?? {}, } } diff --git a/api/src/unraid-api/graph/services/services.resolver.ts b/api/src/unraid-api/graph/services/services.resolver.ts index 133531c1f..b78225447 100644 --- a/api/src/unraid-api/graph/services/services.resolver.ts +++ b/api/src/unraid-api/graph/services/services.resolver.ts @@ -1,5 +1,4 @@ import { bootTimestamp } from '@app/common/dashboard/boot-timestamp'; -import { getServerIdentifier } from '@app/core/utils/server-identifier'; import { API_VERSION } from '@app/environment'; import { DynamicRemoteAccessType, Service } from '@app/graphql/generated/api/types'; import { store } from '@app/store/index'; @@ -15,7 +14,7 @@ export class ServicesResolver { const enabledStatus = config.remote.dynamicRemoteAccessType; return { - id: getServerIdentifier('service/dynamic-remote-access'), + id: 'service/dynamic-remote-access', name: 'dynamic-remote-access', online: enabledStatus !== DynamicRemoteAccessType.DISABLED, version: dynamicRemoteAccess.runningType, @@ -27,7 +26,7 @@ export class ServicesResolver { private getApiService = (): Service => { return { - id: getServerIdentifier('service/unraid-api'), + id: 'service/unraid-api', name: 'unraid-api', online: true, uptime: {