- Backwards-compatible
- Forwards-compatible
Summary
This RFC defines the authorization model for ODF nodes. It specifies how accounts, groups, auth attributes, and policies are expressed within the Resource Framework. The data model is designed to be engine-agnostic. A minimal node can implement simple ownership checks, while a full node can plug in a RBAC / ABAC / ReBAC policy engine.Table of Contents
- Motivation
- Proposed Solution
- Compatibility
- Drawbacks
- Rationale and alternatives
- Prior art
- Unresolved questions
- Future possibilities
- Appendix A: Cedar - Example Schema and Policies
- Appendix B: OpenFGA - Example Schema
- Appendix C: Reserved Names
Motivation
ODF nodes host datasets and other resources that belong to different accounts. A flexible, standards-aligned access control model is needed to govern:- Who can view and manage resources under a given account (control plane)
- Who can decrypt secrets and use signing keys
- Who can read or write the data of a dataset (data plane)
- Whether a dataset is public and who can apply labels to make dataset publicly readable
- Which accounts belong to privileged groups (e.g. node administrators)
Proposed Solution
Terminology
- Node - an entire ODF deployment acting under a single identity
- Resource - a control plane manifest that describes the desired state and status of a data plane entity
- Entity - a data plane object that is materialized from a resource (e.g. an ODF dataset, secret, encryption key)
- User - an authenticated account of an individual person
- Organization - an account that owns resources on behalf of a group of users but can’t authenticate or act itself, so its resources are always accessed through its members
- Service account - a non-human identity used by automation (scheduled flows, CI, installed applications); it can act as a principal but doesn’t own resources
- Account - a general term for a named identity (node, user, organization, service account)
- Anonymous - the principal of an unauthenticated request, which has no identity and owns nothing
- Group - a named, possibly nested set of principals that policies can be bound to, so that permissions are granted to all members at once
- Principal - the identity performing an action in an authorization request, established through authentication (a user, service account, node, or anonymous)
- Subject - the party a grant is given to in a policy binding or relation: a principal or a group
- Action - an operation the principal attempts to perform, identified by a schema URI (e.g.
View,Read,Decrypt) - Object - the target of an action: a resource, an entity, or a resource type (
TypeUri) for actions likeCreateandListwhere no instance exists yet
Auth Maturity Levels
We loosely define several authorization maturity levels that ODF implementations may choose to support:Accounts
ODF recognizes the following kinds of accounts. Node — an ODF node:Group:acme/admins— organization administrators, initially containing the principal that created the organization. Members of this group have the same rights over resources owned by the organization as an owner wouldGroup:acme/members— organization members
Note: To avoid any ambiguity or impersonation, node implementations should ensure that names of nodes, users, and organizations are unique regardless of resource type, making it possible to refer to any account only by its name. Service account names don’t follow this rule as they are always scoped under the owning account.
Principals
A principal is the identity performing an action in an authorization request. It is established through authentication and is always a single concrete identity.Organizationcannot authenticate - its resources are always accessed through its members.Anonymousrepresents unauthenticated requests. It is not a resource and carries no identity.Nodeacts as a principal in system tasks and node-to-node communications. When a node executes work that belongs to a user or service account (e.g. a scheduled flow run) it should do so as that principal rather than as itself.
PrincipalRef, e.g. to specify the service account a flow runs as, or to record the initiator of an operation:
Subjects
A subject is the party a grant is given to in aPolicyBinding. Unlike a principal, a subject may stand for many principals at once.
Subjects are referenced using SubjectRef, which is a union of:
Short forms expand as follows:
Authenticated, Anonymous, and Everyone can be used in policy bindings, but not as group members - otherwise nesting such a subject into a group would extend the group’s permissions to all principals.
Groups
AGroup resource represents a named set of subjects. Membership is declared inline in the spec. Groups support nesting — a group may include other groups as members.
Node account like Group:node/admins can be used to represent node-wide predefined roles.
Attributes
Some properties of a resource directly affect access control — for example, whether a dataset allows public reads. We expresses these properties using the existing labels mechanism. A label schema that declareslabelProperties.isAuthAttribute: true signals that any resource carrying that label should have its value materialized into an auth attribute.
Example auth attribute label:
- Single authoring surface — attributes live on the resource they describe, eliminating cross-ownership ambiguity
- Typed and validated — the label schema’s
typefield is enforced at apply time - Indexed — because auth attributes are labels, they are also queryable in the resource listing API
- Schema-discoverable — implementations enumerate label schemas with
isAuthAttribute: trueat startup to know which labels to materialize, without hardcoding a list - Auth attributes must be labels, not annotations — validators reject a resource that places an
isAuthAttributelabel underannotations - Admission control — changing a label with
isAuthAttribute: truemay require elevated permissions beyond those needed to update the rest of the resource spec.
In resource manifest:
AllowPublicRead: true may require special permissions to set, as not every maintainer of the dataset should have the power to make data public. Per-label access control is discussed in AddLabel action section.
Auth attributes are propagated from resources to entities they define, e.g. an AllowPublicRead attribute on the dataset resource (control plane) will also appear as attribute on ODF dataset entity (data plane).
Note: The effects ofAllowPublicReadandAllowAnonymousReadcan be achieved by defining policies forAuthenticatedorAnonymoussubjects. Attributes, however, remain the primary way to make datasets public, as they are atomic with the resource manifest and can be efficiently filtered on when listing resources. Bindings to these subjects are better suited for granting other actions publicly, e.g. making a resource manifest viewable by everyone. Since theBindPolicyaction receives thesubjectin its context, policies can restrict who is allowed to create such bindings.
Actions
Authorization decisions are made against a set of well-defined actions. Like label schemas, each action is defined by a JSON schema using theAction metaschema. The schema URI is the canonical action identifier used in authorization requests and policies, for example https://opendatafabric.org/schemas/resources/v1alpha1/actions/View. The schema itself describes the context values needed to authorize the action (e.g. the label being added for AddLabel), while actionProperties.objectTypes lists the types of objects the action applies to. Every action lists the object types it applies to.
Below is a set of initial actions ODF will specify.
Resource actions — operate on resource instances. Exceptions are Create and List, which operate on a resource type (TypeUri) and supply the account under which resources are created or listed in the request context (targetOwner):
Dataset actions — operate on dataset instances in the data plane, separate from dataset resources:
Config actions — operate on configuration entities:
AddLabel action
AddLabel is an example action that operates on the sub-resource level. It authorizes applying a specific label to a resource. It is distinct from edit to allow fine-grained control. A user may have edit permission but be restricted from changing auth-sensitive labels like AllowPublicRead.
An example of Cedar policy using this action may look like this:
Policies and Policy Bindings
Beyond resource ownership,Policy and PolicyBinding resources are the primary mechanism for granting permissions.
A Policy resource is a reusable, potentially parameterized grant template. It declares what actions are permitted but may leave subject and object as slots to be filled in later.
A PolicyBinding instantiates a Policy by supplying the concrete subject and object. This separation mirrors the Kubernetes Role/RoleBinding pattern: a node admin or org admin defines policy templates once; a resource owner creates bindings to apply them to specific resources.
Engine-specific policies that don’t declare subject and object slots are not templates — they take effect on their own without a binding (e.g. a node-wide policy granting all actions to members of Group:node/admins).
Creating a PolicyBinding is itself an authorized operation — the principal creating it must hold the BindPolicy action on the referenced object. Resource owners implicitly hold BindPolicy on their own resources and can grant it to others, enabling delegated access management without transferring ownership.
A Policy resource’s kind field selects the policy language. ODF spec only defines the Generic policy type — an engine-agnostic way to express grants directly in terms of ODF action URIs. Additional kinds like Cedar and Rego may be added by implementations.
Generic policies
AGeneric policy grants a set of ODF actions on a resource or resource type. It is the portable building block — any node that implements Level 2+ can evaluate it without a specialized policy engine:
object refers to both the resource and the entity materialized from it, so the policy above grants View on the dataset resource and Read on the dataset entity.
Higher-level features like dataset roles (Reader, Editor, Maintainer) are not part of the ODF spec — implementations can build them using groups, Generic policy templates, and PolicyBinding resources.
Engine-specific policies
Nodes with a full policy engine may support richerPolicy kinds that can express attribute-based conditions, org-scoped grants, and custom logic that Generic cannot.
Here is an example of a custom Cedar policy template of acme organization:
?principal and ?resource slots above are filled in by the subject and object of the PolicyBinding:
Policy kind MUST reject the resource with a clear error rather than silently ignoring it.
Compatibility
This RFC is backwards-compatible. No existing metadata event schemas are changed. All new resource types are additive, so existing resources remain valid. Nodes that do not implement auth enforcement will reject the new resources as unknown types.Drawbacks
-
Policybodies for engine-specific kinds are not portable — a Cedar policy means nothing to an OpenFGA node. Interoperability at the policy layer is not a goal. Interoperability at the data layer (accounts, groups,Genericpolicies) is. -
Inline group membership (vs. separate
Memberrelation triples) creates write contention if many members are being added concurrently. Acceptable at ODF node scale - will revisit if needed.
Rationale and alternatives
Why a separateServiceAccount type instead of a User with a flag?
Service accounts differ from users in ways that matter for authorization: they are scoped under an owning account, cannot own resources, and have no email or interactive login. A separate type lets policies distinguish them explicitly (e.g. principal is ServiceAccount) and keeps user-only fields out of their manifests. Installing an application creates a service account under the installing account, so applications don’t need a principal type of their own.
Why are access token scopes not part of the authorization model?
A token may be restricted to a set of scopes (e.g. read-only access to specific datasets), but scopes can only narrow what the principal is allowed to do. Checking whether an action on an object falls within the token’s scopes is a pure function of the token, action, and object that needs no policies or hierarchies, so implementations perform it before policy evaluation. Policies then decide based on the principal’s own permissions, so a token can never grant more than its owner has.
Why are organizations part of the spec while teams and roles are not?
Teams and roles can be fully expressed with groups, Generic policy templates, and bindings, so implementations can build them on top of the spec. Organizations can’t: they are a type of resource owner that cannot act on its own, and granting their admins owner rights over all organization resources is not expressible with single-object Generic bindings. Since ownership is part of every resource header, owner types must also be consistent between implementations for manifests to be portable.
Why inline spec.members on Group instead of separate Member relations?
Simpler to reason about, easier to diff in git, maps directly to auth engine entity parents. The Member relation approach carries Zanzibar-scale complexity that ODF nodes don’t need. A group’s membership is a property of the group, not a separate lifecycle object.
Why no RoleAssignment resource?
Named dataset roles (Reader, Editor, Maintainer) are a useful UX abstraction but are not universal — different implementations may define different roles for different resource types. Rather than standardizing a role vocabulary in the ODF spec, implementations like Kamu build role UX on top of Group, Generic policy templates, and PolicyBinding resources. The spec stays minimal; the implementation provides the ergonomics.
Why a Generic policy kind instead of just engine-specific kinds?
Engine-specific policy bodies (Cedar, Rego) are not portable across implementations. Generic policies — expressed directly in terms of ODF actions — can be evaluated by any Level 2+ node without a specialized engine, preserving interoperability for the common cases (grant action X to principal Y on resource Z).
Why Policy/PolicyBinding instead of a flat ACL?
Policies and bindings have different lifecycles and different authors. A system admin writes Policy templates once; a data owner creates PolicyBinding resources to instantiate them per resource. Separating them mirrors the Cedar template/link model and the Kubernetes Role/RoleBinding pattern.
Why engine-agnostic instead of mandating Cedar?
ODF is a specification, not an implementation. Mandating a specific policy engine would exclude implementations that use Rego, custom evaluators, or pure ReBAC engines. The layered model lets each implementation choose what fits its stack while remaining interoperable at the data level.
Prior art
- Google Zanzibar — the canonical ReBAC system.
- OpenFGA — open-source ReBAC engine implementing the Zanzibar model.
- Cedar — open-source policy engine by AWS; used by the reference implementation for engine-specific policies.
- Open Policy Agent (OPA) / Rego — general-purpose policy engine and its declarative language; an alternative engine for nodes that prefer Rego’s flexibility over Cedar’s formal verification guarantees.
- Kubernetes RBAC —
Role+RoleBindingpattern; directly analogous toPolicy+PolicyBinding. - GitHub repository permissions — teams (groups) + per-repo role assignments; the UX model ODF targets.
- Other projects: Auth0 FGA, Topaz, StrongDM, SpiceDB, Amazon Verified Permissions
Unresolved questions
- Interplay between this model and decentralized capabilities-based auth (UCAN).
- How should the admission layer enforce that
headers.accounton aPolicyBindingresource matches the owner of all referenced objects — reject at admission or flag as a validation error in status? - Who can author
Policyresources? Initially only node admins should be allowed to create policies, as an engine-specific policy (e.g.Cedar) can grant arbitrary permissions across the node. Delegating policy authoring to e.g. organization admins will be addressed in future RFCs.
Future possibilities
- Cross-node relations: A subject on one ODF node granting access to an object on another, using DIDs for stable cross-node identity.
- Audit log: Surface all auth resource changes as events in the event sourcing store for compliance and forensics.
- Transfer ownership: A dedicated action and policy mechanism for transferring resource ownership between accounts.
- Bulk bindings: Allowing
PolicyBindingobjects to target a resource type within an account (e.g. allDatasets ofacme) so that organization admins could express org-wide grants withGenericpolicies, without resorting to engine-specific policies.
Appendix A: Cedar - Example Schema and Policies
Full prototype is available here. A schema can be code-generated based on types registered in the system allowing policies to be validated against known resource types and attributes:Note: For brevity, actions here use short names (e.g."read"). A generated schema would use full action schema URIs (e.g."https://opendatafabric.org/schemas/datasets/v1alpha1/actions/Read") as in the examples above.
PolicyBinding resources become template-linked policies, with the ?principal slot filled based on the SubjectRef variant:
Appendix B: OpenFGA - Example Schema
Full prototype is available here.Note: This model predates theExample schema:ServiceAccountandNodeprincipals and is kept as an illustration of how the model maps onto a ReBAC engine.
Appendix C: Reserved Names
Names that implementations must reserve and treat consistently. Accounts The following names cannot be used byUser or Organization accounts, to prevent impersonation of the node or its administrators:
Matching is case-insensitive. Implementations may reserve additional names (e.g. their own product name).
Groups
Reserved groups are created together with the account that owns them and cannot be created or deleted independently.
Subject keywords
Subject keywords cannot collide with resource names, as resource references in short form always include a type prefix, a DID, or an ID.