diff --git a/api/dev/sessions/sess_mock-user-session b/api/dev/sessions/sess_mockusersession similarity index 100% rename from api/dev/sessions/sess_mock-user-session rename to api/dev/sessions/sess_mockusersession diff --git a/api/src/graphql/generated/api/operations.ts b/api/src/graphql/generated/api/operations.ts index 0021fef98d..7516212375 100755 --- a/api/src/graphql/generated/api/operations.ts +++ b/api/src/graphql/generated/api/operations.ts @@ -324,6 +324,7 @@ export function CreateApiKeyInputSchema(): z.ZodObject AddPermissionInputSchema())).nullish(), roles: z.array(RoleSchema).nullish() }) diff --git a/api/src/graphql/generated/api/types.ts b/api/src/graphql/generated/api/types.ts index 4090403341..faaf57df1a 100644 --- a/api/src/graphql/generated/api/types.ts +++ b/api/src/graphql/generated/api/types.ts @@ -348,6 +348,8 @@ export enum ContainerState { export type CreateApiKeyInput = { description?: InputMaybe; name: Scalars['String']['input']; + /** This will replace the existing key if one already exists with the same name, otherwise returns the existing key */ + overwrite?: InputMaybe; permissions?: InputMaybe>; roles?: InputMaybe>; }; diff --git a/api/src/graphql/schema/types/auth/auth.graphql b/api/src/graphql/schema/types/api-key/api-key.graphql similarity index 89% rename from api/src/graphql/schema/types/auth/auth.graphql rename to api/src/graphql/schema/types/api-key/api-key.graphql index a498c48c8a..b35f8c6b16 100644 --- a/api/src/graphql/schema/types/auth/auth.graphql +++ b/api/src/graphql/schema/types/api-key/api-key.graphql @@ -27,6 +27,8 @@ input CreateApiKeyInput { description: String roles: [Role!] permissions: [AddPermissionInput!] + """ This will replace the existing key if one already exists with the same name, otherwise returns the existing key """ + overwrite: Boolean } input AddPermissionInput { diff --git a/api/src/graphql/schema/types/auth/roles.graphql b/api/src/graphql/schema/types/api-key/roles.graphql similarity index 100% rename from api/src/graphql/schema/types/auth/roles.graphql rename to api/src/graphql/schema/types/api-key/roles.graphql diff --git a/api/src/unraid-api/auth/api-key.service.ts b/api/src/unraid-api/auth/api-key.service.ts index 9c54084f0e..4607f09624 100644 --- a/api/src/unraid-api/auth/api-key.service.ts +++ b/api/src/unraid-api/auth/api-key.service.ts @@ -127,7 +127,7 @@ export class ApiKeyService implements OnModuleInit { const existingKey = this.findByField('name', sanitizedName); if (!overwrite && existingKey) { - throw new GraphQLError('API key name already exists, use overwrite flag to update'); + return existingKey; } const apiKey: Partial = { id: uuidv4(), diff --git a/api/src/unraid-api/graph/resolvers/auth/auth.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts similarity index 95% rename from api/src/unraid-api/graph/resolvers/auth/auth.resolver.spec.ts rename to api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts index 23052c3663..df418a48a4 100644 --- a/api/src/unraid-api/graph/resolvers/auth/auth.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts @@ -8,10 +8,10 @@ import { ApiKeyService } from '@app/unraid-api/auth/api-key.service'; import { AuthService } from '@app/unraid-api/auth/auth.service'; import { CookieService } from '@app/unraid-api/auth/cookie.service'; -import { AuthResolver } from './auth.resolver'; +import { ApiKeyResolver } from './api-key.resolver'; -describe('AuthResolver', () => { - let resolver: AuthResolver; +describe('ApiKeyResolver', () => { + let resolver: ApiKeyResolver; let authService: AuthService; let apiKeyService: ApiKeyService; let authzService: AuthZService; @@ -45,7 +45,7 @@ describe('AuthResolver', () => { authzService = new AuthZService(enforcer); cookieService = new CookieService(); authService = new AuthService(cookieService, apiKeyService, authzService); - resolver = new AuthResolver(authService, apiKeyService); + resolver = new ApiKeyResolver(authService, apiKeyService); }); describe('apiKeys', () => { @@ -98,6 +98,7 @@ describe('AuthResolver', () => { expect(apiKeyService.create).toHaveBeenCalledWith({ name: input.name, description: input.description, + overwrite: false, roles: input.roles, permissions: [], }); diff --git a/api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts b/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.ts similarity index 96% rename from api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts rename to api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.ts index 74b21a636d..19e9f4301e 100644 --- a/api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.ts @@ -6,7 +6,6 @@ import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import type { AddRoleForApiKeyInput, - AddRoleForUserInput, ApiKey, ApiKeyWithSecret, CreateApiKeyInput, @@ -17,10 +16,10 @@ import { ApiKeyService } from '@app/unraid-api/auth/api-key.service'; import { GraphqlAuthGuard } from '@app/unraid-api/auth/auth.guard'; import { AuthService } from '@app/unraid-api/auth/auth.service'; -@Resolver('Auth') +@Resolver('ApiKey') @UseGuards(GraphqlAuthGuard) @Throttle({ default: { limit: 100, ttl: 60000 } }) // 100 requests per minute -export class AuthResolver { +export class ApiKeyResolver { constructor( private authService: AuthService, private apiKeyService: ApiKeyService @@ -61,6 +60,7 @@ export class AuthResolver { description: input.description ?? undefined, roles: input.roles ?? [], permissions: input.permissions ?? [], + overwrite: input.overwrite ?? false }); await this.authService.syncApiKeyRoles(apiKey.id, apiKey.roles); diff --git a/api/src/unraid-api/graph/resolvers/resolvers.module.ts b/api/src/unraid-api/graph/resolvers/resolvers.module.ts index b0b954d5d8..b69ec06f3f 100644 --- a/api/src/unraid-api/graph/resolvers/resolvers.module.ts +++ b/api/src/unraid-api/graph/resolvers/resolvers.module.ts @@ -4,13 +4,14 @@ import { AuthModule } from '@app/unraid-api/auth/auth.module'; import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver'; import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver'; -import { AuthResolver } from './auth/auth.resolver'; +import { ApiKeyResolver } from './api-key/api-key.resolver'; import { CloudResolver } from './cloud/cloud.resolver'; import { ConfigResolver } from './config/config.resolver'; import { DisksResolver } from './disks/disks.resolver'; import { DisplayResolver } from './display/display.resolver'; import { FlashResolver } from './flash/flash.resolver'; import { InfoResolver } from './info/info.resolver'; +import { MeResolver } from './me/me.resolver'; import { NotificationsResolver } from './notifications/notifications.resolver'; import { NotificationsService } from './notifications/notifications.service'; import { OnlineResolver } from './online/online.resolver'; @@ -19,13 +20,12 @@ import { RegistrationResolver } from './registration/registration.resolver'; import { ServerResolver } from './servers/server.resolver'; import { VarsResolver } from './vars/vars.resolver'; import { VmsResolver } from './vms/vms.resolver'; -import { MeResolver } from './me/me.resolver'; @Module({ imports: [AuthModule], providers: [ ArrayResolver, - AuthResolver, + ApiKeyResolver, CloudResolver, ConfigResolver, DisksResolver, @@ -43,6 +43,6 @@ import { MeResolver } from './me/me.resolver'; NotificationsService, MeResolver, ], - exports: [AuthModule, AuthResolver], + exports: [AuthModule, ApiKeyResolver], }) export class ResolversModule {} diff --git a/web/components/UserProfile.ce.vue b/web/components/UserProfile.ce.vue index 8b6983e205..047b0189c0 100644 --- a/web/components/UserProfile.ce.vue +++ b/web/components/UserProfile.ce.vue @@ -96,7 +96,7 @@ onBeforeMount(() => { onMounted(() => { if (devConfig.VITE_MOCK_USER_SESSION && devConfig.NODE_ENV === 'development') { - document.cookie = 'unraid_session_cookie=mock-user-session'; + document.cookie = 'unraid_session_cookie=mockusersession'; } }) diff --git a/web/composables/gql/graphql.ts b/web/composables/gql/graphql.ts index 754fd92043..c7c640228b 100644 --- a/web/composables/gql/graphql.ts +++ b/web/composables/gql/graphql.ts @@ -44,10 +44,8 @@ export type AccessUrlInput = { }; export type AddPermissionInput = { - action: Scalars['String']['input']; - possession: Scalars['String']['input']; + actions: Array; resource: Resource; - role: Role; }; export type AddRoleForApiKeyInput = { @@ -70,6 +68,7 @@ export type ApiKey = { description?: Maybe; id: Scalars['ID']['output']; name: Scalars['String']['output']; + permissions: Array; roles: Array; }; @@ -86,6 +85,7 @@ export type ApiKeyWithSecret = { id: Scalars['ID']['output']; key: Scalars['String']['output']; name: Scalars['String']['output']; + permissions: Array; roles: Array; }; @@ -351,8 +351,13 @@ export enum ContainerState { export type CreateApiKeyInput = { description?: InputMaybe; + /** Whether to create the key in memory only (true), or on disk (false) - memory only keys will not persist through reboots of the API */ + memory?: InputMaybe; name: Scalars['String']['input']; - roles: Array; + /** This will replace the existing key if one already exists with the same name, otherwise returns the existing key */ + overwrite?: InputMaybe; + permissions?: InputMaybe>; + roles?: InputMaybe>; }; export type Devices = { @@ -599,7 +604,7 @@ export type Me = UserAccount & { description: Scalars['String']['output']; id: Scalars['ID']['output']; name: Scalars['String']['output']; - permissions?: Maybe; + permissions?: Maybe>; roles: Array; }; @@ -1028,6 +1033,12 @@ export type Pci = { vendorname?: Maybe; }; +export type Permission = { + __typename?: 'Permission'; + actions: Array; + resource: Resource; +}; + export type ProfileModel = { __typename?: 'ProfileModel'; avatar?: Maybe; @@ -1196,7 +1207,7 @@ export enum Resource { Cloud = 'cloud', Config = 'config', Connect = 'connect', - CrashReportingEnabled = 'crash_reporting_enabled', + ConnectRemoteAccess = 'connect__remote_access', Customizations = 'customizations', Dashboard = 'dashboard', Disk = 'disk', @@ -1224,10 +1235,8 @@ export enum Resource { /** Available roles for API keys and users */ export enum Role { Admin = 'admin', - Guest = 'guest', - MyServers = 'my_servers', - Notifier = 'notifier', - Upc = 'upc' + Connect = 'connect', + Guest = 'guest' } export type Server = { @@ -1450,6 +1459,7 @@ export type User = UserAccount & { name: Scalars['String']['output']; /** If the account has a password set */ password?: Maybe; + permissions?: Maybe>; roles: Array; }; @@ -1457,6 +1467,7 @@ export type UserAccount = { description: Scalars['String']['output']; id: Scalars['ID']['output']; name: Scalars['String']['output']; + permissions?: Maybe>; roles: Array; }; @@ -1678,6 +1689,7 @@ export enum VmState { export type Vms = { __typename?: 'Vms'; domain?: Maybe>; + id: Scalars['ID']['output']; }; export enum WAN_ACCESS_TYPE { diff --git a/web/helpers/create-apollo-client.ts b/web/helpers/create-apollo-client.ts index 94dec61d20..5ae63cb124 100644 --- a/web/helpers/create-apollo-client.ts +++ b/web/helpers/create-apollo-client.ts @@ -1,11 +1,4 @@ -import { - ApolloClient, - ApolloLink, - createHttpLink, - from, - Observable, - split, -} from '@apollo/client/core/index.js'; +import { ApolloClient, ApolloLink, createHttpLink, from, Observable, split } from '@apollo/client/core/index.js'; import { onError } from '@apollo/client/link/error/index.js'; import { RetryLink } from '@apollo/client/link/retry/index.js'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js'; @@ -20,7 +13,7 @@ const httpEndpoint = WEBGUI_GRAPHQL; const wsEndpoint = new URL(WEBGUI_GRAPHQL.toString().replace('http', 'ws')); const headers = { - 'x-csrf-token': globalThis.csrf_token, + 'x-csrf-token': globalThis.csrf_token ?? '0000000000000000', }; const httpLink = createHttpLink({ @@ -108,4 +101,4 @@ export const client = new ApolloClient({ cache: createApolloCache(), }); -provideApolloClient(client); +provideApolloClient(client); \ No newline at end of file diff --git a/web/helpers/urls.ts b/web/helpers/urls.ts index ed34b4c265..36ccea3570 100644 --- a/web/helpers/urls.ts +++ b/web/helpers/urls.ts @@ -40,11 +40,6 @@ const DOCS_REGISTRATION_REPLACE_KEY = new URL('/go/changing-the-flash-device/', const SUPPORT = new URL('https://unraid.net'); -// initialize csrf_token in nuxt playground -if (import.meta.env.VITE_CSRF_TOKEN) { - globalThis.csrf_token = import.meta.env.VITE_CSRF_TOKEN; -} - export { ACCOUNT, ACCOUNT_CALLBACK, diff --git a/web/pages/index.vue b/web/pages/index.vue index b999b39105..19c3f4292b 100644 --- a/web/pages/index.vue +++ b/web/pages/index.vue @@ -2,9 +2,9 @@ import { ExclamationTriangleIcon } from '@heroicons/vue/24/solid'; import { BrandButton, BrandLogo } from '@unraid/ui'; import { serverState } from '~/_data/serverState'; +import SsoButtonCe from '~/components/SsoButton.ce.vue'; import type { SendPayloads } from '~/store/callback'; import AES from 'crypto-js/aes'; -import SsoButtonCe from '~/components/SsoButton.ce.vue'; const { registerEntry } = useCustomElements(); onBeforeMount(() => { @@ -15,6 +15,10 @@ useHead({ meta: [{ name: 'viewport', content: 'width=1300' }], }); +onMounted(() => { + document.cookie = 'unraid_session_cookie=mockusersession'; +}); + const valueToMakeCallback = ref(); const callbackDestination = ref(''); @@ -156,7 +160,7 @@ onMounted(() => {

SSO Button Component

- +