mirror of
https://github.com/unraid/api.git
synced 2025-12-31 05:29:48 -06:00
feat: add permission documentation by using a custom decorator (#1355)
* usePermissions applies both authz + graphQL directive logic to allow permissions and documentation in one place <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new GraphQL permission directive that documents required permissions for API fields. - Added enums for defining action verbs (create, update, delete, read) and possession types (any, own, own any) to enable granular access control. - Added a new health field to the Query type for improved API health monitoring. - **Chores** - Consolidated permission handling by updating import sources and retiring legacy authorization tests and code, enhancing overall maintainability. - Updated configuration version in the API settings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="4.6.6"
|
||||
version="4.4.1"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||
# ------------------------------------------------------
|
||||
|
||||
"""Directive to document required permissions for fields"""
|
||||
directive @usePermissions(
|
||||
"""The action verb required for access"""
|
||||
action: AuthActionVerb
|
||||
|
||||
"""The resource required for access"""
|
||||
resource: String
|
||||
|
||||
"""The possession type required for access"""
|
||||
possession: AuthPossession
|
||||
) on FIELD_DEFINITION
|
||||
|
||||
type ApiKeyResponse {
|
||||
valid: Boolean!
|
||||
error: String
|
||||
@@ -1412,6 +1424,8 @@ type Query {
|
||||
services: [Service!]!
|
||||
shares: [Share!]!
|
||||
vars: Vars!
|
||||
|
||||
"""Get information about all VMs on the system"""
|
||||
vms: Vms!
|
||||
parityHistory: [ParityCheck!]!
|
||||
array: UnraidArray!
|
||||
@@ -1421,6 +1435,7 @@ type Query {
|
||||
docker: Docker!
|
||||
disks: [Disk!]!
|
||||
disk(id: String!): Disk!
|
||||
health: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@@ -1590,4 +1605,19 @@ type Subscription {
|
||||
serversSubscription: Server!
|
||||
parityHistorySubscription: ParityCheck!
|
||||
arraySubscription: UnraidArray!
|
||||
}
|
||||
|
||||
"""Available authentication action verbs"""
|
||||
enum AuthActionVerb {
|
||||
CREATE
|
||||
UPDATE
|
||||
DELETE
|
||||
READ
|
||||
}
|
||||
|
||||
"""Available authentication possession types"""
|
||||
enum AuthPossession {
|
||||
ANY
|
||||
OWN
|
||||
OWN_ANY
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
import { Enforcer } from 'casbin';
|
||||
import { GraphQLResolveInfo, GraphQLSchema } from 'graphql';
|
||||
import { AuthActionVerb, AuthPossession, AuthZService, UsePermissions } from 'nest-authz';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
authSchemaTransformer,
|
||||
getAuthEnumTypeDefs,
|
||||
transformResolvers,
|
||||
} from '@app/unraid-api/graph/directives/auth.directive.js';
|
||||
|
||||
// Mock UsePermissions function
|
||||
vi.mock('nest-authz', () => ({
|
||||
AuthActionVerb: {
|
||||
READ: 'READ',
|
||||
CREATE: 'CREATE',
|
||||
UPDATE: 'UPDATE',
|
||||
DELETE: 'DELETE',
|
||||
},
|
||||
AuthPossession: {
|
||||
OWN: 'OWN',
|
||||
ANY: 'ANY',
|
||||
},
|
||||
UsePermissions: vi.fn(),
|
||||
}));
|
||||
|
||||
describe.skip('Auth Directive', () => {
|
||||
let schema: GraphQLSchema;
|
||||
|
||||
const typeDefs = `
|
||||
${getAuthEnumTypeDefs()}
|
||||
|
||||
type Query {
|
||||
protectedField: String @auth(action: READ, resource: "USER", possession: OWN)
|
||||
unprotectedField: String
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
protectedField: () => 'protected data',
|
||||
unprotectedField: () => 'public data',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const authZService = new AuthZService({} as Enforcer);
|
||||
// Create schema for each test
|
||||
schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers: transformResolvers(resolvers, authZService),
|
||||
});
|
||||
|
||||
// Apply our auth schema transformer
|
||||
schema = authSchemaTransformer(schema);
|
||||
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('authSchemaTransformer', () => {
|
||||
it('should add permission information to field description', () => {
|
||||
const queryType = schema.getQueryType();
|
||||
if (!queryType) throw new Error('Query type not found in schema');
|
||||
const protectedField = queryType.getFields().protectedField;
|
||||
|
||||
expect(protectedField.description).toContain('Required Permissions');
|
||||
expect(protectedField.description).toContain('Action: **READ**');
|
||||
expect(protectedField.description).toContain('Resource: **USER**');
|
||||
expect(protectedField.description).toContain('Possession: **OWN**');
|
||||
});
|
||||
|
||||
it('should store permission requirements in field extensions', () => {
|
||||
const queryType = schema.getQueryType();
|
||||
if (!queryType) throw new Error('Query type not found in schema');
|
||||
const protectedField = queryType.getFields().protectedField;
|
||||
|
||||
expect(protectedField.extensions).toBeDefined();
|
||||
expect(protectedField.extensions.requiredPermissions).toEqual({
|
||||
action: 'READ',
|
||||
resource: 'USER',
|
||||
possession: 'OWN',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify fields without auth directive', () => {
|
||||
const queryType = schema.getQueryType();
|
||||
if (!queryType) throw new Error('Query type not found in schema');
|
||||
const unprotectedField = queryType.getFields().unprotectedField;
|
||||
|
||||
expect(unprotectedField.extensions?.requiredPermissions).toBeUndefined();
|
||||
expect(unprotectedField.description).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformResolvers', () => {
|
||||
it('should wrap resolvers to check permissions before execution', async () => {
|
||||
const queryType = schema.getQueryType();
|
||||
if (!queryType) throw new Error('Query type not found in schema');
|
||||
const protectedField = queryType.getFields().protectedField;
|
||||
|
||||
const mockSource = {};
|
||||
const mockArgs = {};
|
||||
const mockContext: { requiredPermissions?: any } = {};
|
||||
|
||||
// Instead of mocking GraphQLResolveInfo, we can invoke the wrapped resolver directly
|
||||
// Create a simple function to extract the resolver and call it with our mock objects
|
||||
const testResolver = async () => {
|
||||
if (!schema.getQueryType()) return;
|
||||
|
||||
// Get the schema fields
|
||||
const queryFields = schema.getQueryType()!.getFields();
|
||||
|
||||
// Call the manually wrapped resolver
|
||||
if (queryFields.protectedField && queryFields.protectedField.resolve) {
|
||||
const result = await queryFields.protectedField.resolve(
|
||||
mockSource,
|
||||
mockArgs,
|
||||
mockContext,
|
||||
{
|
||||
fieldName: 'protectedField',
|
||||
parentType: { name: 'Query' },
|
||||
schema,
|
||||
// We need these fields, but they aren't actually used in the auth directive code
|
||||
fieldNodes: [] as any,
|
||||
returnType: {} as any,
|
||||
path: {} as any,
|
||||
fragments: {} as any,
|
||||
rootValue: null as any,
|
||||
operation: {} as any,
|
||||
variableValues: {} as any,
|
||||
} as unknown as GraphQLResolveInfo
|
||||
);
|
||||
return result;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
await testResolver();
|
||||
|
||||
// Check that permissions were set in context
|
||||
expect(mockContext).toHaveProperty('requiredPermissions');
|
||||
expect(mockContext.requiredPermissions).toEqual({
|
||||
action: 'READ',
|
||||
resource: 'USER',
|
||||
possession: 'OWN',
|
||||
});
|
||||
|
||||
// Check that UsePermissions was called with the right params
|
||||
expect(UsePermissions).toHaveBeenCalledWith({
|
||||
action: 'READ',
|
||||
resource: 'USER',
|
||||
possession: 'OWN',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply permissions for unprotected fields', async () => {
|
||||
const queryType = schema.getQueryType();
|
||||
if (!queryType) throw new Error('Query type not found in schema');
|
||||
const unprotectedField = queryType.getFields().unprotectedField;
|
||||
|
||||
const mockSource = {};
|
||||
const mockArgs = {};
|
||||
const mockContext: { requiredPermissions?: any } = {};
|
||||
|
||||
// Instead of mocking GraphQLResolveInfo, we can invoke the wrapped resolver directly
|
||||
const testResolver = async () => {
|
||||
if (!schema.getQueryType()) return;
|
||||
|
||||
// Get the schema fields
|
||||
const queryFields = schema.getQueryType()!.getFields();
|
||||
|
||||
// Call the manually wrapped resolver
|
||||
if (queryFields.unprotectedField && queryFields.unprotectedField.resolve) {
|
||||
const result = await queryFields.unprotectedField.resolve(
|
||||
mockSource,
|
||||
mockArgs,
|
||||
mockContext,
|
||||
{
|
||||
fieldName: 'unprotectedField',
|
||||
parentType: { name: 'Query' },
|
||||
schema,
|
||||
// We need these fields, but they aren't actually used in the auth directive code
|
||||
fieldNodes: [] as any,
|
||||
returnType: {} as any,
|
||||
path: {} as any,
|
||||
fragments: {} as any,
|
||||
rootValue: null as any,
|
||||
operation: {} as any,
|
||||
variableValues: {} as any,
|
||||
} as unknown as GraphQLResolveInfo
|
||||
);
|
||||
return result;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
await testResolver();
|
||||
|
||||
// Check that permissions were not set or checked
|
||||
expect(mockContext.requiredPermissions).toBeUndefined();
|
||||
expect(UsePermissions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle an array of resolvers', () => {
|
||||
const authZService = new AuthZService({} as Enforcer);
|
||||
const resolversArray = [
|
||||
{ Query: { field1: () => 'data' } },
|
||||
{ Mutation: { field2: () => 'data' } },
|
||||
] as any; // Type assertion to avoid complex IResolvers typing
|
||||
|
||||
const transformed = transformResolvers(resolversArray, authZService);
|
||||
expect(Array.isArray(transformed)).toBe(true);
|
||||
expect(transformed).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAuthEnumTypeDefs', () => {
|
||||
it('should generate valid SDL for auth enums', () => {
|
||||
const typeDefs = getAuthEnumTypeDefs();
|
||||
|
||||
expect(typeDefs).toContain('enum AuthActionVerb');
|
||||
expect(typeDefs).toContain('enum AuthPossession');
|
||||
expect(typeDefs).toContain('directive @auth');
|
||||
|
||||
// Check for enum values
|
||||
Object.keys(AuthActionVerb)
|
||||
.filter((key) => isNaN(Number(key)))
|
||||
.forEach((key) => {
|
||||
expect(typeDefs).toContain(key);
|
||||
});
|
||||
|
||||
Object.keys(AuthPossession)
|
||||
.filter((key) => isNaN(Number(key)))
|
||||
.forEach((key) => {
|
||||
expect(typeDefs).toContain(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,219 +0,0 @@
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
import { getDirective, IResolvers, MapperKind, mapSchema } from '@graphql-tools/utils';
|
||||
import { GraphQLEnumType, GraphQLSchema } from 'graphql';
|
||||
import { AuthActionVerb, AuthPossession, AuthZService, BatchApproval } from 'nest-authz';
|
||||
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
|
||||
/**
|
||||
* @wip : This function does not correctly apply permission to every field.
|
||||
* @todo : Once we've determined how to fix the transformResolvers function, uncomment this.
|
||||
*/
|
||||
export function transformResolvers(
|
||||
resolvers: IResolvers | IResolvers[],
|
||||
authZService: AuthZService
|
||||
): IResolvers | IResolvers[] {
|
||||
if (Array.isArray(resolvers)) {
|
||||
return resolvers.map((r) => transformResolvers(r, authZService)) as IResolvers[];
|
||||
}
|
||||
|
||||
// Iterate over each type in the resolvers object
|
||||
Object.keys(resolvers).forEach((typeName) => {
|
||||
const typeResolvers = resolvers[typeName];
|
||||
// Iterate over each field within the type
|
||||
Object.keys(typeResolvers).forEach((fieldName) => {
|
||||
const fieldResolver = typeResolvers[fieldName];
|
||||
// Skip non-function resolvers (or if it's not defined)
|
||||
|
||||
if (typeof fieldResolver !== 'function') {
|
||||
return;
|
||||
}
|
||||
// Check if this field has permission metadata in its extensions property
|
||||
// We need to wrap the resolver in a function that checks if the user has the required permissions
|
||||
|
||||
const originalResolver = fieldResolver;
|
||||
|
||||
// Create a wrapped resolver that will extract permissions from info
|
||||
typeResolvers[fieldName] = async (source, args, context, info) => {
|
||||
// Access the extensions from the field definition in the schema
|
||||
console.log(
|
||||
'resolving',
|
||||
info.fieldName,
|
||||
info.parentType.name,
|
||||
info.schema.getType(info.parentType.name).getFields()[info.fieldName].extensions
|
||||
);
|
||||
console.log('user', context?.req?.user);
|
||||
const fieldExtensions = info.schema.getType(info.parentType.name).getFields()[
|
||||
info.fieldName
|
||||
].extensions;
|
||||
if (fieldExtensions?.requiredPermissions && context?.req?.user) {
|
||||
const { action, resource, possession } = fieldExtensions.requiredPermissions;
|
||||
|
||||
if (context) {
|
||||
// Handle OWN_ANY possession by checking both ANY and OWN permissions
|
||||
if (possession === AuthPossession.OWN_ANY) {
|
||||
context.requiredPermissions = [
|
||||
{
|
||||
action: action.toUpperCase(),
|
||||
resource: resource.toUpperCase(),
|
||||
possession: AuthPossession.ANY,
|
||||
},
|
||||
{
|
||||
action: action.toUpperCase(),
|
||||
resource: resource.toUpperCase(),
|
||||
possession: AuthPossession.OWN,
|
||||
},
|
||||
];
|
||||
// For OWN_ANY, we want to check both ANY and OWN permissions
|
||||
// If either check passes, the user has permission
|
||||
const hasPermission = await authZService.enforce(
|
||||
context.user,
|
||||
resource.toUpperCase(),
|
||||
action.toUpperCase(),
|
||||
BatchApproval.ANY
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new UnauthorizedException(
|
||||
'Unauthorized: User does not have required permissions'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
context.requiredPermissions = {
|
||||
action: action.toUpperCase(),
|
||||
resource: resource.toUpperCase(),
|
||||
possession: possession.toUpperCase(),
|
||||
};
|
||||
|
||||
// For regular permissions, we check the specific possession type
|
||||
const hasPermission = await authZService.enforce(
|
||||
context.user,
|
||||
resource.toUpperCase(),
|
||||
`${action.toUpperCase()}:${possession.toUpperCase()}`
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new UnauthorizedException(
|
||||
'Unauthorized: User does not have required permissions'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the original resolver after permission check
|
||||
return await originalResolver(source, args, context, info);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return resolvers;
|
||||
}
|
||||
|
||||
export function authSchemaTransformer(schema: GraphQLSchema): GraphQLSchema {
|
||||
return mapSchema(schema, {
|
||||
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
|
||||
const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];
|
||||
|
||||
if (authDirective) {
|
||||
const {
|
||||
action: actionValue,
|
||||
resource: resourceValue,
|
||||
possession: possessionValue,
|
||||
} = authDirective;
|
||||
|
||||
if (!actionValue || !resourceValue || !possessionValue) {
|
||||
console.warn(
|
||||
`Auth directive on ${typeName}.${fieldName} is missing required arguments.`
|
||||
);
|
||||
return fieldConfig;
|
||||
}
|
||||
|
||||
// Append permission information to the field description
|
||||
const permissionDoc = `
|
||||
|
||||
#### Required Permissions:
|
||||
|
||||
- Action: **${actionValue}**
|
||||
- Resource: **${resourceValue}**
|
||||
- Possession: **${possessionValue}**`;
|
||||
const newDescription = fieldConfig.description
|
||||
? `${fieldConfig.description}${permissionDoc}`
|
||||
: permissionDoc;
|
||||
fieldConfig.description = newDescription;
|
||||
|
||||
// Store the required permissions in the field config extensions
|
||||
fieldConfig.extensions = {
|
||||
...fieldConfig.extensions,
|
||||
requiredPermissions: {
|
||||
action: actionValue.toUpperCase() as AuthActionVerb,
|
||||
resource: resourceValue.toUpperCase() as Resource,
|
||||
possession: possessionValue.toUpperCase() as AuthPossession,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return fieldConfig;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates GraphQL SDL strings for the authentication enums.
|
||||
*/
|
||||
export function getAuthEnumTypeDefs(): string {
|
||||
// Helper to generate enum values string part with descriptions
|
||||
const getEnumValues = <T>(tsEnum: Record<string, T>): string => {
|
||||
return Object.entries(tsEnum)
|
||||
.filter(([key]) => isNaN(Number(key))) // Filter out numeric keys
|
||||
.map(([key]) => ` ${key}`)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
return `"""
|
||||
Available authentication action verbs
|
||||
"""
|
||||
enum AuthActionVerb {
|
||||
${getEnumValues(AuthActionVerb)}
|
||||
}
|
||||
|
||||
"""
|
||||
Available authentication possession types
|
||||
"""
|
||||
enum AuthPossession {
|
||||
${getEnumValues(AuthPossession)}
|
||||
}
|
||||
|
||||
directive @auth(
|
||||
action: AuthActionVerb!,
|
||||
resource: String!,
|
||||
possession: AuthPossession!
|
||||
) on FIELD_DEFINITION
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic function to convert TypeScript enums to GraphQL enums
|
||||
* (Kept for potential other uses, but not used for Auth enums in schema generation anymore)
|
||||
*/
|
||||
export function createGraphQLEnumFromTSEnum<T>(
|
||||
tsEnum: Record<string, T>,
|
||||
name: string,
|
||||
description: string
|
||||
): GraphQLEnumType {
|
||||
const enumValues = {};
|
||||
|
||||
Object.keys(tsEnum).forEach((key) => {
|
||||
if (isNaN(Number(key))) {
|
||||
// Skip numeric keys (enum in TS has both string and number keys)
|
||||
enumValues[key] = { value: tsEnum[key] };
|
||||
}
|
||||
});
|
||||
|
||||
return new GraphQLEnumType({
|
||||
name,
|
||||
description,
|
||||
values: enumValues,
|
||||
});
|
||||
}
|
||||
126
api/src/unraid-api/graph/directives/use-permissions.directive.ts
Normal file
126
api/src/unraid-api/graph/directives/use-permissions.directive.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Directive } from '@nestjs/graphql';
|
||||
|
||||
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
|
||||
import {
|
||||
DirectiveLocation,
|
||||
GraphQLDirective,
|
||||
GraphQLEnumType,
|
||||
GraphQLSchema,
|
||||
GraphQLString,
|
||||
} from 'graphql';
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions as NestAuthzUsePermissions } from 'nest-authz';
|
||||
|
||||
// Re-export the types from nest-authz
|
||||
export { AuthActionVerb, AuthPossession };
|
||||
|
||||
const buildGraphQLEnum = (
|
||||
enumObj: Record<string, string | number>,
|
||||
name: string,
|
||||
description: string
|
||||
) => {
|
||||
const values = Object.entries(enumObj)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.reduce(
|
||||
(acc, [key]) => {
|
||||
acc[key] = { value: key };
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { value: string }>
|
||||
);
|
||||
|
||||
return new GraphQLEnumType({ name, description, values });
|
||||
};
|
||||
|
||||
// Create GraphQL enum types for auth action verbs and possessions
|
||||
const AuthActionVerbEnum = buildGraphQLEnum(
|
||||
AuthActionVerb,
|
||||
'AuthActionVerb',
|
||||
'Available authentication action verbs'
|
||||
);
|
||||
|
||||
const AuthPossessionEnum = buildGraphQLEnum(
|
||||
AuthPossession,
|
||||
'AuthPossession',
|
||||
'Available authentication possession types'
|
||||
);
|
||||
|
||||
// Create the auth directive
|
||||
export const UsePermissionsDirective = new GraphQLDirective({
|
||||
name: 'usePermissions',
|
||||
description: 'Directive to document required permissions for fields',
|
||||
locations: [DirectiveLocation.FIELD_DEFINITION],
|
||||
args: {
|
||||
action: {
|
||||
type: AuthActionVerbEnum,
|
||||
description: 'The action verb required for access',
|
||||
},
|
||||
resource: {
|
||||
type: GraphQLString,
|
||||
description: 'The resource required for access',
|
||||
},
|
||||
possession: {
|
||||
type: AuthPossessionEnum,
|
||||
description: 'The possession type required for access',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a decorator that combines both the GraphQL directive and UsePermissions
|
||||
export const UsePermissions = (permissions: {
|
||||
action: AuthActionVerb;
|
||||
resource: string;
|
||||
possession: AuthPossession;
|
||||
}) => {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
// Apply UsePermissions for actual authorization
|
||||
NestAuthzUsePermissions(permissions)(target, propertyKey, descriptor);
|
||||
|
||||
// Apply GraphQL directive using NestJS's @Directive decorator
|
||||
Directive(
|
||||
`@usePermissions(action: ${permissions.action.toUpperCase()}, resource: "${permissions.resource}", possession: ${permissions.possession.toUpperCase()})`
|
||||
)(target, propertyKey, descriptor);
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
};
|
||||
|
||||
// Schema transformer to add permission documentation
|
||||
export function usePermissionsSchemaTransformer(schema: GraphQLSchema) {
|
||||
return mapSchema(schema, {
|
||||
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
|
||||
const usePermissionsDirective = getDirective(schema, fieldConfig, 'usePermissions')?.[0];
|
||||
if (usePermissionsDirective) {
|
||||
const {
|
||||
action: actionValue,
|
||||
resource: resourceValue,
|
||||
possession: possessionValue,
|
||||
} = usePermissionsDirective;
|
||||
|
||||
if (!actionValue || !resourceValue || !possessionValue) {
|
||||
console.warn(
|
||||
`UsePermissions directive on ${typeName}.${fieldName} is missing required arguments.`
|
||||
);
|
||||
return fieldConfig;
|
||||
}
|
||||
|
||||
// Append permission information to the field description
|
||||
const permissionDoc = `
|
||||
#### Required Permissions:
|
||||
|
||||
- Action: **${actionValue}**
|
||||
- Resource: **${resourceValue}**
|
||||
- Possession: **${possessionValue}**`;
|
||||
const descriptionDoc = fieldConfig.description
|
||||
? `
|
||||
|
||||
#### Description:
|
||||
|
||||
${fieldConfig.description}`
|
||||
: '';
|
||||
fieldConfig.description = permissionDoc + descriptionDoc;
|
||||
}
|
||||
|
||||
return fieldConfig;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import { JSONResolver, URLResolver } from 'graphql-scalars';
|
||||
import { ENVIRONMENT } from '@app/environment.js';
|
||||
import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
import {
|
||||
UsePermissionsDirective,
|
||||
usePermissionsSchemaTransformer,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin.js';
|
||||
import { ResolversModule } from '@app/unraid-api/graph/resolvers/resolvers.module.js';
|
||||
import { sandboxPlugin } from '@app/unraid-api/graph/sandbox-plugin.js';
|
||||
@@ -50,7 +54,9 @@ import { sandboxPlugin } from '@app/unraid-api/graph/sandbox-plugin.js';
|
||||
},
|
||||
buildSchemaOptions: {
|
||||
dateScalarMode: 'isoDate',
|
||||
directives: [UsePermissionsDirective],
|
||||
},
|
||||
transformSchema: usePermissionsSchemaTransformer,
|
||||
validationRules: [NoUnusedVariablesRule],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
|
||||
import { AuthService } from '@app/unraid-api/auth/auth.service.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import {
|
||||
AddRoleForApiKeyInput,
|
||||
ApiKey,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Args, Mutation, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import {
|
||||
ArrayDisk,
|
||||
ArrayDiskInput,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Query, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { UnraidArray } from '@app/unraid-api/graph/resolvers/array/array.model.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Args, Mutation, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { GraphQLJSON } from 'graphql-scalars';
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { ParityService } from '@app/unraid-api/graph/resolvers/array/parity.service.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { ParityCheckMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
|
||||
import { Query, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { GraphQLJSON } from 'graphql-scalars';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
import { ParityCheck } from '@app/unraid-api/graph/resolvers/array/parity.model.js';
|
||||
import { ParityService } from '@app/unraid-api/graph/resolvers/array/parity.service.js';
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { getAllowedOrigins } from '@app/common/allowed-origins.js';
|
||||
import { checkApi } from '@app/graphql/resolvers/query/cloud/check-api.js';
|
||||
import { checkCloud } from '@app/graphql/resolvers/query/cloud/check-cloud.js';
|
||||
import { checkMinigraphql } from '@app/graphql/resolvers/query/cloud/check-minigraphql.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Cloud } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js';
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { getters } from '@app/store/index.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Config } from '@app/unraid-api/graph/resolvers/config/config.model.js';
|
||||
|
||||
|
||||
@@ -3,11 +3,15 @@ import { Args, ID, Mutation, Query, ResolveField, Resolver } from '@nestjs/graph
|
||||
|
||||
import { Layout } from '@jsonforms/core';
|
||||
import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { getAllowedOrigins } from '@app/common/allowed-origins.js';
|
||||
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js';
|
||||
import { logoutUser, updateAllowedOrigins } from '@app/store/modules/config.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { ConnectSettingsService } from '@app/unraid-api/graph/resolvers/connect/connect-settings.service.js';
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { store } from '@app/store/index.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import {
|
||||
Connect,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Args, Int, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Disk } from '@app/unraid-api/graph/resolvers/disks/disks.model.js';
|
||||
import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js';
|
||||
|
||||
@@ -3,10 +3,13 @@ import { existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Display } from '@app/unraid-api/graph/resolvers/info/info.model.js';
|
||||
|
||||
@@ -60,12 +63,12 @@ const states = {
|
||||
|
||||
@Resolver(() => Display)
|
||||
export class DisplayResolver {
|
||||
@Query(() => Display)
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.READ,
|
||||
resource: Resource.DISPLAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
@Query(() => Display)
|
||||
public async display(): Promise<Display> {
|
||||
/**
|
||||
* This is deprecated, remove it eventually
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js';
|
||||
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import {
|
||||
Docker,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { getters } from '@app/store/index.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Flash } from '@app/unraid-api/graph/resolvers/flash/flash.model.js';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
import { baseboard as getBaseboard, system as getSystem } from 'systeminformation';
|
||||
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
@@ -14,6 +13,11 @@ import {
|
||||
generateOs,
|
||||
generateVersions,
|
||||
} from '@app/graphql/resolvers/query/info.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import {
|
||||
Baseboard,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Args, Int, Query, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { LogFile, LogFileContent } from '@app/unraid-api/graph/resolvers/logs/logs.model.js';
|
||||
import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { getServerIps } from '@app/graphql/resolvers/subscription/network.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { AccessUrl, Network } from '@app/unraid-api/graph/resolvers/connect/connect.model.js';
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Args, Mutation, Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import {
|
||||
Notification,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Online } from '@app/unraid-api/graph/resolvers/online/online.model.js';
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Query, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Owner } from '@app/unraid-api/graph/resolvers/owner/owner.model.js';
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Query, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import { getKeyFile } from '@app/core/utils/misc/get-key-file.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
import { FileLoadStatus } from '@app/store/types.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import {
|
||||
Registration,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Query, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import { getLocalServer } from '@app/graphql/schema/utils.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Server as ServerModel } from '@app/unraid-api/graph/resolvers/servers/server.model.js';
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { getters } from '@app/store/index.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { Vars } from '@app/unraid-api/graph/resolvers/vars/vars.model.js';
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { VmMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
|
||||
import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { VmDomain, Vms } from '@app/unraid-api/graph/resolvers/vms/vms.model.js';
|
||||
import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js';
|
||||
@@ -10,7 +13,7 @@ import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js';
|
||||
export class VmsResolver {
|
||||
constructor(private readonly vmsService: VmsService) {}
|
||||
|
||||
@Query(() => Vms)
|
||||
@Query(() => Vms, { description: 'Get information about all VMs on the system' })
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.READ,
|
||||
resource: Resource.VMS,
|
||||
@@ -35,12 +38,6 @@ export class VmsResolver {
|
||||
|
||||
@ResolveField(() => [VmDomain])
|
||||
public async domain(): Promise<Array<VmDomain>> {
|
||||
try {
|
||||
return await this.vmsService.getDomains();
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to retrieve VM domains: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
return this.domains();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { bootTimestamp } from '@app/common/dashboard/boot-timestamp.js';
|
||||
import { API_VERSION } from '@app/environment.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { DynamicRemoteAccessType } from '@app/unraid-api/graph/resolvers/connect/connect.model.js';
|
||||
import { Service } from '@app/unraid-api/graph/services/service.model.js';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { getShares } from '@app/core/utils/shares/get-shares.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Share } from '@app/unraid-api/graph/resolvers/array/array.model.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { GraphqlUser } from '@app/unraid-api/auth/user.decorator.js';
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { UserAccount } from '@app/unraid-api/graph/user/user.model.js';
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { AuthPossession } from 'nest-authz';
|
||||
import { AuthActionVerb } from 'nest-authz/dist/src/types.js';
|
||||
|
||||
/**
|
||||
* Generates GraphQL SDL strings for the authentication enums.
|
||||
*/
|
||||
export function getAuthEnumTypeDefs(): string {
|
||||
// Helper to generate enum values string part with descriptions
|
||||
const getEnumValues = <T>(tsEnum: Record<string, T>): string => {
|
||||
return Object.entries(tsEnum)
|
||||
.filter(([key]) => isNaN(Number(key))) // Filter out numeric keys
|
||||
.map(([key]) => ` ${key}`)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
return `"""
|
||||
Available authentication action verbs
|
||||
"""
|
||||
enum AuthActionVerb {
|
||||
${getEnumValues(AuthActionVerb)}
|
||||
}
|
||||
|
||||
"""
|
||||
Available authentication possession types
|
||||
"""
|
||||
enum AuthPossession {
|
||||
${getEnumValues(AuthPossession)}
|
||||
}
|
||||
|
||||
directive @auth(
|
||||
action: AuthActionVerb!,
|
||||
resource: String!,
|
||||
possession: AuthPossession!
|
||||
) on FIELD_DEFINITION
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user