feat(module-grants): Proxmox, Handelsware und DKV mit Freigabestufe Verwalten
- Proxmox-Schreibwege und Handelsware-Einstellungen auf @ModuleManage umgestellt - DKV-Fleet: ganze Klasse Verwalten-Stufe, Benutzen allein bleibt ohne Zugriff - Metadaten-Test belegt umgestellte und bewusst Administratoren vorbehaltene Handler - Webseiten (Proxmox, Handelsware, Widget) folgen canManage, DKV-Zugriffsseite erklärt die Stufe Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import { updateWidgetConfig } from '@/lib/dashboard-api';
|
||||
// Die manuelle Abfrage (POST .../poll) gehoert der Modulseite und darf hier
|
||||
// nie auftauchen — sonst loeste jede Kachel je Benutzer Live-Abfragen aus.
|
||||
import { listServers, type ProxmoxServer } from '@/lib/proxmox-api';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
import { useCanManageModule } from '@/lib/use-module-capability';
|
||||
import {
|
||||
healthSummary,
|
||||
type KeyFigure,
|
||||
@@ -71,8 +71,8 @@ export function ProxmoxWidget({ instanceId, config, isEditMode }: WidgetProps) {
|
||||
const t = useTranslations('widgets');
|
||||
const tp = useTranslations('proxmox');
|
||||
const locale = useLocale();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SUPER_ADMIN';
|
||||
// Einstellungs-Link auch fuer Benutzer mit der Freigabestufe Verwalten (261002-icv).
|
||||
const canManage = useCanManageModule('proxmox') === true;
|
||||
|
||||
// Lokaler Zustand aus `config` initialisiert (wie `viewMode` bei den
|
||||
// Favoriten); Aenderungen gehen per updateWidgetConfig an den Server.
|
||||
@@ -244,7 +244,7 @@ export function ProxmoxWidget({ instanceId, config, isEditMode }: WidgetProps) {
|
||||
|
||||
if (servers.length === 0) {
|
||||
const settingsHint =
|
||||
isAdmin &&
|
||||
canManage &&
|
||||
(isEditMode ? (
|
||||
<span className="text-sm font-medium text-foreground">{tp('card.settingsLink')}</span>
|
||||
) : (
|
||||
|
||||
@@ -25,6 +25,8 @@ vi.mock('next-intl/server', () => ({
|
||||
const translations: Record<string, string> = {
|
||||
'accessDenied.title': 'Kein Zugriff auf dieses Modul',
|
||||
'accessDenied.body': 'Sie haben für dieses Modul keine Freigabe. Wenden Sie sich an Ihren Administrator.',
|
||||
'accessDenied.manageRequiredBody':
|
||||
'Dieses Modul steht nur Benutzern zur Verfügung, die es verwalten dürfen. Wenden Sie sich an Ihren Administrator.',
|
||||
'accessDenied.backToDashboard': 'Zur Startseite',
|
||||
};
|
||||
return translations[key] ?? key;
|
||||
@@ -102,4 +104,51 @@ describe('ModuleAccessGate — server access gate (D-07, PERM-04)', () => {
|
||||
expect(checkModuleAccess).toHaveBeenCalledTimes(1);
|
||||
expect(checkModuleAccess).toHaveBeenCalledWith('tender-radar');
|
||||
});
|
||||
|
||||
describe('Verwalten-pflichtige Module (dkv-fleet, 261002-icv)', () => {
|
||||
const renderGate = async (level: 'none' | 'use' | 'manage' | Error) => {
|
||||
const checkModuleAccess = vi.fn();
|
||||
const getModuleAccessLevel =
|
||||
level instanceof Error
|
||||
? vi.fn().mockRejectedValue(level)
|
||||
: vi.fn().mockResolvedValue(level);
|
||||
vi.doMock('@/lib/module-access-actions', () => ({ checkModuleAccess, getModuleAccessLevel }));
|
||||
const { ModuleAccessGate } = await import('./module-access-gate');
|
||||
const element = await ModuleAccessGate({
|
||||
moduleSlug: 'dkv-fleet',
|
||||
children: <div data-testid="module-children">children</div>,
|
||||
});
|
||||
render(element);
|
||||
return { checkModuleAccess, getModuleAccessLevel };
|
||||
};
|
||||
|
||||
it('manage: zeigt die Kinder', async () => {
|
||||
await renderGate('manage');
|
||||
expect(screen.getByTestId('module-children')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('use: zeigt die erklärende Seite mit dem Verwalten-Hinweis, keine Kinder', async () => {
|
||||
await renderGate('use');
|
||||
expect(screen.getByText(/nur Benutzern zur Verfügung, die es verwalten dürfen/)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('module-children')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('none: zeigt den Standardtext', async () => {
|
||||
await renderGate('none');
|
||||
expect(screen.getByText(/keine Freigabe/)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('module-children')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Fehler beim Abruf: Standardtext, keine Kinder (fail closed)', async () => {
|
||||
await renderGate(new Error('network error'));
|
||||
expect(screen.getByText(/keine Freigabe/)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('module-children')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('nutzt getModuleAccessLevel und nicht checkModuleAccess', async () => {
|
||||
const { checkModuleAccess, getModuleAccessLevel } = await renderGate('manage');
|
||||
expect(getModuleAccessLevel).toHaveBeenCalledWith('dkv-fleet');
|
||||
expect(checkModuleAccess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { checkModuleAccess } from '@/lib/module-access-actions';
|
||||
import { checkModuleAccess, getModuleAccessLevel } from '@/lib/module-access-actions';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ModuleAccessDenied } from './module-access-denied';
|
||||
|
||||
/**
|
||||
* Module, die als Ganzes Verwalten-Stufe verlangen (261002-icv): die API
|
||||
* traegt dort `@ModuleManage` auf der ganzen Klasse (DkvController). Benutzer
|
||||
* mit nur "Benutzen" bekaemen von der API ohnehin 403 — die Seite zeigt ihnen
|
||||
* stattdessen eine erklaerende Zugriffsseite.
|
||||
*/
|
||||
const MANAGE_ONLY_MODULE_SLUGS = new Set(['dkv-fleet']);
|
||||
|
||||
interface ModuleAccessGateProps {
|
||||
moduleSlug: string;
|
||||
children: ReactNode;
|
||||
@@ -29,6 +37,26 @@ interface ModuleAccessGateProps {
|
||||
* missing.
|
||||
*/
|
||||
export async function ModuleAccessGate({ moduleSlug, children }: ModuleAccessGateProps) {
|
||||
if (MANAGE_ONLY_MODULE_SLUGS.has(moduleSlug)) {
|
||||
let level: 'none' | 'use' | 'manage' = 'none';
|
||||
try {
|
||||
level = await getModuleAccessLevel(moduleSlug);
|
||||
} catch {
|
||||
level = 'none';
|
||||
}
|
||||
if (level === 'manage') {
|
||||
return children;
|
||||
}
|
||||
const tm = await getTranslations('modules');
|
||||
return (
|
||||
<ModuleAccessDenied
|
||||
title={tm('accessDenied.title')}
|
||||
body={level === 'use' ? tm('accessDenied.manageRequiredBody') : tm('accessDenied.body')}
|
||||
backToDashboard={tm('accessDenied.backToDashboard')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let hasAccess = false;
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user