Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions packages/data/src/policy-provisioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -100,11 +104,17 @@ export const POLICY_PROVISIONING_CONFIG: Record<string, PolicyProvisioningConfig
hidden: true,
defaultValue: MEMBERSHIP_TYPES.ORGANIZATION,
},
permission: {
type: 'string',
capabilities: {
type: 'string[]',
component: 'capability-select',
label: 'Required Capability',
description: 'Optional capability the user must have',
label: 'Required Capabilities',
description: 'Capabilities the user must have (merged into a single mask)',
},
levels: {
type: 'string[]',
component: 'level-select',
label: 'Required Levels',
description: 'Trust-ladder level names to require (merged into the same mask)',
},
is_admin: {
type: 'boolean',
Expand Down Expand Up @@ -136,11 +146,17 @@ export const POLICY_PROVISIONING_CONFIG: Record<string, PolicyProvisioningConfig
category: 'no-fields',
hasDataNode: false,
fieldOverrides: {
permission: {
type: 'string',
capabilities: {
type: 'string[]',
component: 'capability-select',
label: 'Required Capability',
description: 'Optional capability the user must have',
label: 'Required Capabilities',
description: 'Capabilities the user must have (merged into a single mask)',
},
levels: {
type: 'string[]',
component: 'level-select',
label: 'Required Levels',
description: 'Trust-ladder level names to require (merged into the same mask)',
},
is_admin: {
type: 'boolean',
Expand Down Expand Up @@ -303,10 +319,17 @@ export const POLICY_PROVISIONING_CONFIG: Record<string, PolicyProvisioningConfig
description: 'Field containing entity ID for membership check',
dependsOn: 'obj_table',
},
permission: {
type: 'string',
capabilities: {
type: 'string[]',
component: 'capability-select',
label: 'Required Capability',
label: 'Required Capabilities',
description: 'Capabilities the user must have (merged into a single mask)',
},
levels: {
type: 'string[]',
component: 'level-select',
label: 'Required Levels',
description: 'Trust-ladder level names to require (merged into the same mask)',
},
is_admin: {
type: 'boolean',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,17 @@ export const POLICY_TYPE_UI_CONFIG: Record<string, PolicyTypeUIConfig> = {
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',
Expand Down Expand Up @@ -130,11 +136,17 @@ export const POLICY_TYPE_UI_CONFIG: Record<string, PolicyTypeUIConfig> = {
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',
Expand Down Expand Up @@ -326,11 +338,17 @@ export const POLICY_TYPE_UI_CONFIG: Record<string, PolicyTypeUIConfig> = {
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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -265,6 +265,7 @@ export type FormFieldType =
| 'table-select'
| 'membership-type-select'
| 'capability-select'
| 'level-select'
| 'boolean'
| 'number';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -73,11 +77,17 @@ export const POLICY_PROVISIONING_CONFIG: Record<string, PolicyProvisioningConfig
hidden: true,
defaultValue: MEMBERSHIP_TYPES.ORGANIZATION,
},
permission: {
type: 'string',
capabilities: {
type: 'string[]',
component: 'capability-select',
label: 'Required Capability',
description: 'Optional capability the user must have',
label: 'Required Capabilities',
description: 'Capabilities the user must have (merged into a single mask)',
},
levels: {
type: 'string[]',
component: 'level-select',
label: 'Required Levels',
description: 'Trust-ladder level names to require (merged into the same mask)',
},
is_admin: {
type: 'boolean',
Expand Down Expand Up @@ -109,11 +119,17 @@ export const POLICY_PROVISIONING_CONFIG: Record<string, PolicyProvisioningConfig
category: 'no-fields',
hasDataNode: false,
fieldOverrides: {
permission: {
type: 'string',
capabilities: {
type: 'string[]',
component: 'capability-select',
label: 'Required Capability',
description: 'Optional capability the user must have',
label: 'Required Capabilities',
description: 'Capabilities the user must have (merged into a single mask)',
},
levels: {
type: 'string[]',
component: 'level-select',
label: 'Required Levels',
description: 'Trust-ladder level names to require (merged into the same mask)',
},
is_admin: {
type: 'boolean',
Expand Down Expand Up @@ -276,10 +292,17 @@ export const POLICY_PROVISIONING_CONFIG: Record<string, PolicyProvisioningConfig
description: 'Field containing entity ID for membership check',
dependsOn: 'obj_table',
},
permission: {
type: 'string',
capabilities: {
type: 'string[]',
component: 'capability-select',
label: 'Required Capability',
label: 'Required Capabilities',
description: 'Capabilities the user must have (merged into a single mask)',
},
levels: {
type: 'string[]',
component: 'level-select',
label: 'Required Levels',
description: 'Trust-ladder level names to require (merged into the same mask)',
},
is_admin: {
type: 'boolean',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import {
SelectTrigger,
SelectValue,
} from '@constructive-io/ui/select';
import { MultiSelect } from '@constructive-io/ui/multi-select';
import { Switch } from '@constructive-io/ui/switch';
import { Info } from 'lucide-react';

import { MEMBERSHIP_TYPES } from '@/blocks/schema/schema-builder-core/lib/constants/membership-types';
import type { PolicyTableData } from '@/blocks/schema/schema-builder-core/lib/gql/hooks/schema-builder/policies/use-database-policies';
import type { CapabilityKind } from '../../../lib/gql/hooks/schema-builder/policies/use-capabilities';
import { useCapabilities } from '../../../lib/gql/hooks/schema-builder/policies/use-capabilities';

import { MultiValueFieldEditor } from '../multi-value-field-editor';
Expand Down Expand Up @@ -211,46 +213,58 @@ function DependentFieldSelectField({
}

/**
* Capability select dropdown, populated from useCapabilities
* Multi-select over the capability catalog, populated from useCapabilities.
*
* The platform merges every selected name into a single bitmask, so both the
* `capabilities` and `levels` parameters are string arrays. `kind` splits the one
* catalog into the two: trust-ladder levels are not operator-grantable
* capabilities, so gating a policy on one from the capability picker is never
* what an operator means.
*/
function CapabilitySelectField({
field,
value,
onChange,
disabled,
formData,
kind,
}: {
field: FormFieldSchema;
value: string | undefined;
value: string[] | undefined;
onChange: (value: unknown) => void;
disabled?: boolean;
formData?: Record<string, unknown>;
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 (
<Select value={value || ''} onValueChange={(v) => onChange(v || undefined)} disabled={disabled}>
<SelectTrigger>
<SelectValue
placeholder={isLoading ? 'Loading...' : `Select ${isAppLevel ? 'app' : 'membership'} capability`}
/>
</SelectTrigger>
<SelectContent className='max-h-48'>
{capabilitiesList
.filter((p) => p.name)
.map((capability) => (
<SelectRichItem
key={capability.id}
value={capability.name!}
label={capability.name ?? undefined}
description={capability.description}
/>
))}
</SelectContent>
</Select>
<MultiSelect
options={options}
defaultValue={value ?? []}
onValueChange={(next) => onChange(next.length > 0 ? next : undefined)}
disabled={disabled}
placeholder={isLoading ? 'Loading...' : `Select ${scope} ${noun}`}
emptyIndicator={`No ${scope} ${noun} defined`}
/>
);
}

Expand Down Expand Up @@ -343,13 +357,15 @@ export function DynamicFormField({ field, value, onChange, disabled, tables, for
);

case 'capability-select':
case 'level-select':
return (
<CapabilitySelectField
field={field}
value={(value as string) ?? undefined}
value={(value as string[]) ?? undefined}
onChange={onChange}
disabled={disabled}
formData={formData}
kind={field.type === 'level-select' ? 'level' : 'permission'}
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Loading
Loading