Skip to content

Simplify policy import model: replace entriesAdditions and importsAliases with importReference and alias #2423

@thjaeckle

Description

@thjaeckle

Summary

The Ditto 3.9.0 policy import model (not yet released) introduces several new concepts to enable multi-level policy template hierarchies:

  • entriesAdditions on imports — merge additional subjects/resources/namespaces into imported entries
  • allowedImportAdditions on template entries — control what importing policies may add
  • transitiveImports on imports — explicit multi-level resolution (Support transitive resolution of policy imports via transitiveImports #2420)
  • 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 and importsAliases with two new entry-level fields:

importReference on policy entries

An entry can declare an importReference that points to a specific entry in an imported policy. The 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).

alias on policy entries

Multiple entries can share the same alias label. API operations on the alias (e.g., PUT /entries/{alias}/subjects/{subjectId}) fan out to all entries declaring that alias. This replaces importsAliases without introducing a separate top-level concept.

What changes

Removed concept Replaced by
entriesAdditions on imports local subjects/resources/namespaces on entry
importsAliases on policy alias field on entries
entries filter on import importReference on individual entries (always explicit)
importable: "explicit" vs "implicit" every importReference is explicit by nature

What stays

Concept Reason
transitiveImports explicit control over multi-level resolution depth (no surprises)
allowedImportAdditions template author retains control over what consumers may customize
importable: "never" template author can prevent entries from being referenced

Example: 3-level policy hierarchy

A fleet management scenario with three levels:

  • Template defines roles with resources (what a driver can do)
  • Intermediate assigns regional drivers (who is a driver)
  • Leaf is the actual thing's policy (combines both)

Current approach (Ditto 3.9.0 as 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 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.

Leaf (acme.vehicle:truck-42):

{
  "policyId": "acme.vehicle:truck-42",
  "imports": {
    "acme:fleet-west": {
      "entries": ["driver"],
      "entriesAdditions": {
        "driver": {
          "subjects": {
            "oauth2:charlie@acme.com": { "type": "temp-driver" }
          }
        }
      },
      "transitiveImports": ["acme:fleet-roles"]
    }
  },
  "entries": {
    "owner": {
      "subjects": { "oauth2:fleet-admin@acme.com": { "type": "admin" } },
      "resources": { "policy:/": { "grant": ["READ", "WRITE"], "revoke": [] } }
    }
  }
}

The leaf must:

  1. List "entries": ["driver"] to select which entries to import
  2. Define entriesAdditions to add truck-specific subjects
  3. Declare transitiveImports: ["acme:fleet-roles"] so the intermediate's import of the template is resolved

The resolved "driver" entry gets: resources from template + subjects from intermediate (alice, bob) + subjects from leaf (charlie).

Proposed approach (simplified)

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": {
      "importReference": { "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 an importReference 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": {
      "importReference": { "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": [] } }
    }
  }
}

The leaf declares a "driver" entry with importReference to the intermediate. It adds a truck-specific subject (charlie). transitiveImports ensures the intermediate's reference to the template is resolved first.

Same resolved result: resources from template + subjects from intermediate (alice, bob) + subjects from leaf (charlie).

Side-by-side comparison

Current (entriesAdditions) Proposed (importReference)
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 importReference on individual entries
Fan-out (multi-entry) separate importsAliases top-level concept alias field on entries
Subject API path PUT /entries/{alias}/subjects/... (via importsAliases) PUT /entries/{alias}/subjects/... (via alias field)
Concepts to learn entriesAdditions, allowedImportAdditions, importsAliases, entries filter, importable importReference, alias, allowedImportAdditions, importable: "never"

Multi-namespace fan-out example

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 alias for fan-out:

{
  "imports": { "energy:plant-roles": {} },
  "entries": {
    "reactor-op": {
      "importReference": { "import": "energy:plant-roles", "entry": "reactor-operator" },
      "alias": "operator"
    },
    "turbine-op": {
      "importReference": { "import": "energy:plant-roles", "entry": "turbine-operator" },
      "alias": "operator"
    }
  }
}

PUT /entries/operator/subjects/alice adds alice to both entries — each retaining its own namespace scope. No importsAliases needed.

Resolution semantics

  • importReference inherits resources, namespaces, allowedImportAdditions, and importable 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 importReference chains are resolved (explicit opt-in, no surprises)
  • Cycle detection via visited set + depth limit (same as current implementation)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions