mirror of
https://github.com/unraid/api.git
synced 2026-01-03 23:19:54 -06:00
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Introduced Docker management UI components: Overview, Logs, Console, Preview, and Edit. - Added responsive Card/Detail layouts with grouping, bulk actions, and tabs. - New UnraidToaster component and global toaster configuration. - Component auto-mounting improved with async loading and multi-selector support. - UI/UX - Overhauled theme system (light/dark tokens, primary/orange accents) and added theme variants. - Header OS version now includes integrated changelog modal. - Registration displays warning states; multiple visual polish updates. - API - CPU load now includes percentGuest and percentSteal metrics. - Chores - Migrated web app to Vite; updated artifacts and manifests. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: mdatelle <mike@datelle.net> Co-authored-by: Michael Datelle <mdatelle@icloud.com>
213 lines
7.5 KiB
TypeScript
213 lines
7.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { AuthAction, Resource } from '~/composables/gql/graphql';
|
|
import { decodeScopesToPermissions, encodePermissionsToScopes } from '~/utils/authorizationScopes';
|
|
|
|
describe('authorizationScopes', () => {
|
|
describe('encodePermissionsToScopes', () => {
|
|
describe('duplicate handling', () => {
|
|
it('should deduplicate action verbs when multiple permissions have duplicate actions', () => {
|
|
const permissions = [
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.READ_ANY, AuthAction.READ_OWN, AuthAction.UPDATE_ANY],
|
|
},
|
|
];
|
|
|
|
const scopes = encodePermissionsToScopes([], permissions);
|
|
|
|
// Should produce "docker:read_any+read_own+update_any" with distinct actions preserved
|
|
expect(scopes).toHaveLength(1);
|
|
expect(scopes[0]).toBe('docker:read_any+read_own+update_any');
|
|
});
|
|
|
|
it('should deduplicate resource names when grouped', () => {
|
|
const permissions = [
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.READ_ANY],
|
|
},
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.READ_OWN],
|
|
},
|
|
];
|
|
|
|
const scopes = encodePermissionsToScopes([], permissions);
|
|
|
|
// Should produce "docker:read_any+read_own" merging both permissions
|
|
expect(scopes).toHaveLength(1);
|
|
expect(scopes[0]).toBe('docker:read_any+read_own');
|
|
});
|
|
|
|
it('should handle multiple duplicate resources and actions correctly', () => {
|
|
const permissions = [
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.READ_ANY, AuthAction.READ_OWN, AuthAction.UPDATE_ANY],
|
|
},
|
|
{
|
|
resource: Resource.VMS,
|
|
actions: [AuthAction.READ_ANY, AuthAction.UPDATE_OWN, AuthAction.UPDATE_ANY],
|
|
},
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.READ_OWN, AuthAction.UPDATE_OWN],
|
|
},
|
|
];
|
|
|
|
const scopes = encodePermissionsToScopes([], permissions);
|
|
|
|
// Docker: READ_ANY, READ_OWN, UPDATE_ANY, UPDATE_OWN (merged from both)
|
|
// VMS: READ_ANY, UPDATE_OWN, UPDATE_ANY
|
|
// Different action sets, so separate scopes
|
|
expect(scopes).toHaveLength(2);
|
|
const scopeStrings = scopes.sort();
|
|
expect(scopeStrings).toContain('docker:read_any+read_own+update_any+update_own');
|
|
expect(scopeStrings).toContain('vms:read_any+update_any+update_own');
|
|
});
|
|
|
|
it('should maintain consistent sorting for actions and resources', () => {
|
|
const permissions = [
|
|
{
|
|
resource: Resource.VMS,
|
|
actions: [AuthAction.UPDATE_ANY, AuthAction.READ_ANY],
|
|
},
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.UPDATE_ANY, AuthAction.READ_ANY],
|
|
},
|
|
];
|
|
|
|
const scopes = encodePermissionsToScopes([], permissions);
|
|
|
|
// Should sort resources (docker before vms) and actions alphabetically
|
|
expect(scopes).toHaveLength(1);
|
|
expect(scopes[0]).toBe('docker+vms:read_any+update_any');
|
|
});
|
|
|
|
it('should handle all CRUD actions without creating wildcard', () => {
|
|
const permissions = [
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [
|
|
AuthAction.CREATE_ANY,
|
|
AuthAction.CREATE_OWN,
|
|
AuthAction.READ_ANY,
|
|
AuthAction.READ_OWN,
|
|
AuthAction.UPDATE_ANY,
|
|
AuthAction.UPDATE_OWN,
|
|
AuthAction.DELETE_ANY,
|
|
AuthAction.DELETE_OWN,
|
|
],
|
|
},
|
|
];
|
|
|
|
const scopes = encodePermissionsToScopes([], permissions);
|
|
|
|
// Should just list all actions, no wildcard conversion
|
|
expect(scopes).toHaveLength(1);
|
|
expect(scopes[0]).toBe(
|
|
'docker:create_any+create_own+delete_any+delete_own+read_any+read_own+update_any+update_own'
|
|
);
|
|
});
|
|
|
|
it('should handle edge case with empty actions after deduplication', () => {
|
|
const permissions = [
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [],
|
|
},
|
|
];
|
|
|
|
const scopes = encodePermissionsToScopes([], permissions);
|
|
|
|
// Should not produce any scope for empty actions
|
|
expect(scopes).toHaveLength(0);
|
|
});
|
|
|
|
it('should deduplicate resources across multiple permissions with same action set', () => {
|
|
const permissions = [
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.READ_ANY],
|
|
},
|
|
{
|
|
resource: Resource.VMS,
|
|
actions: [AuthAction.READ_OWN],
|
|
},
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.READ_OWN],
|
|
},
|
|
{
|
|
resource: Resource.VMS,
|
|
actions: [AuthAction.READ_ANY],
|
|
},
|
|
];
|
|
|
|
const scopes = encodePermissionsToScopes([], permissions);
|
|
|
|
// Both DOCKER and VMS have READ_ANY and READ_OWN, should group without duplicates
|
|
expect(scopes).toHaveLength(1);
|
|
expect(scopes[0]).toBe('docker+vms:read_any+read_own');
|
|
});
|
|
|
|
it('should produce deterministic output for same input regardless of order', () => {
|
|
const permissions1 = [
|
|
{ resource: Resource.VMS, actions: [AuthAction.UPDATE_ANY, AuthAction.READ_ANY] },
|
|
{ resource: Resource.DOCKER, actions: [AuthAction.READ_ANY, AuthAction.UPDATE_ANY] },
|
|
];
|
|
|
|
const permissions2 = [
|
|
{ resource: Resource.DOCKER, actions: [AuthAction.UPDATE_ANY, AuthAction.READ_ANY] },
|
|
{ resource: Resource.VMS, actions: [AuthAction.READ_ANY, AuthAction.UPDATE_ANY] },
|
|
];
|
|
|
|
const scopes1 = encodePermissionsToScopes([], permissions1);
|
|
const scopes2 = encodePermissionsToScopes([], permissions2);
|
|
|
|
expect(scopes1).toEqual(scopes2);
|
|
expect(scopes1[0]).toBe('docker+vms:read_any+update_any');
|
|
});
|
|
});
|
|
|
|
describe('roundtrip encoding/decoding', () => {
|
|
it('should maintain permissions through encode/decode cycle with duplicates', () => {
|
|
const originalPermissions = [
|
|
{
|
|
resource: Resource.DOCKER,
|
|
actions: [AuthAction.READ_ANY, AuthAction.READ_OWN, AuthAction.UPDATE_ANY],
|
|
},
|
|
{
|
|
resource: Resource.VMS,
|
|
actions: [AuthAction.READ_OWN, AuthAction.UPDATE_OWN, AuthAction.UPDATE_ANY],
|
|
},
|
|
];
|
|
|
|
const scopes = encodePermissionsToScopes([], originalPermissions);
|
|
const { permissions: decoded } = decodeScopesToPermissions(scopes);
|
|
|
|
// Now possession is preserved in the encoding
|
|
expect(decoded).toHaveLength(2);
|
|
|
|
const dockerPerm = decoded.find((p) => p.resource === Resource.DOCKER);
|
|
const vmsPerm = decoded.find((p) => p.resource === Resource.VMS);
|
|
|
|
// Docker should have its specific actions preserved
|
|
expect(dockerPerm?.actions).toContain(AuthAction.READ_ANY);
|
|
expect(dockerPerm?.actions).toContain(AuthAction.READ_OWN);
|
|
expect(dockerPerm?.actions).toContain(AuthAction.UPDATE_ANY);
|
|
|
|
// VMS should have its specific actions preserved
|
|
expect(vmsPerm?.actions).toContain(AuthAction.READ_OWN);
|
|
expect(vmsPerm?.actions).toContain(AuthAction.UPDATE_OWN);
|
|
expect(vmsPerm?.actions).toContain(AuthAction.UPDATE_ANY);
|
|
|
|
// The scopes should be separate since they have different action sets
|
|
expect(scopes).toHaveLength(2);
|
|
});
|
|
});
|
|
});
|
|
});
|