Identity and Access Management for LXD

Abstract

Identity and Access Management (IAM) is critical to infrastructure services. This is especially pertinent for LXD/microcloud because privileged containers allow root access to the host machine. This specification outlines the current shortfalls of IAM in LXD and discusses the constraints under which LXD may be running that impact an IAM solution. Subsequently, a detailed plan for IAM in LXD is given. The full specification includes an OpenFGA driver built directly into LXD, new API routes for identity, group, and permission management, extraction of group membership from OIDC identity tokens and more. Lastly, some potential issues with the approach are identified and discussed.

Rationale

It is currently possible to authenticate with the LXD HTTPS API via TLS, Candid, or OpenID Connect (OIDC). It is possible to limit permissions when a user authenticates will TLS by restricting their certificate to a set of projects. Additionally, limits can be set on these projects to e.g. disallow creation of privileged containers. Permissions can also be restricted when users authenticate with Candid (if using Canonical role-based access control (RBAC)). However, it is not currently possible to manage user permissions if a user authenticates with OIDC. This was addressed in the initial OpenFGA specification, however these changes were reverted for the following reasons:

  1. It was not possible to grant user permissions via the LXD API. Instead, we required the user to interact with the OpenFGA server directly (e.g. via the OpenFGA CLI). This was a bad user experience.
  2. The initial design was not compatible with TLS authentication because the existing permissions of the certificate were not considered.
  3. It required set up and maintenance of the external OpenFGA server. Some LXD or microcloud configurations may be air-gapped from external services, or may be deployed in an environment with limited resources. An external OpenFGA server that is highly available requires at minimum a highly available Postgres or MySQL cluster.
  4. For OIDC authentication, it was not possible to make use of groups defined by the Identity Provider (IdP) for access control decisions.

With this hindsight, the following constraints on an IAM solution for LXD have been determined:

  1. Fine-grained authorization should work out-of-the-box with TLS and OIDC authentication, with existing permissions defined on TLS certificates being respected.
  2. An external OpenFGA server should still be supported.
  3. It should be possible to allow usage of groups set by an external IdP.
  4. Management of user and group permissions should be possible via the LXD API. These APIs should be consistent for both built-in fine-grained permissions and with the external OpenFGA server.

Specification

Definitions

  • Identity. An identity is any authenticated party making requests to the LXD HTTPS API.
  • Group. A group is a collection of identities. Identities may belong to multiple groups.
  • LXD Entity. A LXD entity is a uniquely accessible LXD API resource with it’s own URL.
  • Entity Type. An entity type is a type of LXD entity. Entity types are associated with LXD API groups. For example, the /1.0/instances API is for management of instance entity types. Entity types are already in use in LXD under the /1.0/warnings API.
  • Entity Reference. A URL to a LXD entity.
  • Entity ID. The ID of the LXD entity in the LXD database.
  • Object. A concatenation of entity type and entity reference delimited by a colon (this will only be used for OpenFGA representations where necessary). For example, an OpenFGA object representation of an instance with name my-instance in project my-project would be instance:/1.0/instances/my-instance?project=my-project.
  • Relation. An OpenFGA relation in our OpenFGA model as defined by OpenFGA.
  • Entitlement. The subset of relations in our OpenFGA model that directly relate identities, service accounts, or group members to object types. For example, can_edit is a relation defined against the server object type and has [identity, service_account, group#member] as directly related user types, so can_edit is an entitlement on the server object type. A counterexample is the server relation defined against project, these relations are added to allow computed relations for built in roles (see the OpenFGA modelling guide for more information).
  • Permission. The relationship tuple which relates an identity or group to an object via an entitlement.
  • Role. A role is a collection of permissions. The functionality provided by roles is catered for by entitlements that are baked into the LXD OpenFGA model. For example, the admin entitlement defined on object type server, which grants full access to LXD via implied relationships. Roles are therefore considered to be an unnecessary indirection for LXD and will not be implemented in this specification.
  • Custom Claim. When performing an OIDC flow, scopes are requested by the application to gain more information about the identity that is being authenticated. Depending on the requested scopes, the IdP will add additional fields to access or identity tokens. These additional fields are called “claims”. A custom claim is a claim that is added to a token that is not part of the OAuth2.0 or OIDC standard. IdPs may be configured to add custom claims during login flow.

Overview

  • For built-in fine-grained authorization an OpenFGA server will be embedded into LXD. The backing data store will read directly from the LXD cluster database.
  • Authorization will be managed via new APIs for groups, identities, and permissions.
  • All permissions will be granted to identities via their group membership. It will not be possible to grant permissions directly to identities unless a Centralised IAM solution is used.
  • A new setting will configure a custom claim to be requested from the IdP. The contents of the claim will define group membership at IdP level. A mapping API will define how IdP groups are translated into LXD groups.

Embedded OpenFGA authorization driver

A key constraint on the IAM solution for LXD is that it should work without any external set up required. It is also required that permission management APIs function identically when using built-in permission management, or when using an external OpenFGA server. Since OpenFGA already provides a concise language and ecosystem for authorization, it makes sense to use OpenFGA as our built-in solution as well.

The OpenFGA server code base is Apache-2.0 licensed and is well structured. A github.com/openfga/openfga/pkg/server.Server can be instantiated directly within our code base. Methods can then be invoked directly, rather than via gRPC. To do this, it will be necessary to implement the github.com/openfga/openfga/pkg/storage.OpenFGADatastore
interface. The LXD implementation of OpenFGADatastore will be function by querying the LXD database directly.

To implement the OpenFGADatastore, some new tables, views, and triggers will be added to the database schema (see OpenFGADatastore schema):

  • The groups table contains minimal information about a group. A group is uniquely identified by its name. Group properties, e.g. identities and permissions will be defined by associative tables.
  • The permissions table contains an entitlement, the entity type that the entitlement is defined against, and the entity ID that the permission applies to. Care must be taken to ensure that the entitlement field correctly applies to the entity_type, this must be validated against the builtin OpenFGA model before writing to this table. The top level server type in the OpenFGA model has no database analogue, so the entity_id will be set to 0.
  • The groups_permissions table is an associative table for groups and permissions. The same permission may belong to many groups, and groups may have multiple permissions. This association between groups and permissions provides the mechanism by which OpenFGA relationship tuples can be generated to allow group members access to LXD entities.
  • The openfga_entity_ref view aggregates all LXD entities into a structured format from which OpenFGA tuples can be derived. This is necessary because there is no reliable delimiter that can be used to reliably generate entity references (URLs) directly in the view. Instead, the OpenFGADatastore implementation will convert each row into an OpenFGA tuple as it is read from the database. This view is quite large and may be a performance concern, see embedded OpenFGA driver performance.
  • The openfga_tuple_ref view is what will be queried by the OpenFGADatastore implementation. This view combines openfga_entity_ref with the groups, permissions, and groups_permissions tables such that each group is granted the correct permissions.

The OpenFGADatastore will be read only, because all tuples are created via other API calls. Additionally, note that permissions are associated with groups only. Permissions are not granted directly to identities.

Once the OpenFGADatastore is implemented, a github.com/openfga/openfga/pkg/server.Server can be instantiated as part of a new implementation of the github.com/canonical/lxd/lxd/auth.Authorizer interface.

TLS authentication and authorization

With the current TLS authorization driver it is possible to restrict a certificate to a set of projects. Restricted certificates cannot edit project settings. Additionally, restricted certificates may not make use of the all-projects query parameter for any resource.

The TLS authorization driver will be removed and replaced with the embedded OpenFGA driver. To maintain compatibility, the embedded and remote OpenFGA drivers will:

  1. Check if the authentication method is TLS, and if so disallow the all-projects query parameter when the certificate is restricted.

  2. For unrestricted certificates, all authorization queries will be allowed.

  3. For restricted certificates, all queries to the embedded Server object will include a contextual tuple for each allowed project:

     {
         "user": "identity:<identity_entity_ref>",
         "relation": "operator",
         "object": "project:<project_entity_ref>"
     }
    

    This is an equivalent permission from the LXD OpenFGA model. It allows the identity to manage resources within that project but does not allow the identity to edit the project settings.

The above contextual tuples are the only cases in which an identity will be granted a permission directly. In all other cases, permissions will be determined via group membership.

To manage fine-grained permissions for certificates a new groups field will be added to the certificate API resource:

type Certificate struct {
/*
... Existing fields ...
*/

// List of groups the certificate belongs to  
// Example: ["administrators"]  
//  
// API extension: permission_management  
Groups []string `json:"groups" yaml:"groups"`
}

Group membership will be tracked via an associative table between groups and certificates:

CREATE TABLE certificates_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    certificate_id INTEGER NOT NULL,
    group_id INTEGER NOT NULL,
    FOREIGN KEY (certificate_id) REFERENCES certificates (id) ON DELETE CASCADE,
    FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
    UNIQUE (group_id, certificate_id)
);

It will be possible to edit the groups that a certificate belongs to via the /1.0/certificates API. Editing a certificate will require the can_edit direct entitlement on the certificate, or can_edit_identities on server.

To factor group membership into Authorization decisions, group memberships will be included as contextual tuples when making requests to the embedded Server object or to the remote OpenFGA server:

{
	"user": "identity:<identity_entity_ref>",
	"relation": "member",
	"object": "group:<group_entity_ref>"
}

This works because all permissions associated with the group are baked into our embedded OpenFGADatastore via the openfga_tuple_ref view.

LXD currently has a cache of certificates that it uses for authentication and authorization. The cache includes the certificates and the projects they belong to. This cache will need to be extended to include group membership.

OIDC authentication and authorization

It is currently not possible to perform authorization decisions for identities that authenticate via OIDC; currently they are simply given full access to LXD. To enable fine-grained access control for OIDC identities, their details will be stored so that their group membership can be established. To do this, two tables will be added to the database:

CREATE TABLE identities (
	id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
	identifier TEXT NOT NULL,
	name TEXT NOT NULL,
	type INTEGER NOT NULL,
	UNIQUE (identifier)
);

CREATE TABLE identities_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    identity_id INTEGER NOT NULL,
    group_id INTEGER NOT NULL,
    FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE,
    FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
    UNIQUE (group_id, identity_id)
);

For OIDC identities, the identifier column of the identities table will be populated with the sub claim of the identity token (if using the user interface) or the sub claim of the access token (if using the CLI). The subject is a unique identifier for the identity from the IdP. When performing the OIDC flow, LXD (or the CLI) requests the email claim but this may not necessarily be present; this will be stored in the name column. The type column of the identities table will be an enumerated type that indicates the authentication method and the type of identity that was authenticated. The identities_groups table is an associative table that links identities to groups.

OIDC identities will be added to the identities table as soon as they authenticate with the LXD server. These identities will not be deleted (see Dangling identities).

To factor group membership into Authorization decisions, groups will be included as contextual tuples when making requests to the embedded Server object or to the remote OpenFGA server:

{
	"user": "identity:<identity_entity_ref>",
	"relation": "member",
	"object": "group:<group_entity_ref>"
}

A cache of OIDC identities and their groups will be implemented. This cache will be updated whenever an identity is added or removed from a group, and when a group is renamed or deleted.

IdP governed groups

It is common for organisations to manage identities and groups at the level of the identity provider e.g. organising groups by department and team membership. In order to make use of these groups, a oidc.groups_claim configuration key will be added to the server settings. When this configuration key is set, the value will be used as an extra OIDC scope to be requested as part of the OIDC flow. For the device authorization grant, the server will indicate the claim to the client with an X-LXD-OIDC-groups-claim header. Then, when verifying the access or identity token the contents of the claim will be extracted and set in the request context.

Question: How will we know what format to expect this as? Could be comma delimited, space delimited, JSON array?

IdP groups must be mapped to LXD groups. This ensures that permissions cannot be inadvertently escalated. The following tables will define the mapping of IdP groups to LXD groups (and vice versa):

CREATE TABLE identity_provider_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    name TEXT NOT NULL,
    UNIQUE (name)
);

CREATE TABLE groups_identity_provider_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    group_id INTEGER NOT NULL,
    identity_provider_group_id INTEGER NOT NULL,
    FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
    FOREIGN KEY (identity_provider_group_id) REFERENCES identity_provider_groups (id) ON DELETE CASCADE,
    UNIQUE (group_id, identity_provider_group_id)
);

To use the IdP groups in authorization queries, the following steps will be followed:

  1. Extract the IdP groups from the request context.
  2. For each IdP group, check if there is a mapping to a LXD group. If there is a mapping, collect all groups in the mapping and append them to a list of contextual tuples.
  3. If no IdP group mappings were found the identity is not authorized. In addition to sending the 403 Forbidden, an error will be returned to indicate that this may be due to a configuration error and can be raised to an administrator.

IdP group mappings will be cached so that authorization queries are as fast as possible.

Note: If a cluster member must forward a request to another member, it is important that the IdP groups are forwarded along with other request details such as the username (IdP subject) and authentication method.

Authorization driver for a remote OpenFGA server

This section will discuss how the embedded OpenFGA driver will be leveraged to improve the implementation of the remote OpenFGA driver. Please read this section alongside the previous OpenFGA specification.

Initial configuration of the remote OpenFGA authorization driver will be identical to the previous specification. However, the initial synchronisation will consider the tuples read from the openfga_tuple_ref as the source of truth. Additionally, only the database leader will perform the synchronisation. Furthermore, when a resource is created in LXD, say an instance, the authorization driver must write a tuple to relate this instance to its parent project. Previously, all object references were generated via Golang methods. Instead, the tuple that will be written to or deleted from the OpenFGA store will be selected directly from the openfga_tuple_ref view by the object_type and entity_id columns.

After the embedded tuples have been synced with the remote authorization checks will function identically to the embedded driver, with the exception that we will use the OpenFGA client instead.

Management of groups, identities, and permissions

The concepts of groups, identities, and permissions have been discussed in the previous sections. To manage these entities, the LXD API will be extended to include some new endpoints. The new API routes will be grouped under /1.0/auth. The client will be extended to include the new API routes, and new commands will be added to the CLI.

Identities API

The identities API will be used for viewing identities and managing their group membership.

Types
The Identity API types will be:

// Identity is the type for an authenticated party that can make requests to the HTTPS API.
//
// swagger:model
type Identity struct {
	// AuthenticationMethod is the authentication method that the identity
	// authenticates to LXD with.
	// Example: tls
	AuthenticationMethod string `json:"authentication_method" yaml:"authentication_method"`

	// Type is the type of identity.
	// Example: oidc-service-account
	Type string `json:"type" yaml:"type"`

	// Identifier is a unique identifier for the identity (e.g. certificate fingerprint or OIDC subject).
	// Example: auth0|4daf5e37ce230e455b64b65b
	Identifier string `json:"id" yaml:"id"`

	// Name is the email of the identity if authenticated via OIDC, or the name
	// of the certificate if authenticated with TLS.
	// Example: jane.doe@example.com
	Name string `json:"name" yaml:"name"`
}

// IdentityInfo expands an Identity to include group membership.
//
// swagger:model
type IdentityInfo struct {
	IdentityPut `yaml:",inline"`
	Identity    `yaml:",inline"`
}

// IdentityPut contains the editable fields of an IdentityFull.
//
// swagger:model
type IdentityPut struct {
	// Groups is the list of groups for which the identity is a member.
	// Example: ["foo", "bar"]
	Groups []string `json:"groups" yaml:"groups"`
}

Routes

  • GET /1.0/auth/identities. Returns a list of identity URLs. The results will be filtered by what the caller identity is authorized to view (e.g. all if related to server via can_manage_permissions, only self if not). Only the identifier of the identities will be given in the URLs. For example:
[
	"/1.0/auth/identities/tls/e1e06266e36f67431c996d5678e66d732dfd12fe5073c161e62e6360619fc226",
	"/1.0/auth/identities/oidc/auth0|4daf5e37ce230e455b64b65b",
]
  • GET /1.0/auth/identities?recursion=1. Returns a list of Identity objects. Results will be filtered by what the identity is authorized to view.
  • GET /1.0/auth/identities?recursion=2. Returns a list of IdentityInfo objects. Results will be filtered by what the identity is authorized to view.
  • GET /1.0/auth/identities/{authenticationMethod}. Returns a list of identity URLs (as in GET /1.0/auth/identities) for only the specified authentication method.
  • GET /1.0/auth/identities/{authenticationMethod}?recursion=1. Is the same as GET /1.0/auth/identities?recursion=1 but filtered by the authentication method.
  • GET /1.0/auth/identities/{authenticationMethod}?recursion=2. Is the same as GET /1.0/auth/identities?recursion=2 but filtered by the authentication method.
  • GET /1.0/auth/identities/{authenticationMethod}/{id|name}. Returns a single IdentityFull. The shortname will be allowed as a path parameter but this must resolve to a unique identity.
  • PUT /1.0/auth/identities/{authenticationMethod}/{id|name}. Sends IdentityPut in the request body and replaces all groups for which the identity is a member.
  • PATCH /1.0/auth/identities/{authenticationMethod}/{id|name}. Sends IdentityPut in the request body and appends groups to the identity (e.g. adds the identity to the given groups).

Groups API

The groups API will be used for creating, deleting, renaming, and assigning permissions to groups.

Types
The Group API types will be:


// Group is the type for a LXD Group.
//
// swagger:model
type Group struct {
	GroupsPost `yaml:",inline"`

	// Identities are the identities that are members of the group.
	Identities []Identity `json:"identities" yaml:"identities"`

	// IdentityProviderGroups are a list of groups from the IdP whose mapping
	// includes this group.
	// Example: ["sales", "operations"]
	IdentityProviderGroups []string
}

// GroupsPost is used for creating a new group.
//
// swagger:model
type GroupsPost struct {
	GroupPost `yaml:",inline"`
	GroupPut  `yaml:",inline"`
}

// GroupPost is used for renaming a group.
//
// swagger:model
type GroupPost struct {
	// Name is the name of the group.
	// Example: default-c1-viewers
	Name string `json:"name" yaml:"name"`
}

// GroupPut contains the editable fields of a group.
//
// swagger:model
type GroupPut struct {
	// Description is a short description of the group.
	// Example: Viewers of instance c1 in the default project.
	Description string `json:"description" yaml:"description"`

	// Permissions are a list of permissions.
	Permissions []Permission `json:"permissions" yaml:"permissions"`
}

// IdentityProviderGroup represents a mapping between LXD groups and groups defined by an identity provider.
//
// swagger:model
type IdentityProviderGroup struct {
	IdentityProviderGroupPost `yaml:",inline"`
	IdentityProviderGroupPut  `yaml:",inline"`
}

// IdentityProviderGroupPost is used for renaming an IdentityProviderGroup.
//
// swagger:model
type IdentityProviderGroupPost struct {
	// Name is the name of the IdP group.
	Name string `json:"name" yaml:"name"`
}

// IdentityProviderGroupPut contains the editable fields of an IdentityProviderGroup.
//
// swagger:model
type IdentityProviderGroupPut struct {
	// Groups are the groups the IdP group resolves to.
	// Example: ["foo", "bar"]
	Groups []string `json:"groups" yaml:"groups"`
}

Routes

  • GET /1.0/auth/groups. Returns a list of group URLs filtered by those the caller is allowed to view. All identities will be able to view groups that they are a member of:
[
	"/1.0/auth/groups/default-project-operators",
	"/1.0/auth/groups/instance-c1-users",
]
  • GET /1.0/auth/groups?recursion=1. Returns a list of Group objects filtered by those the identity is allowed to view. All identities will be able to view groups that they are a member of. The identities belonging to the group will be filtered by what the caller is allowed to view (unless they are related to server via can_manage_permissions this will display only the identity that made the request).
  • GET /1.0/auth/groups/{groupName}. Gets a single Group object.
  • POST /1.0/auth/groups. Creates a new group.
  • PUT /1.0/auth/groups/{groupName}. Replaces the description and permissions of the group.
  • POST /1.0/auth/groups/{groupName}. Renames the group.
  • PATCH /1.0/auth/groups/{groupName}. Partially updates the description (replace if not empty) and permissions (append) of the group.
  • DELETE /1.0/auth/groups/{groupName}. Deletes the group.
  • GET /1.0/auth/identity-provider-groups. Returns a list of IdP group URLs:
[
	"/1.0/auth/identity-provider-groups/sales",
	"/1.0/auth/identity-provider-groups/operations"
]
  • GET /1.0/auth/identity-provider-groups?recursion=1. Returns a list of IdentityProviderGroup objects.
  • GET /1.0/auth/identity-provider-groups/{idpGroupName}. Returns a single IdentityProviderGroup with the given name.
  • POST /1.0/auth/identity-provider-groups creates a new IdP group.
  • POST /1.0/auth/identity-provider-groups/{idpGroupName} renames an IdP group.
  • PUT /1.0/auth/identity-provider-groups/{idpGroupName} replaces the LXD groups that the IdP group resolves to.
  • PATCH /1.0/auth/identity-provider-groups/{idpGroupName} appends a LXD group to the list of groups that the IdP group resolves to.
  • DELETE /1.0/auth/identity-provider-groups/{idpGroupName} deletes the IdP group.

Permissions API

The permissions API will be read-only. The primary purpose of this API is to allow discovery of available permissions so that they can be granted to groups. These will only be viewable by an identity that is related to server via can_view_permissions.

Types
The Permission API types will be:

// Permission represents a permission that may be granted to a group.
//
// swagger:model
type Permission struct {
	// EntityType is the string representation of the entity type.
	// Example: instance
	EntityType string `json:"entity_type" yaml:"entity_type"`

	// EntityReference is the URL of the entity that the permission applies to.
	// Example: /1.0/instances/c1?project=default
	EntityReference string `json:"url" yaml:"url"`

	// Entitlement is the entitlement define for the entity type.
	// Example: can_view
	Entitlement string `json:"entitlement" yaml:"entitlement"`
}

// PermissionInfo expands a Permission to include any groups that may have the specified Permission.
//
// swagger:model
type PermissionInfo struct {
	Permission `yaml:",inline"`

	// Groups is a list of groups that have the Entitlement on the Entity.
	// Example: ["foo", "bar"]
	Groups []string `json:"groups" yaml:"groups"`
}

Routes

  • GET /1.0/auth/permissions. Returns all available permissions. Does not populate groups.
  • GET /1.0/auth/permissions?recursion=1. Returns all available permissions. Populates groups.
    The permissions API will be filterable on the following parameters:
  • project={projectName}: The project of the entity.
  • entity_type={entityType}: The entity type e.g. server.

New CLI commands

New CLI commands will be required for managing authorization. All new commands will be grouped under an auth sub-command.
Groups

  • lxc auth group create [<remote>:]<name>. Create a new group.
  • lxc auth group delete [<remote>:]<name>. Delete the group.
  • lxc auth group edit [<remote>:]<name>. Edit the group as yaml.
  • lxc auth group show [<remote>:]<name>. Show group details.
  • lxc auth group list [<remote>:] . List groups.
  • lxc auth group permission add <group_name> <object_type> [<object_name>] <entitlement> [<key>=<value>...]. Grant a single permission to the group. Key-value pairs can be added as supplementary arguments to uniquely specify the entity for which the permission is being granted against. Examples:
    • lxc auth group permission add foo project default operator. Grant group foo entitlement operator on project default.
    • lxc auth group permission add bar server admin. Grant group bar entitlement admin on the server.
    • lxc auth group permission add baz storage_volume vol1 can_manage_backups project=default pool=default location=node01 type=custom. Grant group baz entitlement can_manage_backups on storage volume vol1, which is of type custom, in project default, in storage pool default , and on cluster member node01.
  • lxc auth group permission remove <group_name> <object_type> <object_name> <entitlement> [<key>=<value>...]. Remove a single permission from the group.

Identities

  • lxc auth identity list [<remote>:]. List identities.
  • lxc auth identity edit [<remote>:]<authentication_method>/<identity_id|identity_name>. Edit the identity as yaml.
  • lxc auth identity show [<remote>:]<authentication_method>/<identity_id|identity_name>. Show the identity.
  • lxc auth identity group add [<remote>:]<authentication_method>/<identity_id|identity_name> <group>. Add an identity to a group.
  • lxc auth identity group remove [<remote>:]<authentication_method>/<identity_id|identity_name> <group>. Remove an identity from a group.

Permissions

  • lxc auth permission list [<key>=<value>...]. Lists permissions. Key-value pairs can be provided for filtering on project and object type.

OpenFGA model migrations

When using the embedded driver updating the OpenFGA model is very simple. A new model will be added, then any relevant changes to the openfga_tuple_ref view can be made via the existing schema update mechanism.

To update the OpenFGA model when using the remote driver we will:

  1. Detect the version of the model that is currently running. Successive versions of the OpenFGA model will be stored in ascending order under lxd/auth/openfga-models. A script will convert the models into JSON and store them in a generated go map. When the authorization driver connects to the OpenFGA store, it will read the latest authorization model and compare it against the models defined in the map.
  2. If the authorization model is not the latest, the driver will apply updates in the order that they are defined in the model map. Hooks will be defined in a corresponding map the will run before (PreModelWrite) and after the new model is written (PostModelWrite). The hooks will be given access to the LXD database, the OpenFGA driver configuration, an OpenFGA client for the store, and the model IDs of the previous and latest models.
  3. Once the latest authorization model has been written, all resources will be synchronised with the OpenFGA store and the authorization driver can be considered ready.

Discussion

Embedded OpenFGA Driver Performance

The openfga_tuple_ref view contains 14 select statements, some of which are non-trivial. In SQLite, the results of a view are not cached, only the execution plan is cached. When using the embedded OpenFGA driver, this view will be queried at least once per API request, therefore it is very important that the driver is performant even for a large cluster.

Performance of the embedded driver must be tested on a large cluster with many LXD entities before this feature is released. If the performance impact is high, it may be necessary to implement a caching strategy for OpenFGA tuples. The github.com/openfga/openfga/pkg/storage/memory.MemoryBackend could then be used. Unfortunately any caching strategy is likely to be quite complex because the cache will need to be refreshed on all nodes if any instance, storage pool, storage volume, network, etc. is created, deleted, or renamed.

Future Work

Dangling permissions

In the previous OpenFGA specification the removal of permissions that are no longer valid was left to the end user. When using the embedded OpenFGA driver invalid permissions will never be returned by the OpenFGADatastore, but they will remain in the permissions table until manually deleted. A periodic background task can be configured to remove these.

Dangling Identities

If left unchecked, the identities table (see OIDC authentication and authorization) could grow unnecessarily large. This is because all identities that log in via OIDC will be stored in this table. If an identity does not belong to any groups there is no reason to store their details. A periodic background task could remove these identities if necessary.

Centralised IAM solutions based on OIDC and OpenFGA

When using a centralised IAM solution, identities, groups, and permissions will be managed externally. It is expected that groups will be added to the OIDC identity token as a custom claim. It will be possible to configure LXD to extract this claim via the oidc.groups_claim server configuration key (see IdP governed groups). However it will no longer be necessary to verify that the groups exist (or have an associated IdP group mapping). To force LXD to skip this verification step a new server configuration key will be added: idp.managed (default false).

Another consequence of using a centralised IAM solution is that it will no longer be necessary to synchronise groups or permissions with the OpenFGA store. Otherwise, LXD will delete any groups created centrally and replace them with what is stored in the LXD database. The value of idp.managed will be passed into the remote OpenFGA authorization driver so that this can be accounted for.

Lastly, a centralised IAM solution will affect OpenFGA model migrations. For example, if a permission is granted centrally that has been renamed in the new model, we will need to collate these permissions and reset them on the new model version. The PreModelWrite and PostModelWrite hooks may be used for this.

Service accounts

A service_account type has been added to the new OpenFGA model. This is to facilitate the introduction of service accounts in future LXD releases but will not be used in the initial implementation of this specification. This is because it is not currently possible to distinguish between service accounts (who will use a Client Credentials Grant) and CLI users (who are using the Device Authorization Grant ) when performing OIDC authentication, as both clients will send an access token to LXD as a bearer token.

In future, we may introduce another custom claim configuration option so that these flows can be distinguished. Additionally, we may replace the metrics type certificate with a service_account type certificate.

Reference material

OpenFGA model

There are some changes from the model in the original specification:

  • The group type has a parent relation to server and new entitlement can_view. Members of a group can view the group (and see themselves as a member, but they won’t be able to see other identities).
  • The user type has been renamed identity.
  • A new service_account type has been added, it is equivalent to identity but may be used to distinguish between human and robotic users in future IAM deisgn.
  • The can_edit entitlement for all entity types has been split into can_edit and can_delete. Where an entity type has a relation to server or project the server and project have a corresponding entitlement for can_view, can_edit, and can_delete. E.g. project has can_view_instances, and instance has can_view.
  • New entitlements for permission management have been added to server. These are used for editing groups, identities, and permissions. Additionally, they are used in the can_edit relation of certificate, since editing a certificate to e.g. unrestrict it is the same as editing a permission.
model  
  schema 1.1  
type identity  
type service_account  
type group  
  relations  
    define server: [server]  
    define member: [identity, service_account]  
    define can_view: member or can_view_groups from server  
    define can_edit: can_edit_groups from server  
    define can_delete: can_delete_groups from server  
type server  
  relations  
    define admin: [identity, service_account, group#member]  
    define viewer: [identity, service_account, group#member]  
    define can_edit: [identity, service_account, group#member] or admin  
    define can_view: [identity:*, service_account:*]  
    define can_view_configuration: [identity, service_account, group#member] or can_edit or viewer  
    define permission_manager: [identity, service_account, group#member]  
    define can_create_identities: [identity, service_account, group#member] or permission_manager or admin  
    define can_view_identities: [identity, service_account, group#member] or permission_manager or admin or viewer  
    define can_edit_identities: [identity, service_account, group#member] or permission_manager or admin  
    define can_delete_identities: [identity, service_account, group#member] or permission_manager or admin  
    define can_create_groups: [identity, service_account, group#member] or permission_manager or admin  
    define can_view_groups: [identity, service_account, group#member] or permission_manager or admin or viewer  
    define can_edit_groups: [identity, service_account, group#member] or permission_manager or admin  
    define can_delete_groups: [identity, service_account, group#member] or permission_manager or admin
    define storage_pool_manager: [identity, service_account, group#member]  
    define can_create_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin  
    define can_edit_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin  
    define can_delete_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin
    define project_manager: [identity, service_account, group#member]  
    define can_create_projects: [identity, service_account, group#member] or project_manager or admin  
    define can_view_projects: [identity, service_account, group#member] or project_manager or viewer or admin  
    define can_edit_projects: [identity, service_account, group#member] or project_manager or admin  
    define can_delete_projects: [identity, service_account, group#member] or project_manager or admin
    define can_override_cluster_target_restriction: [identity, service_account, group#member] or admin  
    define can_view_privileged_events: [identity, service_account, group#member] or admin or viewer  
    define can_view_resources: [identity, service_account, group#member] or admin or viewer  
    define can_view_metrics: [identity, service_account, group#member] or admin or viewer  
    define can_view_warnings: [identity, service_account, group#member] or admin or viewer  
type certificate  
  relations  
    define server: [server]  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_identities from server  
    define can_edit: [identity, service_account, group#member] or can_edit_identities from server  
    define can_delete: [identity, service_account, group#member] or can_delete_identities from server  
type storage_pool  
  relations  
    define server: [server]  
    define can_view: can_view from server  
    define can_edit: [identity, service_account, group#member] or can_edit_storage_pools from server  
    define can_delete: [identity, service_account, group#member] or can_delete_storage_pools from server  
type project  
  relations  
    define server: [server]  
    define operator: [identity, service_account, group#member]  
    define viewer: [identity, service_account, group#member]  
    define can_view: [identity, service_account, group#member] or viewer or operator or can_view_projects from server  
    define can_edit: [identity, service_account, group#member] or can_edit_projects from server  
    define can_delete: [identity, service_account, group#member] or can_delete_projects from server  
    define image_manager: [identity, service_account, group#member]  
    define can_create_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server  
    define can_view_images: [identity, service_account, group#member] or operator or viewer or image_manager or can_view_projects from server  
    define can_edit_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server  
    define can_delete_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server  
    define image_alias_manager: [identity, service_account, group#member]  
    define can_create_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server  
    define can_view_image_aliases: [identity, service_account, group#member] or operator or viewer or image_alias_manager or can_view_projects from server  
    define can_edit_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server  
    define can_delete_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server  
    define instance_manager: [identity, service_account, group#member]  
    define can_create_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server  
    define can_view_instances: [identity, service_account, group#member] or operator or viewer or instance_manager or can_view_projects from server  
    define can_edit_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server  
    define can_delete_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server  
    define can_operate_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server
    define network_manager: [identity, service_account, group#member]  
    define can_create_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server  
    define can_view_networks: [identity, service_account, group#member] or operator or viewer or network_manager or can_view_projects from server  
    define can_edit_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server  
    define can_delete_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server
    define network_acl_manager: [identity, service_account, group#member]  
    define can_create_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server  
    define can_view_network_acls: [identity, service_account, group#member] or operator or viewer or network_acl_manager or can_view_projects from server  
    define can_edit_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server  
    define can_delete_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server
    define network_zone_manager: [identity, service_account, group#member]  
    define can_create_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server  
    define can_view_network_zones: [identity, service_account, group#member] or operator or viewer or network_zone_manager or can_view_projects from server  
    define can_edit_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server  
    define can_delete_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server
    define profile_manager: [identity, service_account, group#member]  
    define can_create_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server  
    define can_view_profiles: [identity, service_account, group#member] or operator or viewer or profile_manager or can_view_projects from server  
    define can_edit_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server  
    define can_delete_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server
    define storage_volume_manager: [identity, service_account, group#member]  
    define can_create_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server  
    define can_view_storage_volumes: [identity, service_account, group#member] or operator or viewer or storage_volume_manager or can_view_projects from server
    define can_edit_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server  
    define can_delete_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server  
    define storage_bucket_manager: [identity, service_account, group#member]  
    define can_create_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server  
    define can_view_storage_buckets: [identity, service_account, group#member] or operator or viewer or storage_bucket_manager or can_view_projects from server 
    define can_edit_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server  
    define can_delete_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server  
    define can_view_operations: [identity, service_account, group#member] or operator or viewer or can_view_projects from server  
    define can_view_events: [identity, service_account, group#member] or operator or viewer or can_view_projects from server  
type image  
  relations  
    define project: [project]  
    define can_edit: [identity, service_account, group#member] or can_edit_images from project  
    define can_delete: [identity, service_account, group#member] or can_delete_images from project  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_images from project  
type image_alias  
  relations  
    define project: [project]  
    define can_edit: [identity, service_account, group#member] or can_edit_image_aliases from project  
    define can_delete: [identity, service_account, group#member] or can_delete_image_aliases from project  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_image_aliases from project  
type instance  
  relations  
    define project: [project]  
    define user: [identity, service_account, group#member]  
    define operator: [identity, service_account, group#member]  
    define can_edit: [identity, service_account, group#member] or can_edit_instances from project  
    define can_delete: [identity, service_account, group#member] or can_delete_instances from project  
    define can_view: [identity, service_account, group#member] or user or operator or can_edit or can_delete or can_view_instances from project  
    define can_update_state: [identity, service_account, group#member] or operator or can_operate_instances from project  
    define can_manage_snapshots: [identity, service_account, group#member] or operator or can_operate_instances from project  
    define can_manage_backups: [identity, service_account, group#member] or operator or can_operate_instances from project  
    define can_connect_sftp: [identity, service_account, group#member] or user or operator or can_operate_instances from project  
    define can_access_files: [identity, service_account, group#member] or user or operator or can_operate_instances from project  
    define can_access_console: [identity, service_account, group#member] or user or operator or can_operate_instances from project  
    define can_exec: [identity, service_account, group#member] or user or operator or can_operate_instances from project  
type network  
  relations  
    define project: [project]  
    define can_edit: [identity, service_account, group#member] or can_edit_networks from project  
    define can_delete: [identity, service_account, group#member] or can_delete_networks from project  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_networks from project  
type network_acl  
  relations  
    define project: [project]  
    define can_edit: [identity, service_account, group#member] or can_edit_network_acls from project  
    define can_delete: [identity, service_account, group#member] or can_delete_network_acls from project  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_network_acls from project  
type network_zone  
  relations  
    define project: [project]  
    define can_edit: [identity, service_account, group#member] or can_edit_network_zones from project  
    define can_delete: [identity, service_account, group#member] or can_delete_network_zones from project  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_network_zones from project  
type profile  
  relations  
    define project: [project]  
    define can_edit: [identity, service_account, group#member] or can_edit_profiles from project  
    define can_delete: [identity, service_account, group#member] or can_delete_profiles from project  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_profiles from project  
type storage_volume  
  relations  
    define project: [project]  
    define can_edit: [identity, service_account, group#member] or can_edit_storage_volumes from project  
    define can_delete: [identity, service_account, group#member] or can_delete_storage_volumes from project  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_storage_volumes from project  
    define can_manage_snapshots: [identity, service_account, group#member] or can_edit_storage_volumes from project  
    define can_manage_backups: [identity, service_account, group#member] or can_edit_storage_volumes from project  
type storage_bucket  
  relations  
    define project: [project]  
    define can_edit: [identity, service_account, group#member] or can_edit_storage_buckets from project  
    define can_delete: [identity, service_account, group#member] or can_delete_storage_buckets from project  
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_storage_buckets from project

OpenFGADatastore schema

CREATE TABLE groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    name TEXT NOT NULL,
    description TEXT NOT NULL,
    UNIQUE (name)
);

CREATE TABLE permissions (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    entitlement TEXT NOT NULL,
    entity_type TEXT NOT NULL,
    entity_id INTEGER NOT NULL,
    UNIQUE (entitlement, entity_type, entity_id)
);

CREATE TABLE groups_permissions (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    group_id INTEGER NOT NULL,
    permission_id INTEGER NOT NULL,
    FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
    FOREIGN KEY (permission_id) REFERENCES permissions (id) ON DELETE CASCADE,
    UNIQUE (group_id, permission_id)
);

CREATE VIEW openfga_entity_ref (user_entity_type, user_entity_ref, user_relation, relation, entity_type, entity_ref, entity_id) AS
	-- Type-bound public access for identities for "can_view" on "server".
	SELECT 23, '*', '', 'can_view', 22, json_array(), 0

	-- Type-bound public access for service accounts for "can_view" on "server".
	UNION SELECT 25, '*', 'can_view', 'service_account', 22, json_array(), 0

	-- Server to project relations.
	UNION SELECT 22, json_array(), '', 'server', 3, json_array(projects.name), projects.id
		FROM projects
	
	-- Server to group relations.
	UNION SELECT 22, json_array(), '', 'server', 24, json_array(groups.name), groups.id
		FROM groups
	
	-- Server to certificate relations.
	UNION SELECT 22, json_array(), '', 'server', 4, json_array(certificates.fingerprint), certificates.id
		FROM certificates

	-- Server to storage pool relations.
  	UNION SELECT 22, json_array(), '', 'server', 12, json_array(storage_pools.name), storage_pools.id
		FROM storage_pools

	-- Project to image relations.
	UNION SELECT 3, json_array(projects.name), '', 'project', 1, json_array(projects.name, images.fingerprint), images.id
		FROM images JOIN projects ON project_id=projects.id
	
	-- Project to image alias relations.
	UNION SELECT 3, json_array(projects.name), '', 'project', 25, json_array(projects.name, images_aliases.name), images_aliases.id
		FROM images_aliases JOIN projects ON project_id=projects.id
    
	-- Project to instance relations.
	UNION SELECT 3, json_array(projects.name), '', 'project', 5, json_array(projects.name, instances.name), instances.id
    	FROM instances JOIN projects ON project_id=projects.id 
	
	-- Project to network relations.
	UNION SELECT 3, json_array(projects.name), '', 'project', 8, json_array(projects.name, networks.name), networks.id
    FROM networks JOIN projects ON project_id=projects.id 
    
    -- Project to network ACL relations.
    UNION SELECT 3, json_array(projects.name), '', 'project', 9, json_array(projects.name, networks_acls.name), networks_acls.id
    FROM networks_acls JOIN projects ON project_id=projects.id 
    
    -- Project to network zone relations.
    UNION SELECT 3, json_array(projects.name), '', 'project', 26, json_array(projects.name, networks_zones.name), networks_zones.id
    FROM networks_zones JOIN projects ON project_id=projects.id 
    
    -- Project to profile relations.
    UNION SELECT 3, json_array(projects.name), '', 'project', 2, json_array(projects.name, profiles.name), profiles.id
    FROM profiles JOIN projects ON project_id=projects.id 
    
    -- Project to storage volume relations.
    UNION SELECT 3, json_array(projects.name), '', 'project', 13,
		json_array(projects.name, storage_pools.name, 			
			-- Use case statement to resolve type to name.
			CASE storage_volumes.type
				WHEN 0 THEN 'container' 
         	    WHEN 1 THEN 'image' 
         	    WHEN 2 THEN 'custom' 
         	    WHEN 3 THEN 'virtual-machine' 
			END, storage_volumes.name, replace(coalesce(nodes.name, ''), 'none', '')),
		storage_volumes.id
    	FROM storage_volumes 
        	JOIN projects ON project_id=projects.id 
        	JOIN storage_pools ON storage_pool_id=storage_pools.id 
    	    LEFT JOIN nodes ON node_id=nodes.id
    
    -- Project to storage bucket relations.	
	UNION SELECT 3, json_array(projects.name), '', 'project', 18,
		json_array(projects.name, storage_pools.name, storage_buckets.name, replace(coalesce(nodes.name, ''), 'none', '')),
		storage_buckets.id
	FROM storage_buckets 
        JOIN projects ON project_id=projects.id 
        JOIN storage_pools ON storage_pool_id=storage_pools.id 
        LEFT JOIN nodes ON node_id=nodes.id;

CREATE VIEW openfga_tuple_ref (user_entity_type, user_entity_ref, user_relation, relation, entity_type, entity_ref, entity_id) AS
	-- Group permissions.
	SELECT 24, json_array(groups.name), 'member',
		permissions.entitlement,
		openfga_entity_ref.entity_type,
		openfga_entity_ref.entity_ref,
		groups_permissions.id
		FROM permissions
			JOIN groups_permissions ON permissions.id = groups_permissions.permission_id 
			JOIN groups ON groups_permissions.group_id = groups.id
			JOIN openfga_entity_ref ON permissions.entity_type = openfga_entity_ref.entity_type AND permissions.entity_id = openfga_entity_ref.entity_id

	UNION SELECT user_entity_type, user_entity_ref, user_relation, relation, entity_type, entity_ref, entity_id FROM openfga_entity_ref;

Is this restating the same thing in a different order, or am I missing some subtle meaning here?

Could you give an example here?

Could you mention the encoding scheme to be used here for when the value(s) being delimited contain a forward slash?

I think this should be defined in the Definitions above.

Is it worth also defining what we mean by “Permissions” in the LXD context here?

Can we use a non-null empty string here? It’d be nice to avoid a nullable field just for this case?

Do you mean “a large cluster with lots of LXD entities” rather than a cluster with lots of hardware resources?

Could we create a view per resource type so that if an API request only needs to check access to specific resource(s) of the same type we dont need to materialise the view for all resource types?

Do triggers actually work in a cluster on dqlite?

Will this allow a period of invalid access if an instance called say “foo” is created and assigned a user/group is assigned an entitlement to it and then that instance is deleted and new instance is created that is also called “foo”? This sounds like a bug in the design to me.

Is ID here correct?

blahblah padding for Discourse.

You mean the OpenFGA data store driver will be read-only? This could be clearer I think.

Please can you move this to its own paragraph and expand on what the implications of that are?
Does it mean that users cannot be assigned access to a specific resource without being in a group of 1?

We should add a last seen at time field so we can trim users who have not been seen for a while and are not associated to group(s).

Thanks for putting this together, comments below:

  1. @shipperizer @Lukewh and @alesstimec are working together on an OpenAPI spec for the identity platform admin UI component, which has to talk to a number of compoennts including OpenFGA. I think it’s better to align those efforts to avoid having 2 components adopting different interfaces to perform the same function. You can find the spec here https://github.com/canonical/openfga-admin-openapi-spec/pull/8
  2. While having a second look at the permission model I think that something needs to be added to express the relationship that exist between personal and robotic accounts, more precisely the fact that the former can manage the latter - you can find an example of that in the related JAAS spec & we can discuss more during our sync
  3. Dangling OIDC users - while this is a valid initial approach there are standards that are trying to automate the exchange of user identity information between connected systems. You can have a look at SCIM.
  4. Centralised IAM solutions based on OIDC and OpenFGA - I think we need a more detailed discussion on how migration strategies need to work when going from a embedded to an external store and vice versa, as this might have impacts on our implementation

As per your questions I think I can only answer the first one:

Question: How will we know what format to expect this as? Could be comma delimited, space delimited, JSON array?

I am assuming here you are referring to the group membership? In this case you can find sample token structures in he Microsoft and Okta official documentation. While I cannot exclude that some IDPs might follow different patterns, the decoded JWT responses tends to be similar in the major providers.

These are indeed different concepts, it basically means that there is a many to many relationship between users and groups

Group. A group is a collection of users. Users may belong to multiple groups and groups can have multiple users.

why calling it a group and not a role?same question applies to entitlements ↔ permissions

To factor group membership into Authorization decisions, groups membership will be included as contextual tuples when making requests to the embedded Server object or to the remote OpenFGA server:

a user can belong to many groups, these groups might or might not have clashing permissions, how do we deal with that?

After the embedded tuples have been synced with the remote authorization checks will function identically to the embedded driver, with the exception that we will use the gRPC client instead.

afaik openfga sdk is http only, the openfga/api repo gives you protobuf-generated clients but i am wondering if we would be missing something by switching to it?from what i heard the grpc sdk is coming but dont have any ETA

The entitlements API will be read-only

following the openapi spec in here https://github.com/canonical/openfga-admin-openapi-spec/pull/9/files this might be a problem, or better not a biggie but u might need to redirect the PUT to a specific endpoint where you can edit/affect the entitlements

Identity and Access Management for LXD

given the relationship we have with openfga guys, might be worth having them to scan the model just to ask if there could be any bottleneck/gotchas/improvement (hopefully thye just say it’s magnificent)

Hi, regarding the dangling users and the comment from @tomp about having a timestamp. Why not storing the expiry timestamp of the token? This way we can be sure the user is safe to delete in case there is no refresh of the token.

For auditing purposes, would it make sense to store timestamps alongside the groups_entitlements and/or alongside users to track when specific access was granted or when users accessed the API for the first/last time using a token/cert?

As this is in the definitions section I was trying to be explicit about the m to n relationship between users and groups:

  • “Users may belong to multiple groups”: 1 to n relationship between users and groups respectively.
  • “Groups can have multiple users”: 1 to n relationship between groups and users respectively.
  • “Users may belong to multiple groups and groups can have multiple users”: m to n relationship between users and groups.

It does feel quite stuttering though so I’m happy to reword it.

1 Like