You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
importsAliases on the policy — fan out subject operations to multiple entries additions targets
While powerful, this results in a complex model with multiple interacting concepts spread across different parts of the policy. This issue proposes a simplification that achieves the same functionality with fewer, more intuitive building blocks.
Proposal
Replace entriesAdditions, importsAliases, and importReference/alias with a single entry-level references array.
references on policy entries
An entry can declare a references array containing objects that point to other entries. Each reference is either:
Import reference ({"import": "policyId", "entry": "label"}) — references an entry in an imported policy. The referencing entry inherits resources and namespaces from the referenced template entry, while defining its own local subjects (and optionally resources/namespaces if the template permits via allowedImportAdditions).
Local reference ({"entry": "label"}) — references another entry within the same policy. The referencing entry inherits subjects, resources, and namespaces from the referenced local entry. This replaces importsAliases without introducing a separate concept: shared subjects live in a single entry, referenced by others.
An entry can have multiple references (both import and local), and all are resolved additively at enforcement time.
The intermediate has no inline entries — it only defines entriesAdditions on its import. The subjects and the entry they target are nested inside the import declaration. The intermediate's structure is opaque: reading it doesn't reveal what entries it provides.
The intermediate declares an explicit entry "driver" with a references entry pointing to the template. Resources and namespaces are inherited; subjects are local. The policy is self-describing — reading it reveals it has a "driver" entry linked to the template.
PUT /entries/operators/subjects/alice adds alice as a standard single-entry operation. At enforcement time, both reactor-op and turbine-op inherit alice's subject via local reference — each retaining its own namespace scope.
No fan-out logic, no alias concept, no special REST semantics. Standard CRUD on the "operators" entry, with references resolved at enforcement time.
Side-by-side comparison
Old (entriesAdditions)
New (references)
Subject declaration
nested in imports.*.entriesAdditions.*.subjects
directly in entries.*.subjects
Entry visibility
intermediate has no entries (opaque until resolution)
intermediate has explicit entries (self-describing)
Entry selection
entries filter on the import + entriesAdditions keys
entries filter (backward compat) or references on entries
Fan-out (multi-entry)
separate importsAliases top-level concept
local references to a shared-subjects entry
Subject API path
PUT /entries/{alias}/subjects/... (ambiguous REST)
PUT /entries/{shared}/subjects/... (standard CRUD)
Reference multiplicity
importReference is singular
references array supports N references (import + local)
Concepts to learn
entriesAdditions, allowedImportAdditions, importsAliases, entries filter, importable, importReference, alias
Each import reference in references inherits resources, namespaces, allowedImportAdditions, and importable from the referenced entry
Each local reference inherits subjects, resources, and namespaces from the referenced entry
Local subjects (and optionally resources/namespaces if allowedImportAdditions permits) are merged with the inherited values
transitiveImports on the import controls whether the imported policy's own reference chains are resolved (explicit opt-in, no surprises)
Cycle detection via visited set + depth limit (same as current implementation)
Entries with no subjects or no resources after resolution are filtered out from the enforcer (optimization: they contribute nothing to access decisions)
REST API
The references field is exposed at:
GET /api/2/policies/{policyId}/entries/{label}/references — retrieve the references array
PUT /api/2/policies/{policyId}/entries/{label}/references — set the references array
DELETE /api/2/policies/{policyId}/entries/{label}/references — remove all references
No special alias endpoints. No fan-out REST semantics. Standard CRUD.
Summary
The Ditto 3.9.0 policy import model (not yet released) introduces several new concepts to enable multi-level policy template hierarchies:
entriesAdditionson imports — merge additional subjects/resources/namespaces into imported entriesallowedImportAdditionson template entries — control what importing policies may addtransitiveImportson imports — explicit multi-level resolution (Support transitive resolution of policy imports via transitiveImports #2420)importsAliaseson the policy — fan out subject operations to multiple entries additions targetsWhile powerful, this results in a complex model with multiple interacting concepts spread across different parts of the policy. This issue proposes a simplification that achieves the same functionality with fewer, more intuitive building blocks.
Proposal
Replace
entriesAdditions,importsAliases, andimportReference/aliaswith a single entry-levelreferencesarray.referenceson policy entriesAn entry can declare a
referencesarray containing objects that point to other entries. Each reference is either:{"import": "policyId", "entry": "label"}) — references an entry in an imported policy. The referencing entry inherits resources and namespaces from the referenced template entry, while defining its own local subjects (and optionally resources/namespaces if the template permits viaallowedImportAdditions).{"entry": "label"}) — references another entry within the same policy. The referencing entry inherits subjects, resources, and namespaces from the referenced local entry. This replacesimportsAliaseswithout introducing a separate concept: shared subjects live in a single entry, referenced by others.An entry can have multiple references (both import and local), and all are resolved additively at enforcement time.
What changes
entriesAdditionson importssubjects/resources/namespaceson entryallowedImportAdditionsvalidation for aliasesreferencesarrayimportsAliaseson policyreferencespointing to a shared-subjects entryWhat stays
entriesfilter on importreferencesimportable(implicit/never)transitiveImportsallowedImportAdditionsExample: 3-level policy hierarchy
A fleet management scenario with three levels:
Current approach (Ditto 3.9.0 as initially implemented)
Template (
acme:fleet-roles):{ "policyId": "acme:fleet-roles", "entries": { "driver": { "subjects": {}, "resources": { "thing:/features/location": { "grant": ["READ"], "revoke": [] }, "thing:/features/fuel": { "grant": ["READ"], "revoke": [] }, "message:/features/fuel/inbox": { "grant": ["WRITE"], "revoke": [] } }, "namespaces": ["acme.vehicle"], "allowedImportAdditions": ["subjects"], "importable": "implicit" } } }Intermediate (
acme:fleet-west):{ "policyId": "acme:fleet-west", "imports": { "acme:fleet-roles": { "entriesAdditions": { "driver": { "subjects": { "oauth2:alice@acme.com": { "type": "employee" }, "oauth2:bob@acme.com": { "type": "employee" } } } } } }, "entries": {} }The intermediate has no inline entries — it only defines
entriesAdditionson its import. The subjects and the entry they target are nested inside the import declaration. The intermediate's structure is opaque: reading it doesn't reveal what entries it provides.Simplified approach (using
references)Template (
acme:fleet-roles) — unchanged:{ "policyId": "acme:fleet-roles", "entries": { "driver": { "subjects": {}, "resources": { "thing:/features/location": { "grant": ["READ"], "revoke": [] }, "thing:/features/fuel": { "grant": ["READ"], "revoke": [] }, "message:/features/fuel/inbox": { "grant": ["WRITE"], "revoke": [] } }, "namespaces": ["acme.vehicle"], "allowedImportAdditions": ["subjects"], "importable": "implicit" } } }Intermediate (
acme:fleet-west):{ "policyId": "acme:fleet-west", "imports": { "acme:fleet-roles": {} }, "entries": { "driver": { "references": [ { "import": "acme:fleet-roles", "entry": "driver" } ], "subjects": { "oauth2:alice@acme.com": { "type": "employee" }, "oauth2:bob@acme.com": { "type": "employee" } } } } }The intermediate declares an explicit entry "driver" with a
referencesentry pointing to the template. Resources and namespaces are inherited; subjects are local. The policy is self-describing — reading it reveals it has a "driver" entry linked to the template.Leaf (
acme.vehicle:truck-42):{ "policyId": "acme.vehicle:truck-42", "imports": { "acme:fleet-west": { "transitiveImports": ["acme:fleet-roles"] } }, "entries": { "driver": { "references": [ { "import": "acme:fleet-west", "entry": "driver" } ], "subjects": { "oauth2:charlie@acme.com": { "type": "temp-driver" } } }, "owner": { "subjects": { "oauth2:fleet-admin@acme.com": { "type": "admin" } }, "resources": { "policy:/": { "grant": ["READ", "WRITE"], "revoke": [] } } } } }Same resolved result: resources from template + subjects from intermediate (alice, bob) + subjects from leaf (charlie).
Multi-namespace fan-out example (using local references)
A power plant template with entries scoped to different namespaces:
{ "policyId": "energy:plant-roles", "entries": { "reactor-operator": { "subjects": {}, "resources": { "thing:/features/reactor": { "grant": ["READ", "WRITE"], "revoke": [] } }, "namespaces": ["plant.reactor"], "allowedImportAdditions": ["subjects"] }, "turbine-operator": { "subjects": {}, "resources": { "thing:/features/turbine": { "grant": ["READ", "WRITE"], "revoke": [] } }, "namespaces": ["plant.turbine"], "allowedImportAdditions": ["subjects"] } } }Consuming policy with local reference for shared subjects:
{ "imports": { "energy:plant-roles": {} }, "entries": { "operators": { "subjects": { "alice": { "type": "engineer" } } }, "reactor-op": { "references": [ { "import": "energy:plant-roles", "entry": "reactor-operator" }, { "entry": "operators" } ] }, "turbine-op": { "references": [ { "import": "energy:plant-roles", "entry": "turbine-operator" }, { "entry": "operators" } ] } } }PUT /entries/operators/subjects/aliceadds alice as a standard single-entry operation. At enforcement time, both reactor-op and turbine-op inherit alice's subject via local reference — each retaining its own namespace scope.No fan-out logic, no alias concept, no special REST semantics. Standard CRUD on the "operators" entry, with references resolved at enforcement time.
Side-by-side comparison
entriesAdditions)references)imports.*.entriesAdditions.*.subjectsentries.*.subjectsentriesfilter on the import +entriesAdditionskeysentriesfilter (backward compat) orreferenceson entriesimportsAliasestop-level conceptreferencesto a shared-subjects entryPUT /entries/{alias}/subjects/...(ambiguous REST)PUT /entries/{shared}/subjects/...(standard CRUD)importReferenceis singularreferencesarray supports N references (import + local)entriesAdditions,allowedImportAdditions,importsAliases,entriesfilter,importable,importReference,aliasreferences,allowedImportAdditions,entriesfilter,importableResolution semantics
referencesinherits resources, namespaces,allowedImportAdditions, andimportablefrom the referenced entryallowedImportAdditionspermits) are merged with the inherited valuestransitiveImportson the import controls whether the imported policy's own reference chains are resolved (explicit opt-in, no surprises)REST API
The
referencesfield is exposed at:GET /api/2/policies/{policyId}/entries/{label}/references— retrieve the references arrayPUT /api/2/policies/{policyId}/entries/{label}/references— set the references arrayDELETE /api/2/policies/{policyId}/entries/{label}/references— remove all referencesNo special alias endpoints. No fan-out REST semantics. Standard CRUD.