Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/src/graphql/generated/api/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ export function CreateApiKeyInputSchema(): z.ZodObject<Properties<CreateApiKeyIn
return z.object({
description: z.string().nullish(),
name: z.string(),
overwrite: z.boolean().nullish(),
permissions: z.array(z.lazy(() => AddPermissionInputSchema())).nullish(),
roles: z.array(RoleSchema).nullish()
})
Expand Down
2 changes: 2 additions & 0 deletions api/src/graphql/generated/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ export enum ContainerState {
export type CreateApiKeyInput = {
description?: InputMaybe<Scalars['String']['input']>;
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<Scalars['Boolean']['input']>;
permissions?: InputMaybe<Array<AddPermissionInput>>;
roles?: InputMaybe<Array<Role>>;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion api/src/unraid-api/auth/api-key.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiKeyWithSecret> = {
id: uuidv4(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +13 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Arrr! We be missin' some important test cases here, matey!

The test suite be lackin' coverage for the new memory and overwrite parameters in the createApiKey mutation.

Add these test cases to yer test suite:

 describe('createApiKey', () => {
+    it('should create new API key in memory when memory flag is true', async () => {
+        const input = {
+            name: 'Memory API Key',
+            roles: [Role.GUEST],
+            memory: true
+        };
+        // Add test implementation
+    });
+
+    it('should overwrite existing API key when overwrite flag is true', async () => {
+        const input = {
+            name: 'Existing Key',
+            roles: [Role.GUEST],
+            overwrite: true
+        };
+        // Add test implementation
+    });
 });

Committable suggestion skipped: line range outside the PR's diff.

let authService: AuthService;
let apiKeyService: ApiKeyService;
let authzService: AuthZService;
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -98,6 +98,7 @@ describe('AuthResolver', () => {
expect(apiKeyService.create).toHaveBeenCalledWith({
name: input.name,
description: input.description,
overwrite: false,
roles: input.roles,
permissions: [],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';

import type {
AddRoleForApiKeyInput,
AddRoleForUserInput,
ApiKey,
ApiKeyWithSecret,
CreateApiKeyInput,
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions api/src/unraid-api/graph/resolvers/resolvers.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -43,6 +43,6 @@ import { MeResolver } from './me/me.resolver';
NotificationsService,
MeResolver,
],
exports: [AuthModule, AuthResolver],
exports: [AuthModule, ApiKeyResolver],
})
export class ResolversModule {}
2 changes: 1 addition & 1 deletion web/components/UserProfile.ce.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
})
</script>
Expand Down
32 changes: 22 additions & 10 deletions web/composables/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,8 @@ export type AccessUrlInput = {
};

export type AddPermissionInput = {
action: Scalars['String']['input'];
possession: Scalars['String']['input'];
actions: Array<Scalars['String']['input']>;
resource: Resource;
role: Role;
};

export type AddRoleForApiKeyInput = {
Expand All @@ -70,6 +68,7 @@ export type ApiKey = {
description?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
permissions: Array<Permission>;
roles: Array<Role>;
};

Expand All @@ -86,6 +85,7 @@ export type ApiKeyWithSecret = {
id: Scalars['ID']['output'];
key: Scalars['String']['output'];
name: Scalars['String']['output'];
permissions: Array<Permission>;
roles: Array<Role>;
};

Expand Down Expand Up @@ -351,8 +351,13 @@ export enum ContainerState {

export type CreateApiKeyInput = {
description?: InputMaybe<Scalars['String']['input']>;
/** 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<Scalars['Boolean']['input']>;
name: Scalars['String']['input'];
roles: Array<Role>;
/** This will replace the existing key if one already exists with the same name, otherwise returns the existing key */
overwrite?: InputMaybe<Scalars['Boolean']['input']>;
permissions?: InputMaybe<Array<AddPermissionInput>>;
roles?: InputMaybe<Array<Role>>;
};

export type Devices = {
Expand Down Expand Up @@ -599,7 +604,7 @@ export type Me = UserAccount & {
description: Scalars['String']['output'];
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
permissions?: Maybe<Scalars['JSON']['output']>;
permissions?: Maybe<Array<Permission>>;
roles: Array<Role>;
};

Expand Down Expand Up @@ -1028,6 +1033,12 @@ export type Pci = {
vendorname?: Maybe<Scalars['String']['output']>;
};

export type Permission = {
__typename?: 'Permission';
actions: Array<Scalars['String']['output']>;
resource: Resource;
};

export type ProfileModel = {
__typename?: 'ProfileModel';
avatar?: Maybe<Scalars['String']['output']>;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -1450,13 +1459,15 @@ export type User = UserAccount & {
name: Scalars['String']['output'];
/** If the account has a password set */
password?: Maybe<Scalars['Boolean']['output']>;
permissions?: Maybe<Array<Permission>>;
roles: Array<Role>;
};

export type UserAccount = {
description: Scalars['String']['output'];
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
permissions?: Maybe<Array<Permission>>;
roles: Array<Role>;
};

Expand Down Expand Up @@ -1678,6 +1689,7 @@ export enum VmState {
export type Vms = {
__typename?: 'Vms';
domain?: Maybe<Array<VmDomain>>;
id: Scalars['ID']['output'];
};

export enum WAN_ACCESS_TYPE {
Expand Down
13 changes: 3 additions & 10 deletions web/helpers/create-apollo-client.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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({
Expand Down Expand Up @@ -108,4 +101,4 @@ export const client = new ApolloClient({
cache: createApolloCache(),
});

provideApolloClient(client);
provideApolloClient(client);
5 changes: 0 additions & 5 deletions web/helpers/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions web/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -15,6 +15,10 @@ useHead({
meta: [{ name: 'viewport', content: 'width=1300' }],
});

onMounted(() => {
document.cookie = 'unraid_session_cookie=mockusersession';
});
Comment on lines +18 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Blimey! This cookie be vulnerable to pirates!

Arr matey, setting cookies directly in the frontend code be askin' for trouble! Any scurvy dog with a browser console could manipulate this session.

Consider these security measures to protect yer treasure:

  • Move cookie settin' to the backend crow's nest
  • Add security flags like httpOnly, secure, and sameSite
  • Use proper session management instead of hardcoded values

Here's a safer way to handle sessions, ye landlubber:

-onMounted(() => {
-  document.cookie = 'unraid_session_cookie=mockusersession';
-});

Move this to your backend authentication route instead!


const valueToMakeCallback = ref<SendPayloads | undefined>();
const callbackDestination = ref<string>('');

Expand Down Expand Up @@ -156,7 +160,7 @@ onMounted(() => {
<div class="bg-background">
<hr class="border-black dark:border-white" />
<h2 class="text-xl font-semibold font-mono">SSO Button Component</h2>
<SsoButtonCe :ssoenabled="serverState.ssoEnabled" />
<SsoButtonCe :ssoenabled="serverState.ssoEnabled" />
</div>
</div>
</client-only>
Expand Down
Loading