af95c3795c
Phase 1 — Structural overhaul: - Move all source from headlamp-sealed-secrets/ subdirectory to repo root - Delete 23 AI-generated docs, 8 pre-built tarballs, release snapshots dir - Remove all working-directory refs from CI/release workflows - Update install-plugin.sh and typedoc.json paths Phase 2 — Config standardization: - Create .eslintrc.js and .prettierrc.js (standard Headlamp configs) - Remove inline eslintConfig/prettier from package.json (drop jsx-a11y, prettier extends) - Rewrite tsconfig.json (package name extend, add compilerOptions.types) - Create vitest.config.mts and vitest.setup.ts (standard from polaris) - Replace headlamp-plugin CLI scripts with direct tool invocation - Rewrite .gitignore with standard baseline Phase 3 — MCP & Claude settings: - Create .mcp.json with github/kubernetes/flux/playwright servers - Create .claude/settings.local.json - Remove 7 specialized agents, keep 3 meta-orchestration agents Phase 4 — Documentation: - Rewrite CLAUDE.md (remove subdirectory refs, standard format) - Add ArtifactHub badge, Architecture section, standardized install methods to README.md - Create CONTRIBUTING.md and SECURITY.md - Fix pre-existing test bugs in validators.test.ts (isValidNamespace returns boolean, not ValidationResult; error message string mismatches) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
4.9 KiB
TypeScript
166 lines
4.9 KiB
TypeScript
/**
|
|
* RBAC Permission Checking
|
|
*
|
|
* Utilities for checking user permissions for SealedSecrets and related
|
|
* Kubernetes resources using SelfSubjectAccessReview API.
|
|
*/
|
|
|
|
import { AsyncResult, Err, Ok, tryCatchAsync } from '../types';
|
|
|
|
/**
|
|
* Resource permissions for a specific resource type
|
|
*/
|
|
export interface ResourcePermissions {
|
|
/** Can create new resources */
|
|
canCreate: boolean;
|
|
/** Can read/get individual resources */
|
|
canRead: boolean;
|
|
/** Can update/patch existing resources */
|
|
canUpdate: boolean;
|
|
/** Can delete resources */
|
|
canDelete: boolean;
|
|
/** Can list resources */
|
|
canList: boolean;
|
|
}
|
|
|
|
/**
|
|
* Check user permissions for SealedSecrets in a namespace
|
|
*
|
|
* Uses Kubernetes SelfSubjectAccessReview API to verify what the current
|
|
* user is allowed to do with SealedSecret resources.
|
|
*
|
|
* @param namespace Optional namespace to check (cluster-wide if omitted)
|
|
* @returns Result containing permission flags or error message
|
|
*/
|
|
export async function checkSealedSecretPermissions(
|
|
namespace?: string
|
|
): AsyncResult<ResourcePermissions, string> {
|
|
try {
|
|
const [canCreate, canRead, canUpdate, canDelete, canList] = await Promise.all([
|
|
checkPermission('create', 'sealedsecrets', 'bitnami.com', namespace),
|
|
checkPermission('get', 'sealedsecrets', 'bitnami.com', namespace),
|
|
checkPermission('update', 'sealedsecrets', 'bitnami.com', namespace),
|
|
checkPermission('delete', 'sealedsecrets', 'bitnami.com', namespace),
|
|
checkPermission('list', 'sealedsecrets', 'bitnami.com', namespace),
|
|
]);
|
|
|
|
return Ok({
|
|
canCreate,
|
|
canRead,
|
|
canUpdate,
|
|
canDelete,
|
|
canList,
|
|
});
|
|
} catch (error: any) {
|
|
return Err(`Failed to check SealedSecret permissions: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user can decrypt secrets (requires get permission on Secrets)
|
|
*
|
|
* @param namespace Namespace to check Secret permissions in
|
|
* @returns true if user has permission to get Secrets
|
|
*/
|
|
export async function canDecryptSecrets(namespace: string): Promise<boolean> {
|
|
try {
|
|
return await checkPermission('get', 'secrets', '', namespace);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user can view sealing keys (requires get permission on Secrets in controller namespace)
|
|
*
|
|
* @param controllerNamespace Namespace where sealed-secrets controller is running
|
|
* @returns true if user has permission to get Secrets in controller namespace
|
|
*/
|
|
export async function canViewSealingKeys(controllerNamespace: string): Promise<boolean> {
|
|
try {
|
|
return await checkPermission('get', 'secrets', '', controllerNamespace);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check a specific permission using SelfSubjectAccessReview
|
|
*
|
|
* @param verb Kubernetes verb (create, get, update, delete, list, etc.)
|
|
* @param resource Resource type (sealedsecrets, secrets, etc.)
|
|
* @param group API group (bitnami.com for SealedSecrets, empty for core resources)
|
|
* @param namespace Optional namespace (cluster-wide if omitted)
|
|
* @returns true if user has permission, false otherwise
|
|
*/
|
|
async function checkPermission(
|
|
verb: string,
|
|
resource: string,
|
|
group: string,
|
|
namespace?: string
|
|
): Promise<boolean> {
|
|
const result = await tryCatchAsync(async () => {
|
|
const reviewRequest = {
|
|
apiVersion: 'authorization.k8s.io/v1',
|
|
kind: 'SelfSubjectAccessReview',
|
|
spec: {
|
|
resourceAttributes: {
|
|
...(group && { group }),
|
|
resource,
|
|
verb,
|
|
...(namespace && { namespace }),
|
|
},
|
|
},
|
|
};
|
|
|
|
const response = await fetch('/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(reviewRequest),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`RBAC check failed: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
return result.status?.allowed === true;
|
|
});
|
|
|
|
// Return false on error (assume no permission)
|
|
return result.ok ? result.value : false;
|
|
}
|
|
|
|
/**
|
|
* Check permissions for multiple namespaces
|
|
*
|
|
* Useful for multi-namespace views to determine which namespaces the user
|
|
* can interact with.
|
|
*
|
|
* @param namespaces Array of namespace names to check
|
|
* @returns Map of namespace to permissions
|
|
*/
|
|
export async function checkMultiNamespacePermissions(
|
|
namespaces: string[]
|
|
): AsyncResult<Record<string, ResourcePermissions>, string> {
|
|
try {
|
|
const results = await Promise.all(
|
|
namespaces.map(async ns => {
|
|
const perms = await checkSealedSecretPermissions(ns);
|
|
return { namespace: ns, permissions: perms };
|
|
})
|
|
);
|
|
|
|
const permissionsMap: Record<string, ResourcePermissions> = {};
|
|
for (const { namespace, permissions } of results) {
|
|
if (permissions.ok) {
|
|
permissionsMap[namespace] = permissions.value;
|
|
}
|
|
}
|
|
|
|
return Ok(permissionsMap);
|
|
} catch (error: any) {
|
|
return Err(`Failed to check multi-namespace permissions: ${error.message}`);
|
|
}
|
|
}
|