diff --git a/packages/data/src/policy-provisioning.ts b/packages/data/src/policy-provisioning.ts index 250a580..32da084 100644 --- a/packages/data/src/policy-provisioning.ts +++ b/packages/data/src/policy-provisioning.ts @@ -4,8 +4,12 @@ export const MEMBERSHIP_TYPES = { GROUP: 3, } as const; -export type PolicyFieldType = 'string' | 'boolean' | 'integer' | 'uuid' | 'uuid[]'; -export type PolicyFieldComponent = 'table-select' | 'membership-type-select' | 'capability-select'; +export type PolicyFieldType = 'string' | 'string[]' | 'boolean' | 'integer' | 'uuid' | 'uuid[]'; +export type PolicyFieldComponent = + | 'table-select' + | 'membership-type-select' + | 'capability-select' + | 'level-select'; export type PolicyProvisioningCategory = 'has-module' | 'needs-fields' | 'needs-table' | 'no-fields'; export interface PolicyFieldOverride { @@ -100,11 +104,17 @@ export const POLICY_PROVISIONING_CONFIG: Record = { hidden: true, defaultValue: MEMBERSHIP_TYPES.ORGANIZATION, }, - permission: { - type: 'string', + capabilities: { + type: 'string[]', component: 'capability-select', - label: 'Required Capability', - description: paramOf(NodeTypes.AuthzEntityMembership)?.permission?.description, + label: 'Required Capabilities', + description: paramOf(NodeTypes.AuthzEntityMembership)?.capabilities?.description, + }, + levels: { + type: 'string[]', + component: 'level-select', + label: 'Required Levels', + description: paramOf(NodeTypes.AuthzEntityMembership)?.levels?.description, }, is_admin: { type: 'boolean', @@ -130,11 +136,17 @@ export const POLICY_TYPE_UI_CONFIG: Record = { category: 'no-fields', hasDataNode: false, fieldOverrides: { - permission: { - type: 'string', + capabilities: { + type: 'string[]', component: 'capability-select', - label: 'Required Capability', - description: paramOf(NodeTypes.AuthzAppMembership)?.permission?.description, + label: 'Required Capabilities', + description: paramOf(NodeTypes.AuthzAppMembership)?.capabilities?.description, + }, + levels: { + type: 'string[]', + component: 'level-select', + label: 'Required Levels', + description: paramOf(NodeTypes.AuthzAppMembership)?.levels?.description, }, is_admin: { type: 'boolean', @@ -326,11 +338,17 @@ export const POLICY_TYPE_UI_CONFIG: Record = { description: paramOf(NodeTypes.AuthzRelatedEntityMembership)?.obj_field?.description, dependsOn: 'obj_table', }, - permission: { - type: 'string', + capabilities: { + type: 'string[]', component: 'capability-select', - label: 'Required Capability', - description: paramOf(NodeTypes.AuthzRelatedEntityMembership)?.permission?.description, + label: 'Required Capabilities', + description: paramOf(NodeTypes.AuthzRelatedEntityMembership)?.capabilities?.description, + }, + levels: { + type: 'string[]', + component: 'level-select', + label: 'Required Levels', + description: paramOf(NodeTypes.AuthzRelatedEntityMembership)?.levels?.description, }, is_admin: { type: 'boolean', diff --git a/packages/schema-builder/src/schema/schema-builder-core/components/policies/policy-types.ts b/packages/schema-builder/src/schema/schema-builder-core/components/policies/policy-types.ts index 2670f9c..cf94283 100644 --- a/packages/schema-builder/src/schema/schema-builder-core/components/policies/policy-types.ts +++ b/packages/schema-builder/src/schema/schema-builder-core/components/policies/policy-types.ts @@ -147,12 +147,12 @@ export interface CreateTableWithPoliciesInput { /** * Primitive data type for a field override. Determines the default form component. */ -export type FieldType = 'string' | 'boolean' | 'integer' | 'uuid' | 'uuid[]'; +export type FieldType = 'string' | 'string[]' | 'boolean' | 'integer' | 'uuid' | 'uuid[]'; /** * Specialized form component override. Takes priority over type-based resolution. */ -export type FieldComponent = 'table-select' | 'membership-type-select' | 'capability-select'; +export type FieldComponent = 'table-select' | 'membership-type-select' | 'capability-select' | 'level-select'; /** * Field override for customizing how a policy parameter is displayed in the form. @@ -265,6 +265,7 @@ export type FormFieldType = | 'table-select' | 'membership-type-select' | 'capability-select' + | 'level-select' | 'boolean' | 'number'; diff --git a/packages/schema-builder/src/schema/schema-builder-core/lib/data/policy-provisioning.ts b/packages/schema-builder/src/schema/schema-builder-core/lib/data/policy-provisioning.ts index 285406a..3fee26c 100644 --- a/packages/schema-builder/src/schema/schema-builder-core/lib/data/policy-provisioning.ts +++ b/packages/schema-builder/src/schema/schema-builder-core/lib/data/policy-provisioning.ts @@ -6,8 +6,12 @@ const MEMBERSHIP_TYPES = { GROUP: 3, } as const; -export type PolicyFieldType = 'string' | 'boolean' | 'integer' | 'uuid' | 'uuid[]'; -export type PolicyFieldComponent = 'table-select' | 'membership-type-select' | 'capability-select'; +export type PolicyFieldType = 'string' | 'string[]' | 'boolean' | 'integer' | 'uuid' | 'uuid[]'; +export type PolicyFieldComponent = + | 'table-select' + | 'membership-type-select' + | 'capability-select' + | 'level-select'; export type PolicyProvisioningCategory = 'has-module' | 'needs-fields' | 'needs-table' | 'no-fields'; export interface PolicyFieldOverride { @@ -73,11 +77,17 @@ export const POLICY_PROVISIONING_CONFIG: Record void; disabled?: boolean; formData?: Record; + kind: CapabilityKind; }) { const { data: capabilities, isLoading } = useCapabilities(); const membershipType = formData?.membership_type as number | null | undefined; const isAppLevel = membershipType === MEMBERSHIP_TYPES.APP; const capabilitiesList = isAppLevel ? capabilities?.appCapabilities || [] : capabilities?.membershipCapabilities || []; + const options = useMemo( + () => + capabilitiesList + .filter((capability) => capability.name && capability.kind === kind) + .map((capability) => ({ + value: capability.name, + label: capability.name, + description: capability.description, + })), + [capabilitiesList, kind], + ); + + const noun = kind === 'level' ? 'levels' : 'capabilities'; + const scope = isAppLevel ? 'app' : 'membership'; + return ( - + onChange(next.length > 0 ? next : undefined)} + disabled={disabled} + placeholder={isLoading ? 'Loading...' : `Select ${scope} ${noun}`} + emptyIndicator={`No ${scope} ${noun} defined`} + /> ); } @@ -343,13 +357,15 @@ export function DynamicFormField({ field, value, onChange, disabled, tables, for ); case 'capability-select': + case 'level-select': return ( ); diff --git a/packages/schema-builder/src/schema/schema-builder-policies/components/policies/policy-hooks.ts b/packages/schema-builder/src/schema/schema-builder-policies/components/policies/policy-hooks.ts index d9ed609..10d5212 100644 --- a/packages/schema-builder/src/schema/schema-builder-policies/components/policies/policy-hooks.ts +++ b/packages/schema-builder/src/schema/schema-builder-policies/components/policies/policy-hooks.ts @@ -235,8 +235,8 @@ export function hasRequiredFieldsMissing( for (const field of fields) { // Booleans are always valid (false is a valid value) if (field.type === 'boolean') continue; - // Capability selects are optional - if (field.type === 'capability-select') continue; + // Capability and level selects are optional + if (field.type === 'capability-select' || field.type === 'level-select') continue; const value = data[field.key]; const isEmpty = diff --git a/packages/schema-builder/src/schema/schema-builder-policies/lib/gql/hooks/schema-builder/policies/use-capabilities.ts b/packages/schema-builder/src/schema/schema-builder-policies/lib/gql/hooks/schema-builder/policies/use-capabilities.ts index 75daf37..0bbbc15 100644 --- a/packages/schema-builder/src/schema/schema-builder-policies/lib/gql/hooks/schema-builder/policies/use-capabilities.ts +++ b/packages/schema-builder/src/schema/schema-builder-policies/lib/gql/hooks/schema-builder/policies/use-capabilities.ts @@ -9,11 +9,18 @@ import { useSchemaBuilderRuntime, } from '@/blocks/schema/schema-builder-core/context/block-config'; +/** + * `permission` is an ordinary capability an operator can grant; `level` is compiled + * into the same bitstring by the trust ladder and is not operator-managed. + */ +export type CapabilityKind = 'permission' | 'level'; + export interface CapabilityNode { bitnum: number | null; bitstr: string | null; description: string | null; id: string; + kind: CapabilityKind; name: string; } @@ -28,6 +35,10 @@ export interface UseCapabilitiesOptions { enabled?: boolean; } +function toCapabilityKind(kind: string | null | undefined): CapabilityKind { + return kind === 'level' ? 'level' : 'permission'; +} + export function useCapabilities(options: UseCapabilitiesOptions = {}) { const isEnabled = options.enabled !== false; const { scope } = useSchemaBuilderRuntime(); @@ -38,8 +49,8 @@ export function useCapabilities(options: UseCapabilitiesOptions = {}) { queryFn: async () => { // Fetch both capability types in parallel using SDK fetch functions const [appResult, orgResult] = await Promise.all([ - fetchAppCapabilitiesQuery({ selection: { fields: { id: true, name: true, bitnum: true, bitstr: true, description: true }, orderBy: ['NAME_ASC'] } }), - fetchOrgCapabilitiesQuery({ selection: { fields: { id: true, name: true, bitnum: true, bitstr: true, description: true }, orderBy: ['NAME_ASC'] } }), + fetchAppCapabilitiesQuery({ selection: { fields: { id: true, name: true, bitnum: true, bitstr: true, description: true, kind: true }, orderBy: ['NAME_ASC'] } }), + fetchOrgCapabilitiesQuery({ selection: { fields: { id: true, name: true, bitnum: true, bitstr: true, description: true, kind: true }, orderBy: ['NAME_ASC'] } }), ]); const appCapabilities: AppCapability[] = (appResult.appCapabilities?.nodes ?? []).map((node) => ({ @@ -48,6 +59,7 @@ export function useCapabilities(options: UseCapabilitiesOptions = {}) { bitnum: node.bitnum ?? null, bitstr: node.bitstr ?? null, description: node.description ?? null, + kind: toCapabilityKind(node.kind), })); const membershipCapabilities: MembershipCapability[] = (orgResult.orgCapabilities?.nodes ?? []).map((node) => ({ @@ -56,6 +68,7 @@ export function useCapabilities(options: UseCapabilitiesOptions = {}) { bitnum: node.bitnum ?? null, bitstr: node.bitstr ?? null, description: node.description ?? null, + kind: toCapabilityKind(node.kind), })); return {