openfga-sync is a daemon which synchronizes identity providers with the
OpenFGA stores used by Incus, Operations Center and Migration Manager for
authorization.
It pulls group and role information from a number of data sources and converts it into OpenFGA relationship tuples, granting users access to the matching deployments, e.g. Incus projects on every cluster where those projects exist.
Multiple sources can be active at the same time, their grants are merged.
The LDAP servers can either be set explicitly through a list of URLs (tried in order) or discovered automatically from the AD DNS SRV records of a domain.
With sync_roles: true, all groups below a configurable base DN are
pulled and their names matched against a list of roles. Each role is a
regular expression pattern carrying a set of grants, applied to every
member of the matching groups, with capture groups from the pattern
available in the relation and object templates.
For example, a role with the pattern ^app-(?P<app>.+)-admin$ and grants
operator on project:app-${app}-stg plus viewer on
project:app-${app}-prod gives every member of the app-1234-admin group
the operator relation on the app-1234-stg project and the viewer
relation on the app-1234-prod project.
With sync_groups: true, the source instead fills in the membership of
the OpenFGA groups that were granted access in the stores by a third
party, keeping them aligned with the matching LDAP groups (same behavior
as the Rauthy version below).
The groups considered can be restricted by name with group_pattern, a
regular expression. Groups not matching are ignored entirely, including
in authoritative mode where their membership is left alone.
To keep the number of queries to a minimum, group members aren't looked up
individually by default: the user name is derived directly from the member
DN (its first RDN value, which for AD is expected to line up with the
sAMAccountName). Setting user_attribute (e.g. mail or
userPrincipalName) switches to resolving each member entry instead, at
the cost of one query per user. Either way the resulting name must match
what Incus sees at authentication time.
Groups nested into other groups are followed, so their members get the
grants of the parent group. Groups located below the base DN are
recognized from the group listing itself at no extra cost. Members
located elsewhere in the directory can only be told apart from users by
looking them up, which happens when user_attribute is set or when
nested_groups: true is set (one query per member either way).
Both paged group retrieval (page_size entries at a time, 1000 by
default) and AD ranged member retrieval (member;range=... on very large
groups) are handled. Debug logging reports every page fetched, the
referrals returned by the server (which aren't followed) and the groups
skipped, to help track down missing entries.
With sync_roles: true, roles matching a configurable pattern are pulled
from Rauthy (0.35 or newer) and their
JSON metadata read for OpenFGA grants. Grants are keyed by application
type and scoped either globally (all deployments of that type) or to
specific deployments (target names):
{"openfga": {
"incus": {
"global": [{"relation": "user", "object": "project:app-1234-stg"}],
"specific": {
"cl001": [{"relation": "admin"}]
}
},
"operations-center": {"global": [{"relation": "viewer"}]},
"migration-manager": {"global": [{"relation": "viewer"}]}
}}Grants without an object default to the server object of their application
type (e.g. server:incus). Every enabled user holding a role gets its
grants applied, using their e-mail address as the OpenFGA user name.
With sync_groups: true, the source synchronizes group membership
instead: each store is scanned for permission tuples granted to groups
(group:NAME#member, typically added by a third party), and the member
tuples of every such OpenFGA group are then kept aligned with the matching
Rauthy group, adding and removing members as they join and leave the group
in Rauthy. The group grants themselves are never touched. As with LDAP,
group_pattern restricts the groups considered by name.
Environments are expected to use one mechanism or the other: either the roles carry the full policy, or the policy is managed externally and only the group membership is filled in from Rauthy.
With sync_roles: true, the active project role assignments
(authorizations) are pulled from Zitadel (v4 or
newer, for its v2 authorization API) and the role keys matched against the
same kind of role pattern list as the LDAP source, applying the grants of
the matching roles to every user holding them. Zitadel roles can't carry
metadata, so the grants live in the openfga-sync configuration rather
than in the identity provider.
The synchronization can be restricted to the roles of a single Zitadel
project with project_id, and user_field selects the value used as the
OpenFGA user name: the user's e-mail address (the default, at the cost of
one query per user), their preferred login name or their user ID (matching
the OIDC sub claim). It must line up with what Incus sees at
authentication time.
The API is accessed with the personal access token of a service user
holding a role that includes the user.grant.read permission.
Zitadel doesn't support user groups in current releases, so there is no
sync_groups mode for this source yet.
Every source ends up with a user name that must match what the
application sees at authentication time. When the identity provider and
the application disagree on the case of the name, user_transforms lists
the transforms applied in order to the user name before it's used in the
tuples: lower and upper.
Similarly, role_transforms applies the same kind of transforms to the
LDAP group name or Zitadel role key before it's matched against the
patterns, e.g. to turn an I12345 group into the i12345 instance name
through a lower transform.
All the applications share a single OpenFGA instance, with one store per application deployment (an Incus server or cluster, an Operations Center or a Migration Manager deployment).
The stores to synchronize are discovered automatically, based on the
TYPE_NAME store naming convention: incus_cl001 is the Incus deployment
named cl001, operations-center_operations-center01 the Operations
Center deployment named operations-center01, and so on. Stores that
don't follow the convention are ignored, and newly created stores get
picked up on the next synchronization pass.
With skip_missing_objects: true, openfga-sync checks which objects
actually exist in each Incus store before writing, based on the object
tuples that Incus itself maintains (e.g. server:incus being the server
of project:NAME). Grants that reference objects which don't exist on a
given cluster are then simply skipped there until a later pass where the
objects showed up, avoiding pointless tuples. The check is done in memory
from the single read of the store's tuples done on each pass. Operations
Center and Migration Manager use flat authorization models, so no such
filtering applies there.
With the check enabled, grant objects can also contain * wildcards,
each matching a single path element (anything but /) of the objects
found in the store. For example instance:*/${1} for the ^i[0-9]+$
pattern grants access to the instance named after the group in whichever
project holds it, expanding to every match when the name exists in
several projects. Wildcards only apply to Incus stores.
By default, a local state directory keeps track of every tuple written by
openfga-sync, one file per store. Only tuples recorded there are ever
deleted, so tuples managed by the applications themselves or added
manually are never touched. The record is checked against the store on
every pass: recorded tuples that went missing (e.g. removed by Incus along
with a deleted project, or by hand) are dropped from it and re-created
when still wanted. In steady state no OpenFGA write traffic is generated
at all, and the state files are only rewritten when something actually
changed.
With authoritative: true, openfga-sync instead owns the user tuples
(user:NAME) within its scope: on every pass, each store's tuples are
read back and anything not matching the configured sources is deleted,
including tuples added by hand (the user:* wildcard used for
authenticated access is left alone). The scope follows the configured
synchronization modes: the permission tuples are owned when a source has
sync_roles enabled, the group memberships (member on group:NAME)
when a source has sync_groups enabled, restricted to the groups
matching its group_pattern if any. Tuples granted to groups
(group:NAME#member) are never owned, so a third party can keep managing
the group grants while openfga-sync is authoritative over the
memberships. No local state is kept in this mode.
The daemon runs a synchronization pass at a configurable interval:
openfga-sync --config /etc/openfga-sync/config.yml
--one-shotruns a single pass and exits.--dry-runonly reports the changes that would be made (ordaemon.dry_run: truein the configuration).--debugenables debug logging (ordaemon.debug: truein the configuration).SIGHUPtriggers a configuration reload.SIGUSR1triggers an immediate synchronization pass.
An annotated example configuration can be found in doc/config.yml.example.
make
make test
make static-analysis
You can file bug reports and feature requests at:
https://github.com/futurfusion/openfga-sync/issues/new
This repository is released under the terms of the Apache 2.0 license.
Fixes and new features are greatly appreciated. Make sure to read our contributing guidelines first!