Identity and Access Management for LXD

Project LXD
Status Completed
Author(s) @markylaing
Approver(s) @tomp
Release 5.21.0
Internal ID LX061

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 identities table contains all identities known to LXD. These identities may authenticate via mutual TLS or via OIDC.

  • The auth_groups table contains minimal information about an authorization group. A group is uniquely identified by its name. Group properties, e.g. identities and permissions will be defined by associative tables.

  • The identities_auth_groups table is an associative table that defines the groups that an identity belongs to and vice versa.

  • The auth_groups_permissions table defines the permissions that a group has. These permissions are defined by:

    • An entitlement.
    • An entity type.
    • An entity ID.

    Using the entity type and ID, an entity reference can be composed for the entity. Similarly, given an entity reference, the entity ID and type can be determined. For example, given the following OpenFGA tuple

    {
      "user": "auth_group:/1.0/auth/groups/my-group",
      "relation": "can_view",
      "object": "instance:/1.0/instances/my-instance?my-project"
    }
    

    The OpenFGADatastore will determine that the entity type of the object is instance, that it’s name is my-instance, and that the project it is contained in is my-project. With this information, it will query the database to get the entity ID of the instance. Subsequently, it can determine the validity of the relation by querying the auth_groups_permissions table.

  • The auth_groups_identity_provider_groups table provides a mapping between groups defined by the Identity Provider and authorization groups in LXD.

A set of triggers will be added to the database such that when an entity is deleted from the database, any corresponding entries in auth_groups_permissions will be deleted. This is necessary because it is not possible for the auth_groups_permissions table to reference an entity directly with a foreign key, since it must join with so many tables. Additionally, care must be taken to ensure that the entitlement column in auth_groups_permissions 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 OpenFGADatastore will be read only, because all tuples are created via other API calls. Additionally, note that auth_group_permissions are references auth_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.

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,
    auth_method INTEGER NOT NULL,
    type INTEGER NOT NULL,
    identifier TEXT NOT NULL,
    name TEXT NOT NULL,
    metadata TEXT NOT NULL,
    first_seen_date DATETIME NOT NULL DEFAULT "0001-01-01T00:00:00Z",
    last_seen_date DATETIME NOT NULL DEFAULT "0001-01-01T00:00:00Z",
    updated_date DATETIME NOT NULL DEFAULT "0001-01-01T00:00:00Z",
    UNIQUE (auth_method, identifier),
    UNIQUE (type, identifier)
);

CREATE TABLE identities_auth_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    identity_id INTEGER NOT NULL,
    auth_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)
);

The identities table will contain all identities known to LXD, including those that authenticate with OIDC and with mutual TLS. The type column of the identities table will be an enumerated type that indicates the authentication method and the type of the authenticated identity.

For mTLS identities, the identifier column will contain the certificate fingerprint, the name column will contain the certificate name, and the metadata column will contain a JSON object with a cert key whose value is the PEM encoded public certificate of the client (this is required for mTLS verification).

For OIDC identities, the identifier column of the identities table will be populated with the email of the identity. LXD will request access to the email via the email OIDC scope. The profile scope will also be requested. This will be used to populate the name column. The metadata column will contain a JSON object with a subject key that will contain the OIDC subject (the subject is a unique identifier for the identity from the IdP).

Note: Since the email address of the identity is used as their unique identifier, if their OIDC subject changes they will retain their access. This may happen if a company chooses to change identity provider, but should be handled with care.

mTLS identities will be moved to the identities table. OIDC identities will be added to the identities table as soon as they authenticate with the LXD server. Deletion of OIDC identities will not be handled by the initial implementation of this specification (see Dangling identities). OIDC identities will be updated if their OIDC subject changes.

The identities_auth_groups table is an associative table that links identities to groups. To factor group membership into Authorization decisions, groups will be included as contextual tuples when making requests to the embedded Server object:

{
	"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.

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.

Though the functionality of the TLS authorization driver is superseded by the embedded OpenFGA authorization driver, it must remain in LXD. This is for two reasons:

  1. Backwards compatibility with existing TLS clients.
  2. The embedded OpenFGA driver requires the cluster database to be operational. The TLS driver must be loaded at start up to processes authorization queries when the cluster starts up (i.e. to grant access to peer cluster members).

To enable fine-grained authorization for TLS clients. We will add a new identity type Client certificate (fine-grained). It will be possible to add identities of this type to authorization groups. Authorization decisions for identities with this certificate type will be processed by the embedded OpenFGA driver. It will not be possible to specify the restricted property of these certificates, nor will it be possible to specify a list of projects.

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. We will expect that the claim contents will be a 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.

LXD will not store the IdP groups that an identity belongs to. When IdP group mappings are in use, the LXD API cannot represent the true permissions of a single identity. A new API endpoint will be added to show the true permissions of the calling identity.

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 OpenFGADatastore as the source of truth. Only the database leader will perform the synchronisation. The mechanism for ongoing synchronisation has not yet been discussed. 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
//
// API extension: access_management.
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 email for OIDC).
	// Example: jane.doe@example.com
	Identifier string `json:"id" yaml:"id"`

	// Name is the Name claim of the identity if authenticated via OIDC, or the name
	// of the certificate if authenticated with TLS.
	// Example: Jane Doe
	Name string `json:"name" yaml:"name"`

	// Groups is the list of groups for which the identity is a member.
	// Example: ["foo", "bar"]
	Groups []string `json:"groups" yaml:"groups"`
}

// IdentityInfo expands an Identity to include effective group membership and effective permissions.
// These fields can only be evaluated for the currently authenticated identity.
//
// swagger:model
//
// API extension: access_management.
type IdentityInfo struct {
	Identity `yaml:",inline"`

	// Effective groups is the combined and deduplicated list of LXD groups that the identity is a direct member of, and
	// the LXD groups that the identity is an effective member of via identity provider group mappings.
	// Example: ["foo", "bar"]
	EffectiveGroups []string `json:"effective_groups" yaml:"effective_groups"`

	// Effective permissions is the combined and deduplicated list of permissions that the identity has by virtue of
	// direct membership to a LXD group, or effective membership of a LXD group via identity provider group mappings.
	EffectivePermissions []Permission `json:"effective_permissions" yaml:"effective_permissions"`
}

// IdentityPut contains the editable fields of an Identity.
//
// swagger:model
//
// API extension: access_management.
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_view_identities, 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/jane.doe@example.com",
]
  • 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/{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}/{id|name}. Returns a single Identity. 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).
  • GET /1.0/auth/identities/current. Returns a single IdentityInfo for the caller. This will include a list of effective groups, which may originate from the identity provider via custom claim.

Groups API

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

Types
The Group API types will be:


// AuthGroup is the type for a LXD group.
//
// swagger:model
//
// API extension: access_management.
type AuthGroup struct {
	// Name is the name of the group.
	// Example: default-c1-viewers
	Name string `json:"name" yaml:"name"`

	// 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"`

	// Identities is a map of authentication method to slice of identity identifiers.
	Identities map[string][]string `json:"identities" yaml:"identities"`

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

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

// AuthGroupsPost is used for creating a new group.
//
// swagger:model
//
// API extension: access_management.
type AuthGroupsPost struct {
	AuthGroupPost `yaml:",inline"`
	AuthGroupPut  `yaml:",inline"`
}

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

// AuthGroupPut contains the editable fields of a group.
//
// swagger:model
//
// API extension: access_management.
type AuthGroupPut 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
//
// API extension: access_management.
type IdentityProviderGroup struct {
	// Name is the name of the IdP group.
	Name string `json:"name" yaml:"name"`

	// Groups are the groups the IdP group resolves to.
	// Example: ["foo", "bar"]
	Groups []string `json:"groups" yaml:"groups"`
}

// IdentityProviderGroupPost is used for renaming an IdentityProviderGroup.
//
// swagger:model
//
// API extension: access_management.
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
//
// API extension: access_management.
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.
  • 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> <entity_type> [<entity_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. The <entity_name> can be omitted when the <entity_type> is 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> <entity_type> [<entity_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 a patch can be run to modify any existing permissions in the auth_groups_permissions table.

Discussion

Embedded OpenFGA Driver Performance

The embedded OpenFGA driver must be able to perform authorization queries quickly for the driver to be functional, since authorization is always on the critical path.

To perform it’s function, the OpenFGADatastore must be able to quickly:

  1. List all entities of a particular type.
  2. Determine the type and ID of an entity based on it’s entity reference (URL).

To accomplish this, the driver will use a heavily optimized set of queries that are standardized such that they can be concatenated with a UNION operator. This will reduce the total number of queries that the driver must perform. Nevertheless, the number of queries that the embedded OpenFGA server will perform depends on a number of factors, including the complexity of the authorization model (e.g. the number of computed relations).

For this reason, performance of the embedded driver should 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 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. The first_seen_date, last_seen_date, and updated_date columns of the identities table will be added to allow backporting any work on this to the latest LTS version.

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

model
  schema 1.1
type identity
  relations
    define server: [server]

    # Grants permission to view the identity.
    define can_view: [identity, service_account, group#member] or can_view_identities from server

    # Grants permission to edit the identity.
    define can_edit: [identity, service_account, group#member] or can_edit_identities from server

    # Grants permission to delete the identity.
    define can_delete: [identity, service_account, group#member] or can_delete_identities from server
type service_account
type group
  relations
    define server: [server]
    define member: [identity, service_account]

    # Grants permission to view the group. Identities can always view groups that they are a member of.
    define can_view: [identity, service_account, group#member] or member or can_view_groups from server

    # Grants permission to edit the group.
    define can_edit: [identity, service_account, group#member] or can_edit_groups from server

    # Grants permission to delete the group.
    define can_delete: [identity, service_account, group#member] or can_delete_groups from server
type identity_provider_group
  relations
    define server: [server]

    # Grants permission to view the identity provider group.
    define can_view: [identity, service_account, group#member] or can_view_identity_provider_groups from server

    # Grants permission to edit the identity provider group.
    define can_edit: [identity, service_account, group#member] or can_edit_identity_provider_groups from server

    # Grants permission to delete the identity provider group.
    define can_delete: [identity, service_account, group#member] or can_delete_identity_provider_groups from server
type server
  relations
    # Grants full access to LXD as if via Unix socket.
    define admin: [identity, service_account, group#member]

    # Grants access to view all resources in the LXD server.
    define viewer: [identity, service_account, group#member]

    # Grants permission to edit server configuration, to edit cluster member configuration, to update the state of a cluster
    # member, to create, edit, and delete cluster groups, to update cluster member certificates, and to edit or delete warnings.
    define can_edit: [identity, service_account, group#member] or admin
    define can_view: [identity:*, service_account:*]

    # Grants permission to view permissions, to create, edit, and delete identities, to view, create, edit, and delete
    # authorization groups, and to view, create, edit, and delete identity provider groups. Note that clients with this
    # permission are able to elevate their own privileges.
    define permission_manager: [identity, service_account, group#member]

    # Grants permission to view permissions.
    define can_view_permissions: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to create identities.
    define can_create_identities: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to view identities.
    define can_view_identities: [identity, service_account, group#member] or permission_manager or admin or viewer

    # Grants permission to edit identities.
    define can_edit_identities: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to delete identities.
    define can_delete_identities: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to create authorization groups.
    define can_create_groups: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to view authorization groups.
    define can_view_groups: [identity, service_account, group#member] or permission_manager or admin or viewer

    # Grants permission to edit authorization groups.
    define can_edit_groups: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to delete authorization groups.
    define can_delete_groups: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to create identity provider groups.
    define can_create_identity_provider_groups: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to view identity provider groups.
    define can_view_identity_provider_groups: [identity, service_account, group#member] or permission_manager or admin or viewer

    # Grants permission to edit identity provider groups.
    define can_edit_identity_provider_groups: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to delete identity provider groups.
    define can_delete_identity_provider_groups: [identity, service_account, group#member] or permission_manager or admin

    # Grants permission to create, edit, and delete storage pools.
    define storage_pool_manager: [identity, service_account, group#member]

    # Grants permission to create storage pools.
    define can_create_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin

    # Grants permission to edit storage pools.
    define can_edit_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin

    # Grants permission to delete storage pools.
    define can_delete_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin

    # Grants permission to view, create, edit, and delete projects, and to create, edit, and delete any resources
    # that are owned by those projects.
    define project_manager: [identity, service_account, group#member]

    # Grants permission to create projects.
    define can_create_projects: [identity, service_account, group#member] or project_manager or admin

    # Grants permission to view projects, and all resources within those projects.
    define can_view_projects: [identity, service_account, group#member] or project_manager or viewer or admin

    # Grants permission to edit projects, and all resources within those projects.
    define can_edit_projects: [identity, service_account, group#member] or project_manager or admin

    # Grants permission to delete projects.
    define can_delete_projects: [identity, service_account, group#member] or project_manager or admin

    # If a project is configured with `restricted.cluster.target`, clients with this permission can override the restriction.
    define can_override_cluster_target_restriction: [identity, service_account, group#member] or admin

    # Grants permission to view privileged event types, such as logging events.
    define can_view_privileged_events: [identity, service_account, group#member] or admin or viewer

    # Grants permission to view server and storage pool resource usage information.
    define can_view_resources: [identity, service_account, group#member] or admin or viewer

    # Grants permission to view all server and project level metrics.
    define can_view_metrics: [identity, service_account, group#member] or admin or viewer

    # Grants permission to view warnings.
    define can_view_warnings: [identity, service_account, group#member] or admin or viewer
type certificate
  relations
    define server: [server]

    # Grants permission to view the certificate.
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_identities from server

    # Grants permission to edit the certificate.
    define can_edit: [identity, service_account, group#member] or can_edit_identities from server

    # Grants permission to delete the certificate.
    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

    # Grants permission to edit the storage pool.
    define can_edit: [identity, service_account, group#member] or can_edit_storage_pools from server

    # Grants permission to delete the storage pool.
    define can_delete: [identity, service_account, group#member] or can_delete_storage_pools from server
type project
  relations
    define server: [server]

    # Grants permission to create, view, edit, and delete all resources belonging to the project, but does not grant
    # permission to edit the project configuration itself.
    define operator: [identity, service_account, group#member]

    # Grants permission to view all resources belonging to the project.
    define viewer: [identity, service_account, group#member]

    # Grants permission to view the project.
    define can_view: [identity, service_account, group#member] or viewer or operator or can_view_projects from server

    # Grants permission to edit the project.
    define can_edit: [identity, service_account, group#member] or can_edit_projects from server

    # Grants permission to delete the project.
    define can_delete: [identity, service_account, group#member] or can_delete_projects from server

    # Grants permission to create, view, edit, and delete all images belonging to the project.
    define image_manager: [identity, service_account, group#member]

    # Grants permission to create images.
    define can_create_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server

    # Grants permission to view images.
    define can_view_images: [identity, service_account, group#member] or operator or viewer or image_manager or can_view_projects from server

    # Grants permission to edit images.
    define can_edit_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server

    # Grants permission to delete images.
    define can_delete_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server

    # Grants permission to create, view, edit, and delete all image aliases belonging to the project.
    define image_alias_manager: [identity, service_account, group#member]

    # Grants permission to create image aliases.
    define can_create_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server

    # Grants permission to view image aliases.
    define can_view_image_aliases: [identity, service_account, group#member] or operator or viewer or image_alias_manager or can_view_projects from server

    # Grants permission to edit image aliases.
    define can_edit_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server

    # Grants permission to delete image aliases.
    define can_delete_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server

    # Grants permission to create, view, edit, and delete all instances belonging to the project.
    define instance_manager: [identity, service_account, group#member]

    # Grants permission to create instances.
    define can_create_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server

    # Grants permission to view instances.
    define can_view_instances: [identity, service_account, group#member] or operator or viewer or instance_manager or can_view_projects from server

    # Grants permission to edit instances.
    define can_edit_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server

    # Grants permission to delete instances.
    define can_delete_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server

    # Grants permission to view instances, manage their state, manage their snapshots and backups, start terminal or console sessions, and access their files.
    define can_operate_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server

    # Grants permission to create, view, edit, and delete all networks belonging to the project.
    define network_manager: [identity, service_account, group#member]

    # Grants permission to create networks.
    define can_create_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server

    # Grants permission to view networks.
    define can_view_networks: [identity, service_account, group#member] or operator or viewer or network_manager or can_view_projects from server

    # Grants permission to edit networks.
    define can_edit_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server

    # Grants permission to delete networks.
    define can_delete_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server

    # Grants permission to create, view, edit, and delete all network ACLs belonging to the project.
    define network_acl_manager: [identity, service_account, group#member]

    # Grants permission to create network ACLs.
    define can_create_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server

    # Grants permission to view network ACLs.
    define can_view_network_acls: [identity, service_account, group#member] or operator or viewer or network_acl_manager or can_view_projects from server

    # Grants permission to edit network ACLs.
    define can_edit_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server

    # Grants permission to delete network ACLs.
    define can_delete_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server

    # Grants permission to create, view, edit, and delete all network zones belonging to the project.
    define network_zone_manager: [identity, service_account, group#member]

    # Grants permission to create network zones.
    define can_create_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server

    # Grants permission to view network zones.
    define can_view_network_zones: [identity, service_account, group#member] or operator or viewer or network_zone_manager or can_view_projects from server

    # Grants permission to edit network zones.
    define can_edit_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server

    # Grants permission to delete network zones.
    define can_delete_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server

    # Grants permission to create, view, edit, and delete all profiles belonging to the project.
    define profile_manager: [identity, service_account, group#member]

    # Grants permission to create profiles.
    define can_create_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server

    # Grants permission to view profiles.
    define can_view_profiles: [identity, service_account, group#member] or operator or viewer or profile_manager or can_view_projects from server

    # Grants permission to edit profiles.
    define can_edit_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server

    # Grants permission to delete profiles.
    define can_delete_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server

    # Grants permission to create, view, edit, and delete all storage volumes belonging to the project.
    define storage_volume_manager: [identity, service_account, group#member]

    # Grants permission to create storage volumes.
    define can_create_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server

    # Grants permission to view storage volumes.
    define can_view_storage_volumes: [identity, service_account, group#member] or operator or viewer or storage_volume_manager or can_view_projects from server

    # Grants permission to edit storage volumes.
    define can_edit_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server

    # Grants permission to delete storage volumes.
    define can_delete_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server

    # Grants permission to create, view, edit, and delete all storage buckets belonging to the project.
    define storage_bucket_manager: [identity, service_account, group#member]

    # Grants permission to create storage buckets.
    define can_create_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server

    # Grants permission to view storage buckets.
    define can_view_storage_buckets: [identity, service_account, group#member] or operator or viewer or storage_bucket_manager or can_view_projects from server

    # Grants permission to edit storage buckets.
    define can_edit_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server

    # Grants permission to delete storage buckets.
    define can_delete_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server

    # Grants permission to view operations relating to the project.
    define can_view_operations: [identity, service_account, group#member] or operator or viewer or can_view_projects from server

    # Grants permission to view events relating to the project.
    define can_view_events: [identity, service_account, group#member] or operator or viewer or can_view_projects from server

    # Grants permission to view project level metrics.
    define can_view_metrics: [identity, service_account, group#member] or operator or viewer or can_view_metrics from server
type image
  relations
    define project: [project]

    # Grants permission to edit the image.
    define can_edit: [identity, service_account, group#member] or can_edit_images from project

    # Grants permission to delete the image.
    define can_delete: [identity, service_account, group#member] or can_delete_images from project

    # Grants permission to view the image.
    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]

    # Grants permission to edit the image alias.
    define can_edit: [identity, service_account, group#member] or can_edit_image_aliases from project

    # Grants permission to delete the image alias.
    define can_delete: [identity, service_account, group#member] or can_delete_image_aliases from project

    # Grants permission to view the image alias.
    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]

    # Grants permission to view the instance, to access files, and to start a terminal or console session.
    define user: [identity, service_account, group#member]

    # Grants permission to view the instance, to access files, start a terminal or console session, and to manage snapshots and backups.
    define operator: [identity, service_account, group#member]

    # Grants permission to edit the instance.
    define can_edit: [identity, service_account, group#member] or can_edit_instances from project

    # Grants permission to delete the instance.
    define can_delete: [identity, service_account, group#member] or can_delete_instances from project

    # Grants permission to view the instance.
    define can_view: [identity, service_account, group#member] or user or operator or can_edit or can_delete or can_view_instances from project

    # Grants permission to change the instance state.
    define can_update_state: [identity, service_account, group#member] or operator or can_operate_instances from project

    # Grants permission to create and delete snapshots of the instance.
    define can_manage_snapshots: [identity, service_account, group#member] or operator or can_operate_instances from project

    # Grants permission to create and delete backups of the instance.
    define can_manage_backups: [identity, service_account, group#member] or operator or can_operate_instances from project

    # Grants permission to get an SFTP client for the instance.
    define can_connect_sftp: [identity, service_account, group#member] or user or operator or can_operate_instances from project

    # Grants permission to push or pull files into or out of the instance.
    define can_access_files: [identity, service_account, group#member] or user or operator or can_operate_instances from project

    # Grants permission to start a console session.
    define can_access_console: [identity, service_account, group#member] or user or operator or can_operate_instances from project

    # Grants permission to start a terminal session.
    define can_exec: [identity, service_account, group#member] or user or operator or can_operate_instances from project
type network
  relations
    define project: [project]

    # Grants permission to edit the network.
    define can_edit: [identity, service_account, group#member] or can_edit_networks from project

    # Grants permission to delete the network.
    define can_delete: [identity, service_account, group#member] or can_delete_networks from project

    # Grants permission to view the network.
    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]

    # Grants permission to edit the network ACL.
    define can_edit: [identity, service_account, group#member] or can_edit_network_acls from project

    # Grants permission to delete the network ACL.
    define can_delete: [identity, service_account, group#member] or can_delete_network_acls from project

    # Grants permission to view the network ACL.
    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]

    # Grants permission to edit the network zone.
    define can_edit: [identity, service_account, group#member] or can_edit_network_zones from project

    # Grants permission to delete the network zone.
    define can_delete: [identity, service_account, group#member] or can_delete_network_zones from project

    # Grants permission to view the network zone.
    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]

    # Grants permission to edit the profile.
    define can_edit: [identity, service_account, group#member] or can_edit_profiles from project

    # Grants permission to delete the profile.
    define can_delete: [identity, service_account, group#member] or can_delete_profiles from project

    # Grants permission to view the profile.
    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]

    # Grants permission to edit the storage volume.
    define can_edit: [identity, service_account, group#member] or can_edit_storage_volumes from project

    # Grants permission to delete the storage volume.
    define can_delete: [identity, service_account, group#member] or can_delete_storage_volumes from project

    # Grants permission to view the storage volume.
    define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_storage_volumes from project

    # Grants permission to create and delete snapshots of the storage volume.
    define can_manage_snapshots: [identity, service_account, group#member] or can_edit_storage_volumes from project

    # Grants permission to create and delete backups of the storage volume.
    define can_manage_backups: [identity, service_account, group#member] or can_edit_storage_volumes from project
type storage_bucket
  relations
    define project: [project]

    # Grants permission to edit the storage bucket.
    define can_edit: [identity, service_account, group#member] or can_edit_storage_buckets from project

    # Grants permission to delete the storage bucket.
    define can_delete: [identity, service_account, group#member] or can_delete_storage_buckets from project

    # Grants permission to view the storage bucket.
    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 auth_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    name TEXT NOT NULL,
    description TEXT NOT NULL,
    UNIQUE (name)
);

CREATE TABLE auth_groups_identity_provider_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    auth_group_id INTEGER NOT NULL,
    identity_provider_group_id INTEGER NOT NULL,
    FOREIGN KEY (auth_group_id) REFERENCES auth_groups (id) ON DELETE CASCADE,
    FOREIGN KEY (identity_provider_group_id) REFERENCES identity_provider_groups (id) ON DELETE CASCADE,
    UNIQUE (auth_group_id, identity_provider_group_id)
);

CREATE TABLE auth_groups_permissions (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    auth_group_id INTEGER NOT NULL,
    entity_type INTEGER NOT NULL,
    entity_id INTEGER NOT NULL,
    entitlement TEXT NOT NULL,
    FOREIGN KEY (auth_group_id) REFERENCES auth_groups (id) ON DELETE CASCADE,
    UNIQUE (auth_group_id, entity_type, entitlement, entity_id)
);

CREATE TABLE identities_auth_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    identity_id INTEGER NOT NULL,
    auth_group_id INTEGER NOT NULL,
    FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE,
    FOREIGN KEY (auth_group_id) REFERENCES auth_groups (id) ON DELETE CASCADE,
    UNIQUE (identity_id, auth_group_id)
);

CREATE TABLE identities (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    auth_method INTEGER NOT NULL,
    type INTEGER NOT NULL,
    identifier TEXT NOT NULL,
    name TEXT NOT NULL,
    metadata TEXT NOT NULL,
    first_seen_date DATETIME NOT NULL DEFAULT "0001-01-01T00:00:00Z",
    last_seen_date DATETIME NOT NULL DEFAULT "0001-01-01T00:00:00Z",
    updated_date DATETIME NOT NULL DEFAULT "0001-01-01T00:00:00Z",
    UNIQUE (auth_method, identifier),
    UNIQUE (type, identifier)
);

-- This table allows backwards compatibility for restricted TLS identities.
CREATE TABLE identities_projects (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    identity_id INTEGER NOT NULL,
    project_id INTEGER NOT NULL,
    FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE,
    FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
    UNIQUE (identity_id, project_id)
);

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

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