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:
- 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.
- The initial design was not compatible with TLS authentication because the existing permissions of the certificate were not considered.
- 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.
- 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:
- Fine-grained authorization should work out-of-the-box with TLS and OIDC authentication, with existing permissions defined on TLS certificates being respected.
- An external OpenFGA server should still be supported.
- It should be possible to allow usage of groups set by an external IdP.
- 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 ofinstance
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 projectmy-project
would beinstance:/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 theserver
object type and has[identity, service_account, group#member]
as directly related user types, socan_edit
is an entitlement on theserver
object type. A counterexample is theserver
relation defined againstproject
, 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 typeserver
, 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 isinstance
, that it’s name ismy-instance
, and that the project it is contained in ismy-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 theauth_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:
- Backwards compatibility with existing TLS clients.
- 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:
- Extract the IdP groups from the request context.
- 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.
- 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 toserver
viacan_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 ofIdentity
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 inGET /1.0/auth/identities
) for only the specified authentication method.GET /1.0/auth/identities/{authenticationMethod}?recursion=1
. Is the same asGET /1.0/auth/identities?recursion=1
but filtered by the authentication method.GET /1.0/auth/identities/{authenticationMethod}/{id|name}
. Returns a singleIdentity
. 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}
. SendsIdentityPut
in the request body and replaces all groups for which the identity is a member.PATCH /1.0/auth/identities/{authenticationMethod}/{id|name}
. SendsIdentityPut
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 singleIdentityInfo
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 ofGroup
objects filtered by those the identity is allowed to view. All identities will be able to view groups that they are a member of. Theidentities
belonging to the group will be filtered by what the caller is allowed to view.GET /1.0/auth/groups/{groupName}
. Gets a singleGroup
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 ofIdentityProviderGroup
objects.GET /1.0/auth/identity-provider-groups/{idpGroupName}
. Returns a singleIdentityProviderGroup
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 groupfoo
entitlementoperator
on projectdefault
.lxc auth group permission add bar server admin
. Grant groupbar
entitlementadmin
on the server. The<entity_name>
can be omitted when the<entity_type>
isserver
.lxc auth group permission add baz storage_volume vol1 can_manage_backups project=default pool=default location=node01 type=custom
. Grant groupbaz
entitlementcan_manage_backups
on storage volumevol1
, which is of typecustom
, in projectdefault
, in storage pooldefault
, and on cluster membernode01
.
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:
- List all entities of a particular type.
- 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)
);