mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-17 14:08:19 +00:00
a1e2846d7d
* feat(rbac): make Settings authorization permission-aware Align Settings visibility and mutations with the existing permission matrix so Node Admin can edit node-scoped operational settings while system and credential surfaces stay Admin-protected. * fix(rbac): tighten settings permission buckets and tests Collapse settings key permission maps into one source of truth, and cover mixed PATCH atomicity plus image-update enabled writes. * fix(rbac): tighten Settings scoped grants and CI assertions Empty settings PATCH fails closed, node:manage is scoped to the active node, system-only Settings stay hidden without system:settings, and Check updates / webhooks mutate gates follow the permission matrix. * fix(rbac): defer Settings section fallback until authz is ready Keep deep links to permission-gated sections (e.g. license) intact while can() is still fail-closed during permission metadata load. * feat: expose Community audit log via system:audit navigation Gate the Audit view on the system:audit permission instead of paid tier, so Community admins can open the existing 14-day recent-activity window. Export, anomaly flags, and stats remain Admiral-only. * test: clarify synthetic Community admin mock lacks system:audit Document that mockCommunityAdmin is a gate-isolation helper, not the real Admin permission matrix where system:audit is always present.
93 lines
3.5 KiB
TypeScript
93 lines
3.5 KiB
TypeScript
import type { FleetTab } from '@/lib/events';
|
|
import type { SectionId } from '@/components/settings/types';
|
|
import { getSettingsItem, isItemVisible, isItemLocked } from '@/components/settings/registry';
|
|
import type { ActiveView } from '@/lib/router/routeTypes';
|
|
import { HUB_ONLY_VIEWS } from '@/lib/router/routeTypes';
|
|
|
|
export type ReadinessStatus = 'loading' | 'ready' | 'error';
|
|
|
|
export interface ReachabilityContext {
|
|
isAdmin: boolean;
|
|
isPaid: boolean;
|
|
can: (action: string) => boolean;
|
|
isRemote: boolean;
|
|
hasFleetCapability: boolean;
|
|
containerLabelsEnabled: boolean;
|
|
permissionsStatus: ReadinessStatus;
|
|
licenseStatus: ReadinessStatus;
|
|
/** Gateway SENCHO_EXPERIMENTAL discovery flag. */
|
|
experimental: boolean;
|
|
/** True once /meta experimental has settled (success or fail-closed). */
|
|
experimentalReady: boolean;
|
|
}
|
|
|
|
/** RBAC/tier gates apply only when permission and license metadata are ready. */
|
|
export function authzReady(ctx: ReachabilityContext): boolean {
|
|
return ctx.permissionsStatus === 'ready' && ctx.licenseStatus === 'ready';
|
|
}
|
|
|
|
/**
|
|
* Experimental discovery gates apply only after /meta settles. Before that,
|
|
* treat surfaces as not-yet-hidden so URL sync does not rewrite enabled
|
|
* deep links during cold load.
|
|
*/
|
|
export function experimentalDiscoveryReady(ctx: ReachabilityContext): boolean {
|
|
return ctx.experimentalReady;
|
|
}
|
|
|
|
/** Role/tier hidden views normalize away only when permission and license metadata are ready. */
|
|
export function isViewHidden(view: ActiveView, ctx: ReachabilityContext): boolean {
|
|
if (!authzReady(ctx)) return false;
|
|
if (ctx.isRemote && HUB_ONLY_VIEWS.has(view)) return true;
|
|
if (
|
|
!ctx.isAdmin &&
|
|
(view === 'global-observability' || view === 'auto-updates' || view === 'scheduled-ops')
|
|
) {
|
|
return true;
|
|
}
|
|
if (!ctx.can('node:read') && (view === 'fleet' || view === 'networking')) return true;
|
|
if (view === 'host-console') return !ctx.can('system:console');
|
|
// Permission-driven on Community and Admiral (14-day window vs paid depth is in-view).
|
|
if (view === 'audit-log') return !ctx.can('system:audit');
|
|
return false;
|
|
}
|
|
|
|
/** Capability-locked views stay reachable but render a lock card. */
|
|
export function isViewCapabilityLocked(view: ActiveView, ctx: ReachabilityContext): boolean {
|
|
if (!authzReady(ctx)) return false;
|
|
if (view === 'fleet') return !ctx.hasFleetCapability;
|
|
return false;
|
|
}
|
|
|
|
export function isFleetTabHidden(tab: FleetTab, ctx: ReachabilityContext): boolean {
|
|
if (!authzReady(ctx)) return false;
|
|
if (tab === 'container-labels' && !ctx.containerLabelsEnabled) return true;
|
|
// Defer experimental hide until ready so deep links survive cold load.
|
|
if ((tab === 'routing' || tab === 'secrets') && experimentalDiscoveryReady(ctx) && !ctx.experimental) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function isSettingsSectionHidden(section: SectionId, ctx: ReachabilityContext): boolean {
|
|
if (!authzReady(ctx)) return false;
|
|
const item = getSettingsItem(section);
|
|
if (!item) return true;
|
|
const visibility = {
|
|
isRemote: ctx.isRemote,
|
|
isAdmin: ctx.isAdmin,
|
|
isPaid: ctx.isPaid,
|
|
can: ctx.can,
|
|
};
|
|
if (!isItemVisible(item, visibility)) return true;
|
|
if (isItemLocked(item, visibility)) return true;
|
|
// fleet-mesh stays reachable: snapshot_documentation lives there even when
|
|
// Mesh discovery is off.
|
|
return false;
|
|
}
|
|
|
|
/** Normalize a hidden view to dashboard on the active node. */
|
|
export function normalizeHiddenView(view: ActiveView, ctx: ReachabilityContext): ActiveView {
|
|
return isViewHidden(view, ctx) ? 'dashboard' : view;
|
|
}
|