From 843d7d69b738edb7bc160fe7ad7344aab9341807 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 18:13:40 +0000 Subject: [PATCH 01/34] api: Adds `authorization_apis` API extension. Signed-off-by: Mark Laing --- doc/api-extensions.md | 4 ++++ shared/version/api.go | 1 + 2 files changed, 5 insertions(+) diff --git a/doc/api-extensions.md b/doc/api-extensions.md index efdd83adae04..dffe5d706619 100644 --- a/doc/api-extensions.md +++ b/doc/api-extensions.md @@ -2362,3 +2362,7 @@ This API extension indicates that the `/1.0/instances/{name}/uefi-vars` endpoint This API extension allows newly created VMs to have their `migration.stateful` configuration key automatically set through the new server-level configuration key `instances.migration.stateful`. If `migration.stateful` is already set at the profile or instance level then `instances.migration.stateful` is not applied. + +## `authorization_apis` + +Adds new APIs under `/1.0/auth` for viewing and managing identities, groups, and permissions. diff --git a/shared/version/api.go b/shared/version/api.go index 5cc3d0a57b7e..2ba76f918db0 100644 --- a/shared/version/api.go +++ b/shared/version/api.go @@ -399,6 +399,7 @@ var APIExtensions = []string{ "instances_uefi_vars", "instances_migration_stateful", "container_syscall_filtering_allow_deny_syntax", + "authorization_apis", } // APIExtensionsCount returns the number of available API extensions. From a804d5827bcd42491a9d2d4ed940f95679dde064 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Tue, 27 Feb 2024 14:21:55 +0000 Subject: [PATCH 02/34] shared/entity: Add identity, auth group and IDP group entity types. Signed-off-by: Mark Laing --- shared/entity/type.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/shared/entity/type.go b/shared/entity/type.go index 1ed9f56193ad..b104c3768d46 100644 --- a/shared/entity/type.go +++ b/shared/entity/type.go @@ -81,6 +81,15 @@ const ( // TypeNetworkZone represents network zone resources. TypeNetworkZone Type = "network_zone" + + // TypeIdentity represents identity resources. + TypeIdentity Type = "identity" + + // TypeAuthGroup represents authorization group resources. + TypeAuthGroup Type = "group" + + // TypeIdentityProviderGroup represents identity provider group resources. + TypeIdentityProviderGroup Type = "identity_provider_group" ) const ( @@ -111,6 +120,9 @@ var entityTypes = []Type{ TypeServer, TypeImageAlias, TypeNetworkZone, + TypeIdentity, + TypeAuthGroup, + TypeIdentityProviderGroup, } // String implements fmt.Stringer for Type. @@ -136,7 +148,7 @@ func (t Type) requiresProject() (bool, error) { return false, err } - return !shared.ValueInSlice(t, []Type{TypeProject, TypeCertificate, TypeNode, TypeOperation, TypeStoragePool, TypeWarning, TypeClusterGroup, TypeServer}), nil + return !shared.ValueInSlice(t, []Type{TypeProject, TypeCertificate, TypeNode, TypeOperation, TypeStoragePool, TypeWarning, TypeClusterGroup, TypeServer, TypeAuthGroup, TypeIdentityProviderGroup, TypeIdentity}), nil } // nRequiredPathArguments returns the number of path arguments (mux variables) that are required to create a unique URL @@ -265,6 +277,12 @@ func (t Type) path() ([]string, error) { return []string{"images", "aliases", pathPlaceholder}, nil case TypeNetworkZone: return []string{"network-zones", pathPlaceholder}, nil + case TypeIdentity: + return []string{"auth", "identities", pathPlaceholder, pathPlaceholder}, nil + case TypeAuthGroup: + return []string{"auth", "groups", pathPlaceholder}, nil + case TypeIdentityProviderGroup: + return []string{"auth", "identity-provider-groups", pathPlaceholder}, nil default: return nil, fmt.Errorf("Missing path definition for entity type %q", t) } @@ -426,3 +444,18 @@ func StorageVolumeURL(projectName string, location string, storagePoolName strin func StorageBucketURL(projectName string, location string, storagePoolName string, storageBucketName string) *api.URL { return TypeStorageBucket.urlMust(projectName, location, storagePoolName, storageBucketName) } + +// IdentityURL returns an *api.URL to an identity. +func IdentityURL(authenticationMethod string, identifier string) *api.URL { + return TypeIdentity.urlMust("", "", authenticationMethod, identifier) +} + +// AuthGroupURL returns an *api.URL to a group. +func AuthGroupURL(groupName string) *api.URL { + return TypeAuthGroup.urlMust("", "", groupName) +} + +// IdentityProviderGroupURL returns an *api.URL to an identity provider group. +func IdentityProviderGroupURL(identityProviderGroupName string) *api.URL { + return TypeIdentityProviderGroup.urlMust("", "", identityProviderGroupName) +} From 42f74fc3e176614ab3ca7af5e70d3680a5cb69cb Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:32:24 +0000 Subject: [PATCH 03/34] lxd/db/cluster: Add schema update for auth APIs. Signed-off-by: Mark Laing --- lxd/db/cluster/update.go | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/lxd/db/cluster/update.go b/lxd/db/cluster/update.go index f30bc0489084..f54ac63d9c17 100644 --- a/lxd/db/cluster/update.go +++ b/lxd/db/cluster/update.go @@ -107,6 +107,64 @@ var updates = map[int]schema.Update{ 68: updateFromV67, 69: updateFromV68, 70: updateFromV69, + 71: updateFromV70, +} + +func updateFromV70(ctx context.Context, tx *sql.Tx) error { + _, err := tx.Exec(` +CREATE TABLE auth_groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + UNIQUE (name) +); + +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 identity_provider_groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name 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 permissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + entitlement TEXT NOT NULL, + entity_type TEXT NOT NULL, + entity_id INTEGER NOT NULL, + UNIQUE (entitlement, entity_type, entity_id) +); + +CREATE TABLE auth_groups_permissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + auth_group_id INTEGER NOT NULL, + permission_id INTEGER NOT NULL, + FOREIGN KEY (auth_group_id) REFERENCES auth_groups (id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions (id) ON DELETE CASCADE, + UNIQUE (auth_group_id, permission_id) +); +`) + if err != nil { + return err + } + + return nil } func updateFromV69(ctx context.Context, tx *sql.Tx) error { From 2d95698cefd5a9c9f93df2b8915d183046cac279 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:32:40 +0000 Subject: [PATCH 04/34] lxd/db/cluster: Run make update-schema. Signed-off-by: Mark Laing --- lxd/db/cluster/schema.go | 44 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/lxd/db/cluster/schema.go b/lxd/db/cluster/schema.go index 63dffde68a73..6c17ad26530c 100644 --- a/lxd/db/cluster/schema.go +++ b/lxd/db/cluster/schema.go @@ -6,6 +6,28 @@ package cluster // modify the database schema, please add a new schema update to update.go // and the run 'make update-schema'. const freshSchema = ` +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, + permission_id INTEGER NOT NULL, + FOREIGN KEY (auth_group_id) REFERENCES auth_groups (id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions (id) ON DELETE CASCADE, + UNIQUE (auth_group_id, permission_id) +); CREATE TABLE "cluster_groups" ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, @@ -28,6 +50,14 @@ CREATE TABLE identities ( 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 (auth_group_id) REFERENCES auth_groups (id) ON DELETE CASCADE, + UNIQUE (identity_id, auth_group_id) +); CREATE TABLE identities_projects ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, identity_id INTEGER NOT NULL, @@ -36,6 +66,11 @@ CREATE TABLE identities_projects ( 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) +); CREATE TABLE "images" ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, fingerprint TEXT NOT NULL, @@ -395,6 +430,13 @@ CREATE TABLE "operations" ( FOREIGN KEY (node_id) REFERENCES "nodes" (id) ON DELETE CASCADE, FOREIGN KEY (project_id) REFERENCES "projects" (id) ON DELETE CASCADE ); +CREATE TABLE permissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + entitlement TEXT NOT NULL, + entity_type TEXT NOT NULL, + entity_id INTEGER NOT NULL, + UNIQUE (entitlement, entity_type, entity_id) +); CREATE TABLE "profiles" ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, @@ -623,5 +665,5 @@ CREATE TABLE "warnings" ( ); CREATE UNIQUE INDEX warnings_unique_node_id_project_id_entity_type_code_entity_id_type_code ON warnings(IFNULL(node_id, -1), IFNULL(project_id, -1), entity_type_code, entity_id, type_code); -INSERT INTO schema (version, updated_at) VALUES (70, strftime("%s")) +INSERT INTO schema (version, updated_at) VALUES (71, strftime("%s")) ` From fdcd9e546f70b25c57a1301e0502fe8d860aec20 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:37:05 +0000 Subject: [PATCH 05/34] lxd/db/cluster: Add database types for groups and permissions. Signed-off-by: Mark Laing --- lxd/db/cluster/auth_groups.go | 37 ++++++++++++++++++++++ lxd/db/cluster/identity_provider_groups.go | 36 +++++++++++++++++++++ lxd/db/cluster/permissions.go | 35 ++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 lxd/db/cluster/auth_groups.go create mode 100644 lxd/db/cluster/identity_provider_groups.go create mode 100644 lxd/db/cluster/permissions.go diff --git a/lxd/db/cluster/auth_groups.go b/lxd/db/cluster/auth_groups.go new file mode 100644 index 000000000000..2095a0a915e4 --- /dev/null +++ b/lxd/db/cluster/auth_groups.go @@ -0,0 +1,37 @@ +package cluster + +// Code generation directives. +// +//go:generate -command mapper lxd-generate db mapper -t auth_groups.mapper.go +//go:generate mapper reset -i -b "//go:build linux && cgo && !agent" +// +//go:generate mapper stmt -e auth_group objects table=auth_groups +//go:generate mapper stmt -e auth_group objects-by-ID table=auth_groups +//go:generate mapper stmt -e auth_group objects-by-Name table=auth_groups +//go:generate mapper stmt -e auth_group id table=auth_groups +//go:generate mapper stmt -e auth_group create table=auth_groups +//go:generate mapper stmt -e auth_group delete-by-Name table=auth_groups +//go:generate mapper stmt -e auth_group update table=auth_groups +//go:generate mapper stmt -e auth_group rename table=auth_groups +// +//go:generate mapper method -i -e auth_group GetMany +//go:generate mapper method -i -e auth_group GetOne +//go:generate mapper method -i -e auth_group ID +//go:generate mapper method -i -e auth_group Exists +//go:generate mapper method -i -e auth_group Create +//go:generate mapper method -i -e auth_group DeleteOne-by-Name +//go:generate mapper method -i -e auth_group Update +//go:generate mapper method -i -e auth_group Rename + +// AuthGroup is the database representation of an api.AuthGroup. +type AuthGroup struct { + ID int + Name string `db:"primary=true"` + Description string +} + +// AuthGroupFilter contains fields upon which an AuthGroup can be filtered. +type AuthGroupFilter struct { + ID *int + Name *string +} diff --git a/lxd/db/cluster/identity_provider_groups.go b/lxd/db/cluster/identity_provider_groups.go new file mode 100644 index 000000000000..b3d3e9dd2212 --- /dev/null +++ b/lxd/db/cluster/identity_provider_groups.go @@ -0,0 +1,36 @@ +package cluster + +// Code generation directives. +// +//go:generate -command mapper lxd-generate db mapper -t identity_provider_groups.mapper.go +//go:generate mapper reset -i -b "//go:build linux && cgo && !agent" +// +//go:generate mapper stmt -e identity_provider_group objects table=identity_provider_groups +//go:generate mapper stmt -e identity_provider_group objects-by-ID table=identity_provider_groups +//go:generate mapper stmt -e identity_provider_group objects-by-Name table=identity_provider_groups +//go:generate mapper stmt -e identity_provider_group id table=identity_provider_groups +//go:generate mapper stmt -e identity_provider_group create table=identity_provider_groups +//go:generate mapper stmt -e identity_provider_group delete-by-Name table=identity_provider_groups +//go:generate mapper stmt -e identity_provider_group update table=identity_provider_groups +//go:generate mapper stmt -e identity_provider_group rename table=identity_provider_groups +// +//go:generate mapper method -i -e identity_provider_group GetMany +//go:generate mapper method -i -e identity_provider_group GetOne +//go:generate mapper method -i -e identity_provider_group ID +//go:generate mapper method -i -e identity_provider_group Exists +//go:generate mapper method -i -e identity_provider_group Create +//go:generate mapper method -i -e identity_provider_group DeleteOne-by-Name +//go:generate mapper method -i -e identity_provider_group Update +//go:generate mapper method -i -e identity_provider_group Rename + +// IdentityProviderGroup is the database representation of an api.IdentityProviderGroup. +type IdentityProviderGroup struct { + ID int + Name string `db:"primary=true"` +} + +// IdentityProviderGroupFilter contains the columns that a queries for identity provider groups can be filtered upon. +type IdentityProviderGroupFilter struct { + ID *int + Name *string +} diff --git a/lxd/db/cluster/permissions.go b/lxd/db/cluster/permissions.go new file mode 100644 index 000000000000..5170d45086aa --- /dev/null +++ b/lxd/db/cluster/permissions.go @@ -0,0 +1,35 @@ +package cluster + +import ( + "github.com/canonical/lxd/lxd/auth" +) + +// Code generation directives. +// +//go:generate -command mapper lxd-generate db mapper -t permissions.mapper.go +//go:generate mapper reset -i -b "//go:build linux && cgo && !agent" +// +//go:generate mapper stmt -e permission objects +//go:generate mapper stmt -e permission objects-by-ID +//go:generate mapper stmt -e permission objects-by-EntityType +//go:generate mapper stmt -e permission objects-by-EntityType-and-EntityID +//go:generate mapper stmt -e permission objects-by-EntityType-and-EntityID-and-Entitlement +// +//go:generate mapper method -i -e permission GetMany +//go:generate mapper method -i -e permission GetOne + +// Permission is the database representation of an api.Permission. +type Permission struct { + ID int + Entitlement auth.Entitlement `db:"primary=true"` + EntityType EntityType `db:"primary=true"` + EntityID int `db:"primary=true"` +} + +// PermissionFilter contains the fields upon which a Permission may be filtered. +type PermissionFilter struct { + ID *int + Entitlement *auth.Entitlement + EntityType *EntityType + EntityID *int +} From 1435f2ea7fd504beb734ae19b694dd1c748893a1 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Fri, 23 Feb 2024 15:49:12 +0000 Subject: [PATCH 06/34] lxd/db/cluster: Runs make update-schema. Signed-off-by: Mark Laing --- .../cluster/auth_groups.interface.mapper.go | 43 ++ lxd/db/cluster/auth_groups.mapper.go | 387 ++++++++++++++++++ ...entity_provider_groups.interface.mapper.go | 43 ++ .../identity_provider_groups.mapper.go | 386 +++++++++++++++++ .../cluster/permissions.interface.mapper.go | 21 + lxd/db/cluster/permissions.mapper.go | 269 ++++++++++++ 6 files changed, 1149 insertions(+) create mode 100644 lxd/db/cluster/auth_groups.interface.mapper.go create mode 100644 lxd/db/cluster/auth_groups.mapper.go create mode 100644 lxd/db/cluster/identity_provider_groups.interface.mapper.go create mode 100644 lxd/db/cluster/identity_provider_groups.mapper.go create mode 100644 lxd/db/cluster/permissions.interface.mapper.go create mode 100644 lxd/db/cluster/permissions.mapper.go diff --git a/lxd/db/cluster/auth_groups.interface.mapper.go b/lxd/db/cluster/auth_groups.interface.mapper.go new file mode 100644 index 000000000000..d068653d4749 --- /dev/null +++ b/lxd/db/cluster/auth_groups.interface.mapper.go @@ -0,0 +1,43 @@ +//go:build linux && cgo && !agent + +package cluster + +import ( + "context" + "database/sql" +) + +// AuthGroupGenerated is an interface of generated methods for AuthGroup. +type AuthGroupGenerated interface { + // GetAuthGroups returns all available auth_groups. + // generator: auth_group GetMany + GetAuthGroups(ctx context.Context, tx *sql.Tx, filters ...AuthGroupFilter) ([]AuthGroup, error) + + // GetAuthGroup returns the auth_group with the given key. + // generator: auth_group GetOne + GetAuthGroup(ctx context.Context, tx *sql.Tx, name string) (*AuthGroup, error) + + // GetAuthGroupID return the ID of the auth_group with the given key. + // generator: auth_group ID + GetAuthGroupID(ctx context.Context, tx *sql.Tx, name string) (int64, error) + + // AuthGroupExists checks if a auth_group with the given key exists. + // generator: auth_group Exists + AuthGroupExists(ctx context.Context, tx *sql.Tx, name string) (bool, error) + + // CreateAuthGroup adds a new auth_group to the database. + // generator: auth_group Create + CreateAuthGroup(ctx context.Context, tx *sql.Tx, object AuthGroup) (int64, error) + + // DeleteAuthGroup deletes the auth_group matching the given key parameters. + // generator: auth_group DeleteOne-by-Name + DeleteAuthGroup(ctx context.Context, tx *sql.Tx, name string) error + + // UpdateAuthGroup updates the auth_group matching the given key parameters. + // generator: auth_group Update + UpdateAuthGroup(ctx context.Context, tx *sql.Tx, name string, object AuthGroup) error + + // RenameAuthGroup renames the auth_group matching the given key parameters. + // generator: auth_group Rename + RenameAuthGroup(ctx context.Context, tx *sql.Tx, name string, to string) error +} diff --git a/lxd/db/cluster/auth_groups.mapper.go b/lxd/db/cluster/auth_groups.mapper.go new file mode 100644 index 000000000000..71ce36e42761 --- /dev/null +++ b/lxd/db/cluster/auth_groups.mapper.go @@ -0,0 +1,387 @@ +//go:build linux && cgo && !agent + +package cluster + +// The code below was generated by lxd-generate - DO NOT EDIT! + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/canonical/lxd/lxd/db/query" + "github.com/canonical/lxd/shared/api" +) + +var _ = api.ServerEnvironment{} + +var authGroupObjects = RegisterStmt(` +SELECT auth_groups.id, auth_groups.name, auth_groups.description + FROM auth_groups + ORDER BY auth_groups.name +`) + +var authGroupObjectsByID = RegisterStmt(` +SELECT auth_groups.id, auth_groups.name, auth_groups.description + FROM auth_groups + WHERE ( auth_groups.id = ? ) + ORDER BY auth_groups.name +`) + +var authGroupObjectsByName = RegisterStmt(` +SELECT auth_groups.id, auth_groups.name, auth_groups.description + FROM auth_groups + WHERE ( auth_groups.name = ? ) + ORDER BY auth_groups.name +`) + +var authGroupID = RegisterStmt(` +SELECT auth_groups.id FROM auth_groups + WHERE auth_groups.name = ? +`) + +var authGroupCreate = RegisterStmt(` +INSERT INTO auth_groups (name, description) + VALUES (?, ?) +`) + +var authGroupDeleteByName = RegisterStmt(` +DELETE FROM auth_groups WHERE name = ? +`) + +var authGroupUpdate = RegisterStmt(` +UPDATE auth_groups + SET name = ?, description = ? + WHERE id = ? +`) + +var authGroupRename = RegisterStmt(` +UPDATE auth_groups SET name = ? WHERE name = ? +`) + +// authGroupColumns returns a string of column names to be used with a SELECT statement for the entity. +// Use this function when building statements to retrieve database entries matching the AuthGroup entity. +func authGroupColumns() string { + return "auths_groups.id, auths_groups.name, auths_groups.description" +} + +// getAuthGroups can be used to run handwritten sql.Stmts to return a slice of objects. +func getAuthGroups(ctx context.Context, stmt *sql.Stmt, args ...any) ([]AuthGroup, error) { + objects := make([]AuthGroup, 0) + + dest := func(scan func(dest ...any) error) error { + a := AuthGroup{} + err := scan(&a.ID, &a.Name, &a.Description) + if err != nil { + return err + } + + objects = append(objects, a) + + return nil + } + + err := query.SelectObjects(ctx, stmt, dest, args...) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"auths_groups\" table: %w", err) + } + + return objects, nil +} + +// getAuthGroupsRaw can be used to run handwritten query strings to return a slice of objects. +func getAuthGroupsRaw(ctx context.Context, tx *sql.Tx, sql string, args ...any) ([]AuthGroup, error) { + objects := make([]AuthGroup, 0) + + dest := func(scan func(dest ...any) error) error { + a := AuthGroup{} + err := scan(&a.ID, &a.Name, &a.Description) + if err != nil { + return err + } + + objects = append(objects, a) + + return nil + } + + err := query.Scan(ctx, tx, sql, dest, args...) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"auths_groups\" table: %w", err) + } + + return objects, nil +} + +// GetAuthGroups returns all available auth_groups. +// generator: auth_group GetMany +func GetAuthGroups(ctx context.Context, tx *sql.Tx, filters ...AuthGroupFilter) ([]AuthGroup, error) { + var err error + + // Result slice. + objects := make([]AuthGroup, 0) + + // Pick the prepared statement and arguments to use based on active criteria. + var sqlStmt *sql.Stmt + args := []any{} + queryParts := [2]string{} + + if len(filters) == 0 { + sqlStmt, err = Stmt(tx, authGroupObjects) + if err != nil { + return nil, fmt.Errorf("Failed to get \"authGroupObjects\" prepared statement: %w", err) + } + } + + for i, filter := range filters { + if filter.Name != nil && filter.ID == nil { + args = append(args, []any{filter.Name}...) + if len(filters) == 1 { + sqlStmt, err = Stmt(tx, authGroupObjectsByName) + if err != nil { + return nil, fmt.Errorf("Failed to get \"authGroupObjectsByName\" prepared statement: %w", err) + } + + break + } + + query, err := StmtString(authGroupObjectsByName) + if err != nil { + return nil, fmt.Errorf("Failed to get \"authGroupObjects\" prepared statement: %w", err) + } + + parts := strings.SplitN(query, "ORDER BY", 2) + if i == 0 { + copy(queryParts[:], parts) + continue + } + + _, where, _ := strings.Cut(parts[0], "WHERE") + queryParts[0] += "OR" + where + } else if filter.ID != nil && filter.Name == nil { + args = append(args, []any{filter.ID}...) + if len(filters) == 1 { + sqlStmt, err = Stmt(tx, authGroupObjectsByID) + if err != nil { + return nil, fmt.Errorf("Failed to get \"authGroupObjectsByID\" prepared statement: %w", err) + } + + break + } + + query, err := StmtString(authGroupObjectsByID) + if err != nil { + return nil, fmt.Errorf("Failed to get \"authGroupObjects\" prepared statement: %w", err) + } + + parts := strings.SplitN(query, "ORDER BY", 2) + if i == 0 { + copy(queryParts[:], parts) + continue + } + + _, where, _ := strings.Cut(parts[0], "WHERE") + queryParts[0] += "OR" + where + } else if filter.ID == nil && filter.Name == nil { + return nil, fmt.Errorf("Cannot filter on empty AuthGroupFilter") + } else { + return nil, fmt.Errorf("No statement exists for the given Filter") + } + } + + // Select. + if sqlStmt != nil { + objects, err = getAuthGroups(ctx, sqlStmt, args...) + } else { + queryStr := strings.Join(queryParts[:], "ORDER BY") + objects, err = getAuthGroupsRaw(ctx, tx, queryStr, args...) + } + + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"auths_groups\" table: %w", err) + } + + return objects, nil +} + +// GetAuthGroup returns the auth_group with the given key. +// generator: auth_group GetOne +func GetAuthGroup(ctx context.Context, tx *sql.Tx, name string) (*AuthGroup, error) { + filter := AuthGroupFilter{} + filter.Name = &name + + objects, err := GetAuthGroups(ctx, tx, filter) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"auths_groups\" table: %w", err) + } + + switch len(objects) { + case 0: + return nil, api.StatusErrorf(http.StatusNotFound, "AuthGroup not found") + case 1: + return &objects[0], nil + default: + return nil, fmt.Errorf("More than one \"auths_groups\" entry matches") + } +} + +// GetAuthGroupID return the ID of the auth_group with the given key. +// generator: auth_group ID +func GetAuthGroupID(ctx context.Context, tx *sql.Tx, name string) (int64, error) { + stmt, err := Stmt(tx, authGroupID) + if err != nil { + return -1, fmt.Errorf("Failed to get \"authGroupID\" prepared statement: %w", err) + } + + row := stmt.QueryRowContext(ctx, name) + var id int64 + err = row.Scan(&id) + if errors.Is(err, sql.ErrNoRows) { + return -1, api.StatusErrorf(http.StatusNotFound, "AuthGroup not found") + } + + if err != nil { + return -1, fmt.Errorf("Failed to get \"auths_groups\" ID: %w", err) + } + + return id, nil +} + +// AuthGroupExists checks if a auth_group with the given key exists. +// generator: auth_group Exists +func AuthGroupExists(ctx context.Context, tx *sql.Tx, name string) (bool, error) { + _, err := GetAuthGroupID(ctx, tx, name) + if err != nil { + if api.StatusErrorCheck(err, http.StatusNotFound) { + return false, nil + } + + return false, err + } + + return true, nil +} + +// CreateAuthGroup adds a new auth_group to the database. +// generator: auth_group Create +func CreateAuthGroup(ctx context.Context, tx *sql.Tx, object AuthGroup) (int64, error) { + // Check if a auth_group with the same key exists. + exists, err := AuthGroupExists(ctx, tx, object.Name) + if err != nil { + return -1, fmt.Errorf("Failed to check for duplicates: %w", err) + } + + if exists { + return -1, api.StatusErrorf(http.StatusConflict, "This \"auths_groups\" entry already exists") + } + + args := make([]any, 2) + + // Populate the statement arguments. + args[0] = object.Name + args[1] = object.Description + + // Prepared statement to use. + stmt, err := Stmt(tx, authGroupCreate) + if err != nil { + return -1, fmt.Errorf("Failed to get \"authGroupCreate\" prepared statement: %w", err) + } + + // Execute the statement. + result, err := stmt.Exec(args...) + if err != nil { + return -1, fmt.Errorf("Failed to create \"auths_groups\" entry: %w", err) + } + + id, err := result.LastInsertId() + if err != nil { + return -1, fmt.Errorf("Failed to fetch \"auths_groups\" entry ID: %w", err) + } + + return id, nil +} + +// DeleteAuthGroup deletes the auth_group matching the given key parameters. +// generator: auth_group DeleteOne-by-Name +func DeleteAuthGroup(ctx context.Context, tx *sql.Tx, name string) error { + stmt, err := Stmt(tx, authGroupDeleteByName) + if err != nil { + return fmt.Errorf("Failed to get \"authGroupDeleteByName\" prepared statement: %w", err) + } + + result, err := stmt.Exec(name) + if err != nil { + return fmt.Errorf("Delete \"auths_groups\": %w", err) + } + + n, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("Fetch affected rows: %w", err) + } + + if n == 0 { + return api.StatusErrorf(http.StatusNotFound, "AuthGroup not found") + } else if n > 1 { + return fmt.Errorf("Query deleted %d AuthGroup rows instead of 1", n) + } + + return nil +} + +// UpdateAuthGroup updates the auth_group matching the given key parameters. +// generator: auth_group Update +func UpdateAuthGroup(ctx context.Context, tx *sql.Tx, name string, object AuthGroup) error { + id, err := GetAuthGroupID(ctx, tx, name) + if err != nil { + return err + } + + stmt, err := Stmt(tx, authGroupUpdate) + if err != nil { + return fmt.Errorf("Failed to get \"authGroupUpdate\" prepared statement: %w", err) + } + + result, err := stmt.Exec(object.Name, object.Description, id) + if err != nil { + return fmt.Errorf("Update \"auths_groups\" entry failed: %w", err) + } + + n, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("Fetch affected rows: %w", err) + } + + if n != 1 { + return fmt.Errorf("Query updated %d rows instead of 1", n) + } + + return nil +} + +// RenameAuthGroup renames the auth_group matching the given key parameters. +// generator: auth_group Rename +func RenameAuthGroup(ctx context.Context, tx *sql.Tx, name string, to string) error { + stmt, err := Stmt(tx, authGroupRename) + if err != nil { + return fmt.Errorf("Failed to get \"authGroupRename\" prepared statement: %w", err) + } + + result, err := stmt.Exec(to, name) + if err != nil { + return fmt.Errorf("Rename AuthGroup failed: %w", err) + } + + n, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("Fetch affected rows failed: %w", err) + } + + if n != 1 { + return fmt.Errorf("Query affected %d rows instead of 1", n) + } + + return nil +} diff --git a/lxd/db/cluster/identity_provider_groups.interface.mapper.go b/lxd/db/cluster/identity_provider_groups.interface.mapper.go new file mode 100644 index 000000000000..4187c06d912d --- /dev/null +++ b/lxd/db/cluster/identity_provider_groups.interface.mapper.go @@ -0,0 +1,43 @@ +//go:build linux && cgo && !agent + +package cluster + +import ( + "context" + "database/sql" +) + +// IdentityProviderGroupGenerated is an interface of generated methods for IdentityProviderGroup. +type IdentityProviderGroupGenerated interface { + // GetIdentityProviderGroups returns all available identity_provider_groups. + // generator: identity_provider_group GetMany + GetIdentityProviderGroups(ctx context.Context, tx *sql.Tx, filters ...IdentityProviderGroupFilter) ([]IdentityProviderGroup, error) + + // GetIdentityProviderGroup returns the identity_provider_group with the given key. + // generator: identity_provider_group GetOne + GetIdentityProviderGroup(ctx context.Context, tx *sql.Tx, name string) (*IdentityProviderGroup, error) + + // GetIdentityProviderGroupID return the ID of the identity_provider_group with the given key. + // generator: identity_provider_group ID + GetIdentityProviderGroupID(ctx context.Context, tx *sql.Tx, name string) (int64, error) + + // IdentityProviderGroupExists checks if a identity_provider_group with the given key exists. + // generator: identity_provider_group Exists + IdentityProviderGroupExists(ctx context.Context, tx *sql.Tx, name string) (bool, error) + + // CreateIdentityProviderGroup adds a new identity_provider_group to the database. + // generator: identity_provider_group Create + CreateIdentityProviderGroup(ctx context.Context, tx *sql.Tx, object IdentityProviderGroup) (int64, error) + + // DeleteIdentityProviderGroup deletes the identity_provider_group matching the given key parameters. + // generator: identity_provider_group DeleteOne-by-Name + DeleteIdentityProviderGroup(ctx context.Context, tx *sql.Tx, name string) error + + // UpdateIdentityProviderGroup updates the identity_provider_group matching the given key parameters. + // generator: identity_provider_group Update + UpdateIdentityProviderGroup(ctx context.Context, tx *sql.Tx, name string, object IdentityProviderGroup) error + + // RenameIdentityProviderGroup renames the identity_provider_group matching the given key parameters. + // generator: identity_provider_group Rename + RenameIdentityProviderGroup(ctx context.Context, tx *sql.Tx, name string, to string) error +} diff --git a/lxd/db/cluster/identity_provider_groups.mapper.go b/lxd/db/cluster/identity_provider_groups.mapper.go new file mode 100644 index 000000000000..bd520a912e07 --- /dev/null +++ b/lxd/db/cluster/identity_provider_groups.mapper.go @@ -0,0 +1,386 @@ +//go:build linux && cgo && !agent + +package cluster + +// The code below was generated by lxd-generate - DO NOT EDIT! + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/canonical/lxd/lxd/db/query" + "github.com/canonical/lxd/shared/api" +) + +var _ = api.ServerEnvironment{} + +var identityProviderGroupObjects = RegisterStmt(` +SELECT identity_provider_groups.id, identity_provider_groups.name + FROM identity_provider_groups + ORDER BY identity_provider_groups.name +`) + +var identityProviderGroupObjectsByID = RegisterStmt(` +SELECT identity_provider_groups.id, identity_provider_groups.name + FROM identity_provider_groups + WHERE ( identity_provider_groups.id = ? ) + ORDER BY identity_provider_groups.name +`) + +var identityProviderGroupObjectsByName = RegisterStmt(` +SELECT identity_provider_groups.id, identity_provider_groups.name + FROM identity_provider_groups + WHERE ( identity_provider_groups.name = ? ) + ORDER BY identity_provider_groups.name +`) + +var identityProviderGroupID = RegisterStmt(` +SELECT identity_provider_groups.id FROM identity_provider_groups + WHERE identity_provider_groups.name = ? +`) + +var identityProviderGroupCreate = RegisterStmt(` +INSERT INTO identity_provider_groups (name) + VALUES (?) +`) + +var identityProviderGroupDeleteByName = RegisterStmt(` +DELETE FROM identity_provider_groups WHERE name = ? +`) + +var identityProviderGroupUpdate = RegisterStmt(` +UPDATE identity_provider_groups + SET name = ? + WHERE id = ? +`) + +var identityProviderGroupRename = RegisterStmt(` +UPDATE identity_provider_groups SET name = ? WHERE name = ? +`) + +// identityProviderGroupColumns returns a string of column names to be used with a SELECT statement for the entity. +// Use this function when building statements to retrieve database entries matching the IdentityProviderGroup entity. +func identityProviderGroupColumns() string { + return "identity_providers_groups.id, identity_providers_groups.name" +} + +// getIdentityProviderGroups can be used to run handwritten sql.Stmts to return a slice of objects. +func getIdentityProviderGroups(ctx context.Context, stmt *sql.Stmt, args ...any) ([]IdentityProviderGroup, error) { + objects := make([]IdentityProviderGroup, 0) + + dest := func(scan func(dest ...any) error) error { + i := IdentityProviderGroup{} + err := scan(&i.ID, &i.Name) + if err != nil { + return err + } + + objects = append(objects, i) + + return nil + } + + err := query.SelectObjects(ctx, stmt, dest, args...) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"identity_providers_groups\" table: %w", err) + } + + return objects, nil +} + +// getIdentityProviderGroupsRaw can be used to run handwritten query strings to return a slice of objects. +func getIdentityProviderGroupsRaw(ctx context.Context, tx *sql.Tx, sql string, args ...any) ([]IdentityProviderGroup, error) { + objects := make([]IdentityProviderGroup, 0) + + dest := func(scan func(dest ...any) error) error { + i := IdentityProviderGroup{} + err := scan(&i.ID, &i.Name) + if err != nil { + return err + } + + objects = append(objects, i) + + return nil + } + + err := query.Scan(ctx, tx, sql, dest, args...) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"identity_providers_groups\" table: %w", err) + } + + return objects, nil +} + +// GetIdentityProviderGroups returns all available identity_provider_groups. +// generator: identity_provider_group GetMany +func GetIdentityProviderGroups(ctx context.Context, tx *sql.Tx, filters ...IdentityProviderGroupFilter) ([]IdentityProviderGroup, error) { + var err error + + // Result slice. + objects := make([]IdentityProviderGroup, 0) + + // Pick the prepared statement and arguments to use based on active criteria. + var sqlStmt *sql.Stmt + args := []any{} + queryParts := [2]string{} + + if len(filters) == 0 { + sqlStmt, err = Stmt(tx, identityProviderGroupObjects) + if err != nil { + return nil, fmt.Errorf("Failed to get \"identityProviderGroupObjects\" prepared statement: %w", err) + } + } + + for i, filter := range filters { + if filter.Name != nil && filter.ID == nil { + args = append(args, []any{filter.Name}...) + if len(filters) == 1 { + sqlStmt, err = Stmt(tx, identityProviderGroupObjectsByName) + if err != nil { + return nil, fmt.Errorf("Failed to get \"identityProviderGroupObjectsByName\" prepared statement: %w", err) + } + + break + } + + query, err := StmtString(identityProviderGroupObjectsByName) + if err != nil { + return nil, fmt.Errorf("Failed to get \"identityProviderGroupObjects\" prepared statement: %w", err) + } + + parts := strings.SplitN(query, "ORDER BY", 2) + if i == 0 { + copy(queryParts[:], parts) + continue + } + + _, where, _ := strings.Cut(parts[0], "WHERE") + queryParts[0] += "OR" + where + } else if filter.ID != nil && filter.Name == nil { + args = append(args, []any{filter.ID}...) + if len(filters) == 1 { + sqlStmt, err = Stmt(tx, identityProviderGroupObjectsByID) + if err != nil { + return nil, fmt.Errorf("Failed to get \"identityProviderGroupObjectsByID\" prepared statement: %w", err) + } + + break + } + + query, err := StmtString(identityProviderGroupObjectsByID) + if err != nil { + return nil, fmt.Errorf("Failed to get \"identityProviderGroupObjects\" prepared statement: %w", err) + } + + parts := strings.SplitN(query, "ORDER BY", 2) + if i == 0 { + copy(queryParts[:], parts) + continue + } + + _, where, _ := strings.Cut(parts[0], "WHERE") + queryParts[0] += "OR" + where + } else if filter.ID == nil && filter.Name == nil { + return nil, fmt.Errorf("Cannot filter on empty IdentityProviderGroupFilter") + } else { + return nil, fmt.Errorf("No statement exists for the given Filter") + } + } + + // Select. + if sqlStmt != nil { + objects, err = getIdentityProviderGroups(ctx, sqlStmt, args...) + } else { + queryStr := strings.Join(queryParts[:], "ORDER BY") + objects, err = getIdentityProviderGroupsRaw(ctx, tx, queryStr, args...) + } + + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"identity_providers_groups\" table: %w", err) + } + + return objects, nil +} + +// GetIdentityProviderGroup returns the identity_provider_group with the given key. +// generator: identity_provider_group GetOne +func GetIdentityProviderGroup(ctx context.Context, tx *sql.Tx, name string) (*IdentityProviderGroup, error) { + filter := IdentityProviderGroupFilter{} + filter.Name = &name + + objects, err := GetIdentityProviderGroups(ctx, tx, filter) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"identity_providers_groups\" table: %w", err) + } + + switch len(objects) { + case 0: + return nil, api.StatusErrorf(http.StatusNotFound, "IdentityProviderGroup not found") + case 1: + return &objects[0], nil + default: + return nil, fmt.Errorf("More than one \"identity_providers_groups\" entry matches") + } +} + +// GetIdentityProviderGroupID return the ID of the identity_provider_group with the given key. +// generator: identity_provider_group ID +func GetIdentityProviderGroupID(ctx context.Context, tx *sql.Tx, name string) (int64, error) { + stmt, err := Stmt(tx, identityProviderGroupID) + if err != nil { + return -1, fmt.Errorf("Failed to get \"identityProviderGroupID\" prepared statement: %w", err) + } + + row := stmt.QueryRowContext(ctx, name) + var id int64 + err = row.Scan(&id) + if errors.Is(err, sql.ErrNoRows) { + return -1, api.StatusErrorf(http.StatusNotFound, "IdentityProviderGroup not found") + } + + if err != nil { + return -1, fmt.Errorf("Failed to get \"identity_providers_groups\" ID: %w", err) + } + + return id, nil +} + +// IdentityProviderGroupExists checks if a identity_provider_group with the given key exists. +// generator: identity_provider_group Exists +func IdentityProviderGroupExists(ctx context.Context, tx *sql.Tx, name string) (bool, error) { + _, err := GetIdentityProviderGroupID(ctx, tx, name) + if err != nil { + if api.StatusErrorCheck(err, http.StatusNotFound) { + return false, nil + } + + return false, err + } + + return true, nil +} + +// CreateIdentityProviderGroup adds a new identity_provider_group to the database. +// generator: identity_provider_group Create +func CreateIdentityProviderGroup(ctx context.Context, tx *sql.Tx, object IdentityProviderGroup) (int64, error) { + // Check if a identity_provider_group with the same key exists. + exists, err := IdentityProviderGroupExists(ctx, tx, object.Name) + if err != nil { + return -1, fmt.Errorf("Failed to check for duplicates: %w", err) + } + + if exists { + return -1, api.StatusErrorf(http.StatusConflict, "This \"identity_providers_groups\" entry already exists") + } + + args := make([]any, 1) + + // Populate the statement arguments. + args[0] = object.Name + + // Prepared statement to use. + stmt, err := Stmt(tx, identityProviderGroupCreate) + if err != nil { + return -1, fmt.Errorf("Failed to get \"identityProviderGroupCreate\" prepared statement: %w", err) + } + + // Execute the statement. + result, err := stmt.Exec(args...) + if err != nil { + return -1, fmt.Errorf("Failed to create \"identity_providers_groups\" entry: %w", err) + } + + id, err := result.LastInsertId() + if err != nil { + return -1, fmt.Errorf("Failed to fetch \"identity_providers_groups\" entry ID: %w", err) + } + + return id, nil +} + +// DeleteIdentityProviderGroup deletes the identity_provider_group matching the given key parameters. +// generator: identity_provider_group DeleteOne-by-Name +func DeleteIdentityProviderGroup(ctx context.Context, tx *sql.Tx, name string) error { + stmt, err := Stmt(tx, identityProviderGroupDeleteByName) + if err != nil { + return fmt.Errorf("Failed to get \"identityProviderGroupDeleteByName\" prepared statement: %w", err) + } + + result, err := stmt.Exec(name) + if err != nil { + return fmt.Errorf("Delete \"identity_providers_groups\": %w", err) + } + + n, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("Fetch affected rows: %w", err) + } + + if n == 0 { + return api.StatusErrorf(http.StatusNotFound, "IdentityProviderGroup not found") + } else if n > 1 { + return fmt.Errorf("Query deleted %d IdentityProviderGroup rows instead of 1", n) + } + + return nil +} + +// UpdateIdentityProviderGroup updates the identity_provider_group matching the given key parameters. +// generator: identity_provider_group Update +func UpdateIdentityProviderGroup(ctx context.Context, tx *sql.Tx, name string, object IdentityProviderGroup) error { + id, err := GetIdentityProviderGroupID(ctx, tx, name) + if err != nil { + return err + } + + stmt, err := Stmt(tx, identityProviderGroupUpdate) + if err != nil { + return fmt.Errorf("Failed to get \"identityProviderGroupUpdate\" prepared statement: %w", err) + } + + result, err := stmt.Exec(object.Name, id) + if err != nil { + return fmt.Errorf("Update \"identity_providers_groups\" entry failed: %w", err) + } + + n, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("Fetch affected rows: %w", err) + } + + if n != 1 { + return fmt.Errorf("Query updated %d rows instead of 1", n) + } + + return nil +} + +// RenameIdentityProviderGroup renames the identity_provider_group matching the given key parameters. +// generator: identity_provider_group Rename +func RenameIdentityProviderGroup(ctx context.Context, tx *sql.Tx, name string, to string) error { + stmt, err := Stmt(tx, identityProviderGroupRename) + if err != nil { + return fmt.Errorf("Failed to get \"identityProviderGroupRename\" prepared statement: %w", err) + } + + result, err := stmt.Exec(to, name) + if err != nil { + return fmt.Errorf("Rename IdentityProviderGroup failed: %w", err) + } + + n, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("Fetch affected rows failed: %w", err) + } + + if n != 1 { + return fmt.Errorf("Query affected %d rows instead of 1", n) + } + + return nil +} diff --git a/lxd/db/cluster/permissions.interface.mapper.go b/lxd/db/cluster/permissions.interface.mapper.go new file mode 100644 index 000000000000..a2654146d21b --- /dev/null +++ b/lxd/db/cluster/permissions.interface.mapper.go @@ -0,0 +1,21 @@ +//go:build linux && cgo && !agent + +package cluster + +import ( + "context" + "database/sql" + + "github.com/canonical/lxd/lxd/auth" +) + +// PermissionGenerated is an interface of generated methods for Permission. +type PermissionGenerated interface { + // GetPermissions returns all available permissions. + // generator: permission GetMany + GetPermissions(ctx context.Context, tx *sql.Tx, filters ...PermissionFilter) ([]Permission, error) + + // GetPermission returns the permission with the given key. + // generator: permission GetOne + GetPermission(ctx context.Context, tx *sql.Tx, entitlement auth.Entitlement, entityType EntityType, entityID int) (*Permission, error) +} diff --git a/lxd/db/cluster/permissions.mapper.go b/lxd/db/cluster/permissions.mapper.go new file mode 100644 index 000000000000..1f290a4d315e --- /dev/null +++ b/lxd/db/cluster/permissions.mapper.go @@ -0,0 +1,269 @@ +//go:build linux && cgo && !agent + +package cluster + +// The code below was generated by lxd-generate - DO NOT EDIT! + +import ( + "context" + "database/sql" + "fmt" + "net/http" + "strings" + + "github.com/canonical/lxd/lxd/auth" + "github.com/canonical/lxd/lxd/db/query" + "github.com/canonical/lxd/shared/api" +) + +var _ = api.ServerEnvironment{} + +var permissionObjects = RegisterStmt(` +SELECT permissions.id, permissions.entitlement, permissions.entity_type, permissions.entity_id + FROM permissions + ORDER BY permissions.entitlement, permissions.entity_type, permissions.entity_id +`) + +var permissionObjectsByID = RegisterStmt(` +SELECT permissions.id, permissions.entitlement, permissions.entity_type, permissions.entity_id + FROM permissions + WHERE ( permissions.id = ? ) + ORDER BY permissions.entitlement, permissions.entity_type, permissions.entity_id +`) + +var permissionObjectsByEntityType = RegisterStmt(` +SELECT permissions.id, permissions.entitlement, permissions.entity_type, permissions.entity_id + FROM permissions + WHERE ( permissions.entity_type = ? ) + ORDER BY permissions.entitlement, permissions.entity_type, permissions.entity_id +`) + +var permissionObjectsByEntityTypeAndEntityID = RegisterStmt(` +SELECT permissions.id, permissions.entitlement, permissions.entity_type, permissions.entity_id + FROM permissions + WHERE ( permissions.entity_type = ? AND permissions.entity_id = ? ) + ORDER BY permissions.entitlement, permissions.entity_type, permissions.entity_id +`) + +var permissionObjectsByEntityTypeAndEntityIDAndEntitlement = RegisterStmt(` +SELECT permissions.id, permissions.entitlement, permissions.entity_type, permissions.entity_id + FROM permissions + WHERE ( permissions.entity_type = ? AND permissions.entity_id = ? AND permissions.entitlement = ? ) + ORDER BY permissions.entitlement, permissions.entity_type, permissions.entity_id +`) + +// permissionColumns returns a string of column names to be used with a SELECT statement for the entity. +// Use this function when building statements to retrieve database entries matching the Permission entity. +func permissionColumns() string { + return "permissions.id, permissions.entitlement, permissions.entity_type, permissions.entity_id" +} + +// getPermissions can be used to run handwritten sql.Stmts to return a slice of objects. +func getPermissions(ctx context.Context, stmt *sql.Stmt, args ...any) ([]Permission, error) { + objects := make([]Permission, 0) + + dest := func(scan func(dest ...any) error) error { + p := Permission{} + err := scan(&p.ID, &p.Entitlement, &p.EntityType, &p.EntityID) + if err != nil { + return err + } + + objects = append(objects, p) + + return nil + } + + err := query.SelectObjects(ctx, stmt, dest, args...) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"permissions\" table: %w", err) + } + + return objects, nil +} + +// getPermissionsRaw can be used to run handwritten query strings to return a slice of objects. +func getPermissionsRaw(ctx context.Context, tx *sql.Tx, sql string, args ...any) ([]Permission, error) { + objects := make([]Permission, 0) + + dest := func(scan func(dest ...any) error) error { + p := Permission{} + err := scan(&p.ID, &p.Entitlement, &p.EntityType, &p.EntityID) + if err != nil { + return err + } + + objects = append(objects, p) + + return nil + } + + err := query.Scan(ctx, tx, sql, dest, args...) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"permissions\" table: %w", err) + } + + return objects, nil +} + +// GetPermissions returns all available permissions. +// generator: permission GetMany +func GetPermissions(ctx context.Context, tx *sql.Tx, filters ...PermissionFilter) ([]Permission, error) { + var err error + + // Result slice. + objects := make([]Permission, 0) + + // Pick the prepared statement and arguments to use based on active criteria. + var sqlStmt *sql.Stmt + args := []any{} + queryParts := [2]string{} + + if len(filters) == 0 { + sqlStmt, err = Stmt(tx, permissionObjects) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjects\" prepared statement: %w", err) + } + } + + for i, filter := range filters { + if filter.EntityType != nil && filter.EntityID != nil && filter.Entitlement != nil && filter.ID == nil { + args = append(args, []any{filter.EntityType, filter.EntityID, filter.Entitlement}...) + if len(filters) == 1 { + sqlStmt, err = Stmt(tx, permissionObjectsByEntityTypeAndEntityIDAndEntitlement) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjectsByEntityTypeAndEntityIDAndEntitlement\" prepared statement: %w", err) + } + + break + } + + query, err := StmtString(permissionObjectsByEntityTypeAndEntityIDAndEntitlement) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjects\" prepared statement: %w", err) + } + + parts := strings.SplitN(query, "ORDER BY", 2) + if i == 0 { + copy(queryParts[:], parts) + continue + } + + _, where, _ := strings.Cut(parts[0], "WHERE") + queryParts[0] += "OR" + where + } else if filter.EntityType != nil && filter.EntityID != nil && filter.ID == nil && filter.Entitlement == nil { + args = append(args, []any{filter.EntityType, filter.EntityID}...) + if len(filters) == 1 { + sqlStmt, err = Stmt(tx, permissionObjectsByEntityTypeAndEntityID) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjectsByEntityTypeAndEntityID\" prepared statement: %w", err) + } + + break + } + + query, err := StmtString(permissionObjectsByEntityTypeAndEntityID) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjects\" prepared statement: %w", err) + } + + parts := strings.SplitN(query, "ORDER BY", 2) + if i == 0 { + copy(queryParts[:], parts) + continue + } + + _, where, _ := strings.Cut(parts[0], "WHERE") + queryParts[0] += "OR" + where + } else if filter.ID != nil && filter.Entitlement == nil && filter.EntityType == nil && filter.EntityID == nil { + args = append(args, []any{filter.ID}...) + if len(filters) == 1 { + sqlStmt, err = Stmt(tx, permissionObjectsByID) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjectsByID\" prepared statement: %w", err) + } + + break + } + + query, err := StmtString(permissionObjectsByID) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjects\" prepared statement: %w", err) + } + + parts := strings.SplitN(query, "ORDER BY", 2) + if i == 0 { + copy(queryParts[:], parts) + continue + } + + _, where, _ := strings.Cut(parts[0], "WHERE") + queryParts[0] += "OR" + where + } else if filter.EntityType != nil && filter.ID == nil && filter.Entitlement == nil && filter.EntityID == nil { + args = append(args, []any{filter.EntityType}...) + if len(filters) == 1 { + sqlStmt, err = Stmt(tx, permissionObjectsByEntityType) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjectsByEntityType\" prepared statement: %w", err) + } + + break + } + + query, err := StmtString(permissionObjectsByEntityType) + if err != nil { + return nil, fmt.Errorf("Failed to get \"permissionObjects\" prepared statement: %w", err) + } + + parts := strings.SplitN(query, "ORDER BY", 2) + if i == 0 { + copy(queryParts[:], parts) + continue + } + + _, where, _ := strings.Cut(parts[0], "WHERE") + queryParts[0] += "OR" + where + } else if filter.ID == nil && filter.Entitlement == nil && filter.EntityType == nil && filter.EntityID == nil { + return nil, fmt.Errorf("Cannot filter on empty PermissionFilter") + } else { + return nil, fmt.Errorf("No statement exists for the given Filter") + } + } + + // Select. + if sqlStmt != nil { + objects, err = getPermissions(ctx, sqlStmt, args...) + } else { + queryStr := strings.Join(queryParts[:], "ORDER BY") + objects, err = getPermissionsRaw(ctx, tx, queryStr, args...) + } + + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"permissions\" table: %w", err) + } + + return objects, nil +} + +// GetPermission returns the permission with the given key. +// generator: permission GetOne +func GetPermission(ctx context.Context, tx *sql.Tx, entitlement auth.Entitlement, entityType EntityType, entityID int) (*Permission, error) { + filter := PermissionFilter{} + filter.Entitlement = &entitlement + filter.EntityType = &entityType + filter.EntityID = &entityID + + objects, err := GetPermissions(ctx, tx, filter) + if err != nil { + return nil, fmt.Errorf("Failed to fetch from \"permissions\" table: %w", err) + } + + switch len(objects) { + case 0: + return nil, api.StatusErrorf(http.StatusNotFound, "Permission not found") + case 1: + return &objects[0], nil + default: + return nil, fmt.Errorf("More than one \"permissions\" entry matches") + } +} From d33b148f00f447fc78687a28b5c58b1c48970030 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:51:25 +0000 Subject: [PATCH 07/34] shared/api: Adds auth API types. Signed-off-by: Mark Laing --- shared/api/auth.go | 161 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/shared/api/auth.go b/shared/api/auth.go index 90c7bcbbf407..96c5cce677b2 100644 --- a/shared/api/auth.go +++ b/shared/api/auth.go @@ -24,3 +24,164 @@ const ( // IdentityTypeOIDCClient represents an identity that authenticates with OIDC.. IdentityTypeOIDCClient = "OIDC client" ) + +// Identity is the type for an authenticated party that can make requests to the HTTPS API. +// +// swagger:model +// +// API extension: authorization_apis. +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"` +} + +// IdentityInfo expands an Identity to include group membership. +// +// swagger:model +// +// API extension: authorization_apis. +type IdentityInfo struct { + IdentityPut `yaml:",inline"` + Identity `yaml:",inline"` +} + +// IdentityPut contains the editable fields of an IdentityInfo. +// +// swagger:model +// +// API extension: authorization_apis. +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"` +} + +// AuthGroup is the type for a LXD group. +// +// swagger:model +// +// API extension: authorization_apis. +type AuthGroup struct { + AuthGroupsPost `yaml:",inline"` + + // Identities are the identities that are members of the group. + Identities []Identity `json:"identities" yaml:"identities"` + + // IdentityProviderGroups are a list of groups from the IdP whose mapping + // includes this group. + // Example: ["sales", "operations"] + IdentityProviderGroups []string `json:"identity_provider_groups" yaml:"identity_provider_groups"` +} + +// AuthGroupsPost is used for creating a new group. +// +// swagger:model +// +// API extension: authorization_apis. +type AuthGroupsPost struct { + AuthGroupPost `yaml:",inline"` + AuthGroupPut `yaml:",inline"` +} + +// AuthGroupPost is used for renaming a group. +// +// swagger:model +// +// API extension: authorization_apis. +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: authorization_apis. +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: authorization_apis. +type IdentityProviderGroup struct { + IdentityProviderGroupPost `yaml:",inline"` + IdentityProviderGroupPut `yaml:",inline"` +} + +// IdentityProviderGroupPost is used for renaming an IdentityProviderGroup. +// +// swagger:model +// +// API extension: authorization_apis. +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: authorization_apis. +type IdentityProviderGroupPut struct { + // Groups are the groups the IdP group resolves to. + // Example: ["foo", "bar"] + Groups []string `json:"groups" yaml:"groups"` +} + +// Permission represents a permission that may be granted to a group. +// +// swagger:model +// +// API extension: authorization_apis. +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 +// +// API extension: authorization_apis. +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"` +} From f584c6ef1d7f971ae9ff08b651a42d8da7398926 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:51:37 +0000 Subject: [PATCH 08/34] lxd/db/cluster: Adds database methods for auth groups. Signed-off-by: Mark Laing --- lxd/db/cluster/auth_groups.go | 270 ++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) diff --git a/lxd/db/cluster/auth_groups.go b/lxd/db/cluster/auth_groups.go index 2095a0a915e4..1561cc0c76f9 100644 --- a/lxd/db/cluster/auth_groups.go +++ b/lxd/db/cluster/auth_groups.go @@ -1,5 +1,15 @@ package cluster +import ( + "context" + "database/sql" + "fmt" + + "github.com/canonical/lxd/lxd/db/query" + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/entity" +) + // Code generation directives. // //go:generate -command mapper lxd-generate db mapper -t auth_groups.mapper.go @@ -35,3 +45,263 @@ type AuthGroupFilter struct { ID *int Name *string } + +// ToAPI converts the Group to an api.AuthGroup, making extra database queries as necessary. +func (g *AuthGroup) ToAPI(ctx context.Context, tx *sql.Tx) (*api.AuthGroup, error) { + group := &api.AuthGroup{ + AuthGroupsPost: api.AuthGroupsPost{ + AuthGroupPost: api.AuthGroupPost{Name: g.Name}, + AuthGroupPut: api.AuthGroupPut{Description: g.Description}, + }, + } + + permissions, err := GetPermissionsByAuthGroupID(ctx, tx, g.ID) + if err != nil { + return nil, err + } + + entityURLs, err := GetPermissionEntityURLs(ctx, tx, permissions) + if err != nil { + return nil, err + } + + apiPermissions := make([]api.Permission, 0, len(permissions)) + for _, p := range permissions { + entityURLs, ok := entityURLs[entity.Type(p.EntityType)] + if !ok { + return nil, fmt.Errorf("Entity URLs missing for permissions with entity type %q", p.EntityType) + } + + u, ok := entityURLs[p.EntityID] + if !ok { + return nil, fmt.Errorf("Entity URL missing for permission with entity type %q and entity ID `%d`", p.EntityType, p.EntityID) + } + + apiPermissions = append(apiPermissions, api.Permission{ + EntityType: string(p.EntityType), + EntityReference: u.String(), + Entitlement: string(p.Entitlement), + }) + } + + group.Permissions = apiPermissions + + identities, err := GetIdentitiesByAuthGroupID(ctx, tx, g.ID) + if err != nil { + return nil, err + } + + group.Identities = make([]api.Identity, 0, len(identities)) + for _, identity := range identities { + group.Identities = append(group.Identities, api.Identity{ + AuthenticationMethod: string(identity.AuthMethod), + Type: string(identity.Type), + Identifier: identity.Identifier, + Name: identity.Name, + }) + } + + identityProviderGroups, err := GetIdentityProviderGroupsByGroupID(ctx, tx, g.ID) + if err != nil { + return nil, err + } + + for _, idpGroup := range identityProviderGroups { + group.IdentityProviderGroups = append(group.IdentityProviderGroups, idpGroup.Name) + } + + return group, nil +} + +// GetIdentitiesByAuthGroupID returns the identities that are members of the group with the given ID. +func GetIdentitiesByAuthGroupID(ctx context.Context, tx *sql.Tx, groupID int) ([]Identity, error) { + stmt := ` +SELECT identities.id, identities.auth_method, identities.type, identities.identifier, identities.name, identities.metadata +FROM identities +JOIN identities_auth_groups ON identities.id = identities_auth_groups.identity_id +WHERE identities_auth_groups.auth_group_id = ?` + + var result []Identity + dest := func(scan func(dest ...any) error) error { + i := Identity{} + err := scan(&i.ID, &i.AuthMethod, &i.Type, &i.Identifier, &i.Name, &i.Metadata) + if err != nil { + return err + } + + result = append(result, i) + + return nil + } + + err := query.Scan(ctx, tx, stmt, dest, groupID) + if err != nil { + return nil, fmt.Errorf("Failed to get identities for the group with ID `%d`: %w", groupID, err) + } + + return result, nil +} + +// GetAllIdentitiesByAuthGroupIDs returns a map of group IDs to the identities that are members of the group with that ID. +func GetAllIdentitiesByAuthGroupIDs(ctx context.Context, tx *sql.Tx) (map[int][]Identity, error) { + stmt := ` +SELECT identities_auth_groups.auth_group_id, identities.id, identities.auth_method, identities.type, identities.identifier, identities.name, identities.metadata +FROM identities +JOIN identities_auth_groups ON identities.id = identities_auth_groups.identity_id` + + result := make(map[int][]Identity) + dest := func(scan func(dest ...any) error) error { + var groupID int + i := Identity{} + err := scan(&groupID, &i.ID, &i.AuthMethod, &i.Type, &i.Identifier, &i.Name, &i.Metadata) + if err != nil { + return err + } + + result[groupID] = append(result[groupID], i) + + return nil + } + + err := query.Scan(ctx, tx, stmt, dest) + if err != nil { + return nil, fmt.Errorf("Failed to get identities for all groups: %w", err) + } + + return result, nil +} + +// GetIdentityProviderGroupsByGroupID returns the identity provider groups that map to the group with the given ID. +func GetIdentityProviderGroupsByGroupID(ctx context.Context, tx *sql.Tx, groupID int) ([]IdentityProviderGroup, error) { + stmt := ` +SELECT identity_provider_groups.id, identity_provider_groups.name +FROM identity_provider_groups +JOIN auth_groups_identity_provider_groups ON identity_provider_groups.id = auth_groups_identity_provider_groups.identity_provider_group_id +WHERE auth_groups_identity_provider_groups.auth_group_id = ?` + + var result []IdentityProviderGroup + dest := func(scan func(dest ...any) error) error { + i := IdentityProviderGroup{} + err := scan(&i.ID, &i.Name) + if err != nil { + return err + } + + result = append(result, i) + + return nil + } + + err := query.Scan(ctx, tx, stmt, dest, groupID) + if err != nil { + return nil, fmt.Errorf("Failed to get identity provider groups for the group with ID `%d`: %w", groupID, err) + } + + return result, nil +} + +// GetAllIdentityProviderGroupsByGroupIDs returns a map of group IDs to the IdentityProviderGroups that map to the group with that ID. +func GetAllIdentityProviderGroupsByGroupIDs(ctx context.Context, tx *sql.Tx) (map[int][]IdentityProviderGroup, error) { + stmt := ` +SELECT auth_groups_identity_provider_groups.auth_group_id, identity_provider_groups.id, identity_provider_groups.name +FROM identity_provider_groups +JOIN auth_groups_identity_provider_groups ON identity_provider_groups.id = auth_groups_identity_provider_groups.identity_provider_group_id` + + result := make(map[int][]IdentityProviderGroup) + dest := func(scan func(dest ...any) error) error { + var groupID int + i := IdentityProviderGroup{} + err := scan(&groupID, &i.ID, &i.Name) + if err != nil { + return err + } + + result[groupID] = append(result[groupID], i) + + return nil + } + + err := query.Scan(ctx, tx, stmt, dest) + if err != nil { + return nil, fmt.Errorf("Failed to get identity provider groups for all groups: %w", err) + } + + return result, nil +} + +// GetPermissionsByAuthGroupID returns the permissions that belong to the group with the given ID. +func GetPermissionsByAuthGroupID(ctx context.Context, tx *sql.Tx, groupID int) ([]Permission, error) { + stmt := fmt.Sprintf(` +SELECT %s FROM permissions +JOIN auth_groups_permissions ON permissions.id = auth_groups_permissions.permission_id +WHERE auth_groups_permissions.auth_group_id = ?`, permissionColumns()) + + var result []Permission + dest := func(scan func(dest ...any) error) error { + p := Permission{} + err := scan(&p.ID, &p.Entitlement, &p.EntityType, &p.EntityID) + if err != nil { + return err + } + + result = append(result, p) + return nil + } + + err := query.Scan(ctx, tx, stmt, dest, groupID) + if err != nil { + return nil, fmt.Errorf("Failed to get permissions for the group with ID `%d`: %w", groupID, err) + } + + return result, nil +} + +// GetAllPermissionsByAuthGroupIDs returns a map of group ID to the permissions that belong to the auth group with that ID. +func GetAllPermissionsByAuthGroupIDs(ctx context.Context, tx *sql.Tx) (map[int][]Permission, error) { + stmt := fmt.Sprintf(` +SELECT auth_groups_permissions.auth_group_id, %s +FROM permissions +JOIN auth_groups_permissions ON permissions.id = auth_groups_permissions.permission_id`, permissionColumns()) + + result := make(map[int][]Permission) + dest := func(scan func(dest ...any) error) error { + var groupID int + p := Permission{} + err := scan(&groupID, &p.ID, &p.Entitlement, &p.EntityType, &p.EntityID) + if err != nil { + return err + } + + result[groupID] = append(result[groupID], p) + return nil + } + + err := query.Scan(ctx, tx, stmt, dest) + if err != nil { + return nil, fmt.Errorf("Failed to get permissions for all groups: %w", err) + } + + return result, nil +} + +// SetAuthGroupPermissions deletes all auth_group -> permission mappings from the `auth_group_permissions` table +// where the group ID is equal to the given value. Then it inserts a new row for each given permission ID. +func SetAuthGroupPermissions(ctx context.Context, tx *sql.Tx, groupID int, permissionIDs []int) error { + _, err := tx.ExecContext(ctx, `DELETE FROM auth_groups_permissions WHERE auth_group_id = ?`, groupID) + if err != nil { + return fmt.Errorf("Failed to delete existing permissions for group with ID `%d`: %w", groupID, err) + } + + if len(permissionIDs) == 0 { + return nil + } + + for _, permissionID := range permissionIDs { + _, err := tx.ExecContext(ctx, `INSERT INTO auth_groups_permissions (auth_group_id, permission_id) VALUES (?, ?);`, groupID, permissionID) + if err != nil { + return fmt.Errorf("Failed to write group permissions: %w", err) + } + } + + return nil +} From 3175a3ba8df3046fd22459a350c4fa1a0fdba2cb Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:52:16 +0000 Subject: [PATCH 09/34] lxd/db/cluster: Adds methods for populating api.IdentityInfo data. Signed-off-by: Mark Laing --- lxd/db/cluster/identities.go | 165 +++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/lxd/db/cluster/identities.go b/lxd/db/cluster/identities.go index 0e8012b801bb..9f7bf734587a 100644 --- a/lxd/db/cluster/identities.go +++ b/lxd/db/cluster/identities.go @@ -3,14 +3,19 @@ package cluster import ( + "context" "crypto/x509" + "database/sql" "database/sql/driver" "encoding/json" "encoding/pem" "errors" "fmt" + "net/http" + "strings" "github.com/canonical/lxd/lxd/certificate" + "github.com/canonical/lxd/lxd/db/query" "github.com/canonical/lxd/lxd/identity" "github.com/canonical/lxd/shared/api" ) @@ -284,3 +289,163 @@ func (i Identity) Subject() (string, error) { return metadata.Subject, nil } + +// ToAPIInfo converts an Identity to an api.IdentityInfo, executing database queries as necessary. +func (i *Identity) ToAPIInfo(ctx context.Context, tx *sql.Tx) (*api.IdentityInfo, error) { + groups, err := GetAuthGroupsByIdentityID(ctx, tx, i.ID) + if err != nil { + return nil, err + } + + groupNames := make([]string, 0, len(groups)) + for _, group := range groups { + groupNames = append(groupNames, group.Name) + } + + return &api.IdentityInfo{ + Identity: api.Identity{ + AuthenticationMethod: string(i.AuthMethod), + Type: string(i.Type), + Identifier: i.Identifier, + Name: i.Name, + }, + IdentityPut: api.IdentityPut{ + Groups: groupNames, + }, + }, nil +} + +// GetAuthGroupsByIdentityID returns a slice of groups that the identity with the given ID is a member of. +func GetAuthGroupsByIdentityID(ctx context.Context, tx *sql.Tx, identityID int) ([]AuthGroup, error) { + stmt := ` +SELECT auth_groups.id, auth_groups.name, auth_groups.description +FROM auth_groups +JOIN identities_auth_groups ON auth_groups.id = identities_auth_groups.auth_group_id +WHERE identities_auth_groups.identity_id = ?` + + var result []AuthGroup + dest := func(scan func(dest ...any) error) error { + g := AuthGroup{} + err := scan(&g.ID, &g.Name, &g.Description) + if err != nil { + return err + } + + result = append(result, g) + + return nil + } + + err := query.Scan(ctx, tx, stmt, dest, identityID) + if err != nil { + return nil, fmt.Errorf("Failed to get groups for identity with ID `%d`: %w", identityID, err) + } + + return result, nil +} + +// GetAllAuthGroupsByIdentityIDs returns a map of identity ID to slice of groups the identity with that ID is a member of. +func GetAllAuthGroupsByIdentityIDs(ctx context.Context, tx *sql.Tx) (map[int][]AuthGroup, error) { + stmt := ` +SELECT identities_auth_groups.identity_id, auth_groups.id, auth_groups.name, auth_groups.description +FROM auth_groups +JOIN identities_auth_groups ON auth_groups.id = identities_auth_groups.auth_group_id` + + result := make(map[int][]AuthGroup) + dest := func(scan func(dest ...any) error) error { + var identityID int + g := AuthGroup{} + err := scan(&identityID, &g.ID, &g.Name, &g.Description) + if err != nil { + return err + } + + result[identityID] = append(result[identityID], g) + + return nil + } + + err := query.Scan(ctx, tx, stmt, dest) + if err != nil { + return nil, fmt.Errorf("Failed to get identities for all groups: %w", err) + } + + return result, nil +} + +// GetIdentityByNameOrIdentifier attempts to get an identity by the authentication method and identifier. If that fails +// it will try to use the nameOrID argument as a name and will return the result only if the query matches a single Identity. +// It will return an api.StatusError with http.StatusNotFound if none are found or http.StatusBadRequest if multiple are found. +func GetIdentityByNameOrIdentifier(ctx context.Context, tx *sql.Tx, authenticationMethod string, nameOrID string) (*Identity, error) { + id, err := GetIdentity(ctx, tx, AuthMethod(authenticationMethod), nameOrID) + if err != nil && !api.StatusErrorCheck(err, http.StatusNotFound) { + return nil, err + } else if err != nil { + dbAuthMethod := AuthMethod(authenticationMethod) + identities, err := GetIdentitys(ctx, tx, IdentityFilter{ + AuthMethod: &dbAuthMethod, + Name: &nameOrID, + }) + if err != nil { + return nil, err + } + + if len(identities) == 0 { + return nil, api.StatusErrorf(http.StatusNotFound, "No identity found with name or identifier %q", nameOrID) + } else if len(identities) > 1 { + return nil, api.StatusErrorf(http.StatusBadRequest, "More than one identity found with name %q", nameOrID) + } + + id = &identities[0] + } + + return id, nil +} + +// SetIdentityAuthGroups deletes all auth_group -> identity mappings from the `identities_auth_groups` table +// where the identity ID is equal to the given value. Then it inserts new associations into the table where the +// group IDs correspond to the given group names. +func SetIdentityAuthGroups(ctx context.Context, tx *sql.Tx, identityID int, groupNames []string) error { + _, err := tx.ExecContext(ctx, `DELETE FROM identities_auth_groups WHERE identity_id = ?`, identityID) + if err != nil { + return fmt.Errorf("Failed to delete existing groups for identity with ID `%d`: %w", identityID, err) + } + + if len(groupNames) == 0 { + return nil + } + + args := []any{identityID} + var builder strings.Builder + builder.WriteString(` +INSERT INTO identities_auth_groups (identity_id, auth_group_id) +SELECT ?, auth_groups.id +FROM auth_groups +WHERE auth_groups.name IN ( +`) + for i, groupName := range groupNames { + if i == len(groupNames)-1 { + builder.WriteString(`?)`) + } else { + builder.WriteString(`?, `) + } + + args = append(args, groupName) + } + + res, err := tx.ExecContext(ctx, builder.String(), args...) + if err != nil { + return fmt.Errorf("Failed to write identity provider group mappings: %w", err) + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("Failed to check validity of identity provider group mapping creation: %w", err) + } + + if int(rowsAffected) != len(groupNames) { + return fmt.Errorf("Failed to write expected number of rows to identity provider group association table (expected %d, got %d)", len(groupNames), rowsAffected) + } + + return nil +} From 40925ddd3054e0c746a6113c8bfb0114a3dcd48a Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:52:38 +0000 Subject: [PATCH 10/34] lxd/db/cluster: Adds methods for populating api.IdentityProviderGroup data. Signed-off-by: Mark Laing --- lxd/db/cluster/identity_provider_groups.go | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/lxd/db/cluster/identity_provider_groups.go b/lxd/db/cluster/identity_provider_groups.go index b3d3e9dd2212..3c122562c45a 100644 --- a/lxd/db/cluster/identity_provider_groups.go +++ b/lxd/db/cluster/identity_provider_groups.go @@ -1,5 +1,15 @@ package cluster +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/canonical/lxd/lxd/db/query" + "github.com/canonical/lxd/shared/api" +) + // Code generation directives. // //go:generate -command mapper lxd-generate db mapper -t identity_provider_groups.mapper.go @@ -34,3 +44,100 @@ type IdentityProviderGroupFilter struct { ID *int Name *string } + +// ToAPI converts the IdentityProviderGroup to an api.IdentityProviderGroup, making more database calls as necessary. +func (i *IdentityProviderGroup) ToAPI(ctx context.Context, tx *sql.Tx) (*api.IdentityProviderGroup, error) { + idpGroup := &api.IdentityProviderGroup{ + IdentityProviderGroupPost: api.IdentityProviderGroupPost{Name: i.Name}, + } + + groups, err := GetAuthGroupsByIdentityProviderGroupID(ctx, tx, i.ID) + if err != nil { + return nil, err + } + + groupNames := make([]string, 0, len(groups)) + for _, group := range groups { + groupNames = append(groupNames, group.Name) + } + + idpGroup.Groups = groupNames + return idpGroup, nil +} + +// GetAuthGroupsByIdentityProviderGroupID returns a list of a groups that the identity provider group with the given ID. +func GetAuthGroupsByIdentityProviderGroupID(ctx context.Context, tx *sql.Tx, idpGroupID int) ([]AuthGroup, error) { + stmt := ` +SELECT auth_groups.id, auth_groups.name, auth_groups.description +FROM auth_groups_identity_provider_groups +JOIN auth_groups ON auth_groups_identity_provider_groups.auth_group_id = auth_groups.id +WHERE auth_groups_identity_provider_groups.identity_provider_group_id = ?` + + var result []AuthGroup + dest := func(scan func(dest ...any) error) error { + g := AuthGroup{} + err := scan(&g.ID, &g.Name, &g.Description) + if err != nil { + return err + } + + result = append(result, g) + + return nil + } + + err := query.Scan(ctx, tx, stmt, dest, idpGroupID) + if err != nil { + return nil, fmt.Errorf("Failed to get group mappings for identity provider group with ID `%d`: %w", idpGroupID, err) + } + + return result, nil +} + +// SetIdentityProviderGroupMapping deletes all auth_group -> identity_provider_group mappings from the `ath_groups_identity_provider_groups` table +// where the identity provider group ID is equal to the given value. Then it inserts new assocations into the table where the +// group IDs correspond to the given group names. +func SetIdentityProviderGroupMapping(ctx context.Context, tx *sql.Tx, identityProviderGroupID int, groupNames []string) error { + _, err := tx.ExecContext(ctx, `DELETE FROM auth_groups_identity_provider_groups WHERE identity_provider_group_id = ?`, identityProviderGroupID) + if err != nil { + return fmt.Errorf("Failed to delete existing identity provider group mappings: %w", err) + } + + if len(groupNames) == 0 { + return nil + } + + args := []any{identityProviderGroupID} + var builder strings.Builder + builder.WriteString(` +INSERT INTO auth_groups_identity_provider_groups (auth_group_id, identity_provider_group_id) +SELECT ?, auth_groups.id +FROM auth_groups +WHERE auth_groups.name IN ( +`) + for i, groupName := range groupNames { + if i == len(groupNames)-1 { + builder.WriteString(`?)`) + } else { + builder.WriteString(`?, `) + } + + args = append(args, groupName) + } + + res, err := tx.ExecContext(ctx, builder.String(), args...) + if err != nil { + return fmt.Errorf("Failed to write identity provider group mappings: %w", err) + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("Failed to check validity of identity provider group mapping creation: %w", err) + } + + if int(rowsAffected) != len(groupNames) { + return fmt.Errorf("Failed to write expected number of rows to identity provider group association table (expected %d, got %d)", len(groupNames), rowsAffected) + } + + return nil +} From 6c9a474fe2d8fd2587e063828acd94f25e9804f9 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:53:21 +0000 Subject: [PATCH 11/34] lxd/db/cluster: Adds methods for populating api.PermissionInfo data. Signed-off-by: Mark Laing --- lxd/db/cluster/permissions.go | 78 +++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/lxd/db/cluster/permissions.go b/lxd/db/cluster/permissions.go index 5170d45086aa..b3621f92c111 100644 --- a/lxd/db/cluster/permissions.go +++ b/lxd/db/cluster/permissions.go @@ -1,7 +1,14 @@ package cluster import ( + "context" + "database/sql" + "fmt" + "github.com/canonical/lxd/lxd/auth" + "github.com/canonical/lxd/lxd/db/query" + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/entity" ) // Code generation directives. @@ -33,3 +40,74 @@ type PermissionFilter struct { EntityType *EntityType EntityID *int } + +// GetPermissionEntityURLs accepts a slice of Permission and returns a map of entity.Type, to entity ID, to api.URL. +// The returned map contains the URL of the entity of each given permission. It is used for populating api.Permission. +func GetPermissionEntityURLs(ctx context.Context, tx *sql.Tx, permissions []Permission) (map[entity.Type]map[int]*api.URL, error) { + // To make as few calls as possible, categorize the permissions by entity type. + permissionsByEntityType := map[EntityType][]Permission{} + for _, permission := range permissions { + permissionsByEntityType[permission.EntityType] = append(permissionsByEntityType[permission.EntityType], permission) + } + + // For each entity type, if there is only on permission for the entity type, we'll get the URL by its entity type and ID. + // If there are multiple permissions for the entity type, append the entity type to a list for later use. + entityURLs := make(map[entity.Type]map[int]*api.URL) + var entityTypes []entity.Type + for entityType, permissions := range permissionsByEntityType { + if len(permissions) > 1 { + entityTypes = append(entityTypes, entity.Type(entityType)) + continue + } + + u, err := GetEntityURL(ctx, tx, entity.Type(entityType), permissions[0].EntityID) + if err != nil { + return nil, err + } + + entityURLs[entity.Type(entityType)] = make(map[int]*api.URL) + entityURLs[entity.Type(entityType)][permissions[0].EntityID] = u + } + + // If there are any entity types with multiple permissions, get all URLs for those entities. + if len(entityTypes) > 0 { + entityURLsAll, err := GetEntityURLs(ctx, tx, "", entityTypes...) + if err != nil { + return nil, err + } + + for k, v := range entityURLsAll { + entityURLs[k] = v + } + } + + return entityURLs, nil +} + +// GetAllAuthGroupsByPermissionID returns a map of all permission IDs to a slice of groups that have that permission. +func GetAllAuthGroupsByPermissionID(ctx context.Context, tx *sql.Tx) (map[int][]AuthGroup, error) { + stmt := ` +SELECT auth_groups_permissions.permission_id, auth_groups.id, auth_groups.name, auth_groups.description +FROM auth_groups +JOIN auth_groups_permissions ON auth_groups.id = auth_groups_permissions.auth_group_id` + + result := make(map[int][]AuthGroup) + dest := func(scan func(dest ...any) error) error { + var permissionID int + p := AuthGroup{} + err := scan(&permissionID, &p.ID, &p.Name, &p.Description) + if err != nil { + return err + } + + result[permissionID] = append(result[permissionID], p) + return nil + } + + err := query.Scan(ctx, tx, stmt, dest) + if err != nil { + return nil, fmt.Errorf("Failed to get permissions for all groups: %w", err) + } + + return result, nil +} From b4c0415f6fc25e03062612d02b0cb88e10e823fb Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:20:03 +0000 Subject: [PATCH 12/34] lxd/db/cluster: Add auth types to EntityType scanner/valuer implementation. These constants were mistakenly added in 33479dbd13a61b69546cd13b84ea2b2244946c0d. They are now required by the authorization APIs, so this commit adds them to the scanner/valuer implementation and additionally renames `entityTypeGroup` to `entityTypeAuthGroup`. Signed-off-by: Mark Laing --- lxd/db/cluster/entities.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lxd/db/cluster/entities.go b/lxd/db/cluster/entities.go index bfa5470ccaff..0e871d0da795 100644 --- a/lxd/db/cluster/entities.go +++ b/lxd/db/cluster/entities.go @@ -48,7 +48,7 @@ const ( entityTypeNetworkZone int64 = 19 entityTypeImageAlias int64 = 20 entityTypeServer int64 = 21 - entityTypeGroup int64 = 22 + entityTypeAuthGroup int64 = 22 entityTypeIdentityProviderGroup int64 = 23 entityTypeIdentity int64 = 24 ) @@ -118,6 +118,12 @@ func (e *EntityType) Scan(value any) error { *e = EntityType(entity.TypeImageAlias) case entityTypeServer: *e = EntityType(entity.TypeServer) + case entityTypeAuthGroup: + *e = EntityType(entity.TypeAuthGroup) + case entityTypeIdentityProviderGroup: + *e = EntityType(entity.TypeIdentityProviderGroup) + case entityTypeIdentity: + *e = EntityType(entity.TypeIdentity) default: return fmt.Errorf("Unknown entity type %d", entityTypeInt) } @@ -174,6 +180,12 @@ func (e EntityType) Value() (driver.Value, error) { return entityTypeImageAlias, nil case EntityType(entity.TypeServer): return entityTypeServer, nil + case EntityType(entity.TypeAuthGroup): + return entityTypeAuthGroup, nil + case EntityType(entity.TypeIdentityProviderGroup): + return entityTypeIdentityProviderGroup, nil + case EntityType(entity.TypeIdentity): + return entityTypeIdentity, nil default: return nil, fmt.Errorf("Unknown entity type %q", e) } From d555ac4581025f5a0efc470dd3c1172cd035c694 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 14:56:44 +0000 Subject: [PATCH 13/34] lxd/db/cluster: Add queries for generating URLs for new entity types. Signed-off-by: Mark Laing --- lxd/db/cluster/entities.go | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/lxd/db/cluster/entities.go b/lxd/db/cluster/entities.go index 0e871d0da795..f1622a28b227 100644 --- a/lxd/db/cluster/entities.go +++ b/lxd/db/cluster/entities.go @@ -468,6 +468,42 @@ var storageBucketEntityByID = fmt.Sprintf(`%s WHERE storage_buckets.id = ?`, sto // storageBucketEntities returns all entities of type entity.TypeStorageBucket in a particular project. var storageBucketEntitiesByProjectName = fmt.Sprintf(`%s WHERE projects.name = ?`, storageBucketEntities) +// authGroupEntities returns all entities of type entity.TypeGroup. +var authGroupEntities = fmt.Sprintf(`SELECT %d, auth_groups.id, '', '', json_array(auth_groups.name) FROM auth_groups`, entityTypeAuthGroup) + +// authGroupEntityByID gets the entity of type entity.TypeGroup with a particular ID. +var authGroupEntityByID = fmt.Sprintf(`%s WHERE auth_groups.id = ?`, authGroupEntities) + +// identityProviderGroupEntities returns all entities of type entity.TypeIdentityProviderGroup. +var identityProviderGroupEntities = fmt.Sprintf(`SELECT %d, identity_provider_groups.id, '', '', json_array(identity_provider_groups.name) FROM identity_provider_groups`, entityTypeIdentityProviderGroup) + +// identityProviderGroupByEntityID gets the entity of type entity.TypeIdentityProviderGroup with a particular ID. +var identityProviderGroupEntityByID = fmt.Sprintf(`%s WHERE identity_provider_groups.id = ?`, identityProviderGroupEntities) + +// identityEntities returns all entities of type entity.TypeIdentity. +var identityEntities = fmt.Sprintf(` +SELECT + %d, + identities.id, + '', + '', + json_array( + CASE identities.auth_method + WHEN %d THEN '%s' + WHEN %d THEN '%s' + END, + identities.identifier + ) +FROM identities +`, + entityTypeIdentity, + authMethodTLS, api.AuthenticationMethodTLS, + authMethodOIDC, api.AuthenticationMethodOIDC, +) + +// identityEntityByID gets the entity of type entity.TypeIdentity with a particular ID. +var identityEntityByID = fmt.Sprintf(`%s WHERE identities.id = ?`, identityEntities) + // entityStatementsAll is a map of entity type to the statement which queries for all URL information for entities of that type. var entityStatementsAll = map[entity.Type]string{ entity.TypeContainer: containerEntities, @@ -491,6 +527,9 @@ var entityStatementsAll = map[entity.Type]string{ entity.TypeStorageBucket: storageBucketEntities, entity.TypeImageAlias: imageAliasEntities, entity.TypeNetworkZone: networkZoneEntities, + entity.TypeAuthGroup: authGroupEntities, + entity.TypeIdentityProviderGroup: identityProviderGroupEntities, + entity.TypeIdentity: identityEntities, } // entityStatementsByID is a map of entity type to the statement which queries for all URL information for a single entity of that type with a given ID. @@ -516,6 +555,9 @@ var entityStatementsByID = map[entity.Type]string{ entity.TypeStorageBucket: storageBucketEntityByID, entity.TypeImageAlias: imageAliasEntityByID, entity.TypeNetworkZone: networkZoneEntityByID, + entity.TypeAuthGroup: authGroupEntityByID, + entity.TypeIdentityProviderGroup: identityProviderGroupEntityByID, + entity.TypeIdentity: identityEntityByID, } // entityStatementsByProjectName is a map of entity type to the statement which queries for all URL information for all entities of that type within a given project. From 76af03caf0bfaf5a89d1905a6f58a454c81af34c Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:15:06 +0000 Subject: [PATCH 14/34] lxd/db/cluster: Export the EntityRef type. This will be returned in an exported method which will resolve URLs to IDs. Signed-off-by: Mark Laing --- lxd/db/cluster/entities.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lxd/db/cluster/entities.go b/lxd/db/cluster/entities.go index f1622a28b227..bf6fc30ed684 100644 --- a/lxd/db/cluster/entities.go +++ b/lxd/db/cluster/entities.go @@ -580,24 +580,24 @@ var entityStatementsByProjectName = map[entity.Type]string{ entity.TypeNetworkZone: networkZoneEntitiesByProjectName, } -// entityRef represents the expected format of entity URL queries. -type entityRef struct { - entityType EntityType - entityID int - projectName string - location string - pathArgs []string +// EntityRef represents the expected format of entity URL queries. +type EntityRef struct { + EntityType EntityType + EntityID int + ProjectName string + Location string + PathArgs []string } // scan accepts a scanning function (e.g. `(*sql.Row).Scan`) and uses it to parse the row and set its fields. -func (e *entityRef) scan(scan func(dest ...any) error) error { +func (e *EntityRef) scan(scan func(dest ...any) error) error { var pathArgs string - err := scan(&e.entityType, &e.entityID, &e.projectName, &e.location, &pathArgs) + err := scan(&e.EntityType, &e.EntityID, &e.ProjectName, &e.Location, &pathArgs) if err != nil { return fmt.Errorf("Failed to scan entity URL: %w", err) } - err = json.Unmarshal([]byte(pathArgs), &e.pathArgs) + err = json.Unmarshal([]byte(pathArgs), &e.PathArgs) if err != nil { return fmt.Errorf("Failed to unmarshal entity URL path arguments: %w", err) } @@ -605,9 +605,9 @@ func (e *entityRef) scan(scan func(dest ...any) error) error { return nil } -// getURL is a convenience for generating a URL from the entityRef. -func (e *entityRef) getURL() (*api.URL, error) { - u, err := entity.Type(e.entityType).URL(e.projectName, e.location, e.pathArgs...) +// getURL is a convenience for generating a URL from the EntityRef. +func (e *EntityRef) getURL() (*api.URL, error) { + u, err := entity.Type(e.EntityType).URL(e.ProjectName, e.Location, e.PathArgs...) if err != nil { return nil, fmt.Errorf("Failed to create entity URL: %w", err) } @@ -631,7 +631,7 @@ func GetEntityURL(ctx context.Context, tx *sql.Tx, entityType entity.Type, entit return nil, api.StatusErrorf(http.StatusNotFound, "No entity found with id `%d` and type %q", entityID, entityType) } - entityRef := &entityRef{} + entityRef := &EntityRef{} err := entityRef.scan(row.Scan) if err != nil { return nil, fmt.Errorf("Failed to scan entity URL: %w", err) @@ -717,7 +717,7 @@ func GetEntityURLs(ctx context.Context, tx *sql.Tx, projectName string, entityTy } for rows.Next() { - entityRef := &entityRef{} + entityRef := &EntityRef{} err := entityRef.scan(rows.Scan) if err != nil { return nil, fmt.Errorf("Failed to scan entity URL: %w", err) @@ -728,7 +728,7 @@ func GetEntityURLs(ctx context.Context, tx *sql.Tx, projectName string, entityTy return nil, err } - result[entity.Type(entityRef.entityType)][entityRef.entityID] = u + result[entity.Type(entityRef.EntityType)][entityRef.EntityID] = u } return result, nil From 5a75b47ee3051774712687fa474f22a1529bcf05 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:16:23 +0000 Subject: [PATCH 15/34] lxd/db/cluster: Add queries and methods for resolving entity URLs to IDs. Signed-off-by: Mark Laing --- lxd/db/cluster/entities.go | 237 +++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) diff --git a/lxd/db/cluster/entities.go b/lxd/db/cluster/entities.go index bf6fc30ed684..19bbc5466a86 100644 --- a/lxd/db/cluster/entities.go +++ b/lxd/db/cluster/entities.go @@ -733,3 +733,240 @@ func GetEntityURLs(ctx context.Context, tx *sql.Tx, projectName string, entityTy return result, nil } + +/* +The following queries return the ID of an entity by the information contained in its unique URL in a common format. This +allows us to query for the IDs of multiple entities at once by concatenating these queries with UNION. +All of the below queries expect as arguments the project name query parameter, the location query parameter, and the path arguments of the URL. +Each row returned by all of these queries has two columns: + 1. An identifier for the query being executed, this is directly returned so that we can match the result of the query + to the URL (see PopulateEntityReferencesFromURLs below). + 2. The ID of the entity. + +Note: It may be possible to further improve the efficiency of these queries by using WHERE statements, e.g. `WHERE projects.name IN (?, ?, ...)`. +*/ + +// containerIDFromURL gets the ID of a container from its URL. +var containerIDFromURL = fmt.Sprintf(`SELECT ?, instances.id FROM instances JOIN projects ON instances.project_id = projects.id WHERE projects.name = ? AND '' = ? AND instances.name = ? AND instances.type = %d`, instancetype.Container) + +// imageIDFromURL gets the ID of an image from its URL. +var imageIDFromURL = `SELECT ?, images.id FROM images JOIN projects ON images.project_id = projects.id WHERE projects.name = ? AND '' = ? AND images.fingerprint = ?` + +// profileIDFromURL gets the ID of a profile from its URL. +var profileIDFromURL = `SELECT ?, profiles.id FROM profiles JOIN projects ON profiles.project_id = projects.id WHERE projects.name = ? AND '' = ? AND profiles.name = ?` + +// projectIDFromURL gets the ID of a project from its URL. +var projectIDFromURL = `SELECT ?, projects.id FROM projects WHERE '' = ? AND '' = ? AND projects.name = ?` + +// certificateIDFromURL gets the ID of a certificate from its URL. +var certificateIDFromURL = fmt.Sprintf(`SELECT ?, identities.id FROM identities WHERE '' = ? AND '' = ? AND identities.identifier = ? AND identities.auth_method = %d`, authMethodTLS) + +// instanceIDFromURL gets the ID of an instance from its URL. +var instanceIDFromURL = `SELECT ?, instances.id FROM instances JOIN projects ON instances.project_id = projects.id WHERE projects.name = ? AND '' = ? AND instances.name = ?` + +// instanceBackupIDFromURL gets the ID of an instance backup from its URL. +var instanceBackupIDFromURL = `SELECT ?, instances_backups.id FROM instances_backups JOIN instances ON instances_backups.instance_id = instances.id JOIN projects ON instances.project_id = projects.id WHERE projects.name = ? AND '' = ? AND instances.name = ? AND instances_backups.name = ?` + +// instanceSnapshotIDFromURL gets the ID of an instance snapshot from its URL. +var instanceSnapshotIDFromURL = `SELECT ?, instances_snapshots.id FROM instances_snapshots JOIN instances ON instances_snapshots.instance_id = instances.id JOIN projects ON instances.project_id = projects.id WHERE projects.name = ? AND '' = ? AND instances.name = ? AND instances_snapshots.name = ?` + +// networkIDFromURL gets the ID of a network from its URL. +var networkIDFromURL = `SELECT ?, networks.id FROM networks JOIN projects ON networks.project_id = projects.id WHERE projects.name = ? AND '' = ? AND networks.name = ?` + +// networkACLIDFromURL gets the ID of a network ACL from its URL. +var networkACLIDFromURL = `SELECT ?, networks_acls.id FROM networks_acls JOIN projects ON networks_acls.project_id = projects.id WHERE projects.name = ? AND '' = ? AND networks_acls.name = ?` + +// nodeIDFromURL gets the ID of a node from its URL. +var nodeIDFromURL = `SELECT ?, nodes.id FROM nodes WHERE '' = ? AND '' = ? AND nodes.name = ?` + +// operationIDFromURL gets the ID of an operation from its URL. +var operationIDFromURL = `SELECT ?, operations.id FROM operations LEFT JOIN projects ON operations.project_id = projects.id WHERE coalesce(projects.name, '') = ? AND '' = ? AND operations.uuid = ?` + +// storagePoolIDFromURL gets the ID of a storage pool from its URL. +var storagePoolIDFromURL = `SELECT ?, storage_pools.id FROM storage_pools WHERE '' = ? AND '' = ? AND storage_pools.name = ?` + +// storageVolumeIDFromURL gets the ID of a storage volume from its URL. +var storageVolumeIDFromURL = fmt.Sprintf(` +SELECT ?, storage_volumes.id FROM storage_volumes + JOIN projects ON storage_volumes.project_id = projects.id + JOIN storage_pools ON storage_volumes.storage_pool_id = storage_pools.id + LEFT JOIN nodes ON storage_volumes.node_id = nodes.id +WHERE projects.name = ? AND replace(coalesce(nodes.name, ''), 'none', '') = ? AND storage_pools.name = ? AND CASE storage_volumes.type WHEN %d THEN '%s' WHEN %d THEN '%s' WHEN %d THEN '%s' WHEN %d THEN '%s' END = ? AND storage_volumes.name = ? +`, StoragePoolVolumeTypeContainer, StoragePoolVolumeTypeNameContainer, StoragePoolVolumeTypeImage, StoragePoolVolumeTypeNameImage, StoragePoolVolumeTypeCustom, StoragePoolVolumeTypeNameCustom, StoragePoolVolumeTypeVM, StoragePoolVolumeTypeNameVM) + +// storageVolumeBackupIDFromURL gets the ID of a storageVolumeBackup from its URL. +var storageVolumeBackupIDFromURL = fmt.Sprintf(` +SELECT ?, storage_volumes_backups.id FROM storage_volumes_backups + JOIN storage_volumes ON storage_volumes_backups.storage_volume_id = storage_volumes.id + JOIN projects ON storage_volumes.project_id = projects.id + JOIN storage_pools ON storage_volumes.storage_pool_id = storage_pools.id + LEFT JOIN nodes ON storage_volumes.node_id = nodes.id +WHERE projects.name = ? AND replace(coalesce(nodes.name, ''), 'none', '') = ? AND storage_pools.name = ? AND CASE storage_volumes.type WHEN %d THEN '%s' WHEN %d THEN '%s' WHEN %d THEN '%s' WHEN %d THEN '%s' END = ? AND storage_volumes.name = ? AND storage_volumes_backups.name = ? +`, StoragePoolVolumeTypeContainer, StoragePoolVolumeTypeNameContainer, StoragePoolVolumeTypeImage, StoragePoolVolumeTypeNameImage, StoragePoolVolumeTypeCustom, StoragePoolVolumeTypeNameCustom, StoragePoolVolumeTypeVM, StoragePoolVolumeTypeNameVM) + +// storageVolumeSnapshotIDFromURL gets the ID of a storageVolumeSnapshot from its URL. +var storageVolumeSnapshotIDFromURL = fmt.Sprintf(` +SELECT ?, storage_volumes_backups.id FROM storage_volumes_backups + JOIN storage_volumes ON storage_volumes_backups.storage_volume_id = storage_volumes.id + JOIN projects ON storage_volumes.project_id = projects.id + JOIN storage_pools ON storage_volumes.storage_pool_id = storage_pools.id + LEFT JOIN nodes ON storage_volumes.node_id = nodes.id +WHERE projects.name = ? AND replace(coalesce(nodes.name, ''), 'none', '') = ? AND storage_pools.name = ? AND CASE storage_volumes.type WHEN %d THEN '%s' WHEN %d THEN '%s' WHEN %d THEN '%s' WHEN %d THEN '%s' END = ? AND storage_volumes.name = ? AND storage_volumes_backups.name = ? +`, StoragePoolVolumeTypeContainer, StoragePoolVolumeTypeNameContainer, StoragePoolVolumeTypeImage, StoragePoolVolumeTypeNameImage, StoragePoolVolumeTypeCustom, StoragePoolVolumeTypeNameCustom, StoragePoolVolumeTypeVM, StoragePoolVolumeTypeNameVM) + +// warningIDFromURL gets the ID of a warning from its URL. +var warningIDFromURL = `SELECT ?, warnings.id FROM warnings LEFT JOIN projects ON warnings.project_id = projects.id WHERE coalesce(projects.name, '') = ? AND '' = ? AND warnings.uuid = ?` + +// clusterGroupIDFromURL gets the ID of a clusterGroup from its URL. +var clusterGroupIDFromURL = `SELECT ?, cluster_groups.id FROM cluster_groups WHERE '' = ? AND '' = ? AND cluster_groups.name = ?` + +// storageBucketIDFromURL gets the ID of a storageBucket from its URL. +var storageBucketIDFromURL = ` +SELECT ?, storage_buckets.id FROM storage_buckets + JOIN projects ON storage_buckets.project_id = projects.id + JOIN storage_pools ON storage_buckets.storage_pool_id = storage_pools.id + LEFT JOIN nodes ON storage_buckets.node_id = nodes.id + WHERE projects.name = ? AND replace(coalesce(nodes.name, ''), 'none', '') = ? AND storage_pools.name = ? AND storage_buckets.name = ? +` + +// networkZoneIDFromURL gets the ID of a networkZone from its URL. +var networkZoneIDFromURL = `SELECT ?, networks_zones.id FROM networks_zones JOIN projects ON networks_zones.project_id = projects.id WHERE projects.name = ? AND '' = ? AND networks_zones.name = ?` + +// imageAliasIDFromURL gets the ID of a imageAlias from its URL. +var imageAliasIDFromURL = `SELECT ?, images_aliases.id FROM images_aliases JOIN projects ON images_aliases.project_id = projects.id WHERE projects.name = ? AND '' = ? AND images_aliases.name = ? ` + +// authGroupIDFromURL gets the ID of a group from its URL. +var authGroupIDFromURL = `SELECT ?, auth_groups.id FROM auth_groups WHERE '' = ? AND '' = ? AND auth_groups.name = ?` + +// identityProviderGroupIDFromURL gets the ID of a identityProviderGroup from its URL. +var identityProviderGroupIDFromURL = `SELECT ?, identity_provider_groups.id FROM identity_provider_groups WHERE '' = ? AND '' = ? AND identity_provider_groups.name = ?` + +// identityIDFromURL gets the ID of a identity from its URL. +var identityIDFromURL = fmt.Sprintf(`SELECT ?, identities.id FROM identities WHERE '' = ? AND '' = ? AND CASE identities.auth_method WHEN %d THEN '%s' WHEN %d THEN '%s' END = ? and identities.identifier = ?`, authMethodTLS, api.AuthenticationMethodTLS, authMethodOIDC, api.AuthenticationMethodOIDC) + +// identityIDFromURLStatements is a map of entity.Type to a statement that can be used to get the ID of the entity from its URL. +var entityIDFromURLStatements = map[entity.Type]string{ + entity.TypeContainer: containerIDFromURL, + entity.TypeImage: imageIDFromURL, + entity.TypeProfile: profileIDFromURL, + entity.TypeProject: projectIDFromURL, + entity.TypeCertificate: certificateIDFromURL, + entity.TypeInstance: instanceIDFromURL, + entity.TypeInstanceBackup: instanceBackupIDFromURL, + entity.TypeInstanceSnapshot: instanceSnapshotIDFromURL, + entity.TypeNetwork: networkIDFromURL, + entity.TypeNetworkACL: networkACLIDFromURL, + entity.TypeNode: nodeIDFromURL, + entity.TypeOperation: operationIDFromURL, + entity.TypeStoragePool: storagePoolIDFromURL, + entity.TypeStorageVolume: storageVolumeIDFromURL, + entity.TypeStorageVolumeBackup: storageVolumeBackupIDFromURL, + entity.TypeStorageVolumeSnapshot: storageVolumeSnapshotIDFromURL, + entity.TypeWarning: warningIDFromURL, + entity.TypeClusterGroup: clusterGroupIDFromURL, + entity.TypeStorageBucket: storageBucketIDFromURL, + entity.TypeImageAlias: imageAliasIDFromURL, + entity.TypeNetworkZone: networkZoneIDFromURL, + entity.TypeAuthGroup: authGroupIDFromURL, + entity.TypeIdentityProviderGroup: identityProviderGroupIDFromURL, + entity.TypeIdentity: identityIDFromURL, +} + +// PopulateEntityReferencesFromURLs populates the values in the given map with entity references corresponding to the api.URL keys. +// It will return an error if any of the given URLs do not correspond to a LXD entity. +func PopulateEntityReferencesFromURLs(ctx context.Context, tx *sql.Tx, entityURLMap map[*api.URL]*EntityRef) error { + // If the input list is empty, nothing to do. + if len(entityURLMap) == 0 { + return nil + } + + entityURLs := make([]*api.URL, 0, len(entityURLMap)) + for entityURL := range entityURLMap { + entityURLs = append(entityURLs, entityURL) + } + + stmts := make([]string, 0, len(entityURLs)) + var args []any + for i, entityURL := range entityURLs { + // Parse the URL to get the majority of the fields of the EntityRef for that URL. + entityType, projectName, location, pathArgs, err := entity.ParseURL(entityURL.URL) + if err != nil { + return fmt.Errorf("Failed to get entity IDs from URLs: %w", err) + } + + // Populate the result map. + entityURLMap[entityURL] = &EntityRef{ + EntityType: EntityType(entityType), + ProjectName: projectName, + Location: location, + PathArgs: pathArgs, + } + + // If the given URL is the server url it is valid but there is no need to perform a query for it, the entity + // ID of the server is always zero (by virtue of being the zero value for int). + if entityType == entity.TypeServer { + continue + } + + // Get the statement corresponding to the entity type. + stmt, ok := entityIDFromURLStatements[entityType] + if !ok { + return fmt.Errorf("Could not get entity IDs from URLs: No statement found for entity type %q", entityType) + } + + // Each statement accepts an identifier for the query, the project name, the location, and all path arguments as arguments. + // In this case we can use the index of the url from the argument slice as an identifier. + stmts = append(stmts, stmt) + args = append(args, i, projectName, location) + for _, pathArg := range pathArgs { + args = append(args, pathArg) + } + } + + // If the only argument was a server URL we don't have any statements to execute. + if len(stmts) == 0 { + return nil + } + + // Join the statements with a union and execute. + stmt := strings.Join(stmts, " UNION ") + rows, err := tx.QueryContext(ctx, stmt, args...) + if err != nil { + return fmt.Errorf("Failed to get entityIDs from URLS: %w", err) + } + + for rows.Next() { + var rowID, entityID int + err = rows.Scan(&rowID, &entityID) + if err != nil { + return fmt.Errorf("Failed to get entityIDs from URLS: %w", err) + } + + if rowID >= len(entityURLs) { + return fmt.Errorf("Failed to get entityIDs from URLS: Internal error, returned row ID greater than number of URLs") + } + + // Using the row ID, get the *api.URL from the argument slice, then use it as a key in our result map to get the *EntityRef. + entityRef, ok := entityURLMap[entityURLs[rowID]] + if !ok { + return fmt.Errorf("Failed to get entityIDs from URLS: Internal error, entity URL missing from result object") + } + + // Set the value of the EntityID in the *EntityRef. + entityRef.EntityID = entityID + } + + err = rows.Err() + if err != nil { + return fmt.Errorf("Failed to get entity IDs from URLs: %w", err) + } + + // Check that all given URLs have been resolved to an ID. + for u, ref := range entityURLMap { + if ref.EntityID == 0 && ref.EntityType != EntityType(entity.TypeServer) { + return fmt.Errorf("Failed to find entity ID for URL %q", u.String()) + } + } + + return nil +} From 078a57e4df8393b6c0db37c17659f552e22b31e9 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:16:52 +0000 Subject: [PATCH 16/34] lxd/db/cluster: Update tests for entity queries. Signed-off-by: Mark Laing --- lxd/db/cluster/entities_test.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lxd/db/cluster/entities_test.go b/lxd/db/cluster/entities_test.go index cd6c4a54b708..3d88792bebab 100644 --- a/lxd/db/cluster/entities_test.go +++ b/lxd/db/cluster/entities_test.go @@ -10,7 +10,7 @@ import ( func TestEntityStatementValidity(t *testing.T) { schema := Schema() - db, err := schema.ExerciseUpdate(70, nil) + db, err := schema.ExerciseUpdate(71, nil) require.NoError(t, err) for entityType, stmt := range entityStatementsAll { @@ -37,4 +37,13 @@ func TestEntityStatementValidity(t *testing.T) { } } } + + for outerEntityType, outerStmt := range entityIDFromURLStatements { + _, err := db.Prepare(outerStmt) + assert.NoErrorf(t, err, "Entity ID from URL statement %q: %v", outerEntityType, err) + for innerEntityType, innerStmt := range entityIDFromURLStatements { + _, err = db.Prepare(strings.Join([]string{outerStmt, innerStmt}, " UNION ")) + assert.NoErrorf(t, err, "Union entity ID from URL statement (outer: %q; inner: %q): %v", outerEntityType, innerEntityType, err) + } + } } From 499864a8f4c62dd175acf78fc22543bac58a8e5b Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:21:41 +0000 Subject: [PATCH 17/34] lxd/identity: Adds groups and IDP groups to identity cache. Signed-off-by: Mark Laing --- lxd/identity/cache.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lxd/identity/cache.go b/lxd/identity/cache.go index 00d5d5fed484..b913f885703d 100644 --- a/lxd/identity/cache.go +++ b/lxd/identity/cache.go @@ -15,7 +15,10 @@ type Cache struct { // entries is a map of authentication method to map of identifier to CacheEntry. The identifier is either a // certificate fingerprint (tls) or an email address (oidc). entries map[string]map[string]*CacheEntry - mu sync.RWMutex + + // identityProviderGroups is a map of identity provider group name to slice of LXD group names. + identityProviderGroups map[string]*[]string + mu sync.RWMutex } // CacheEntry represents an identity. @@ -25,6 +28,7 @@ type CacheEntry struct { AuthenticationMethod string IdentityType string Projects []string + Groups []string // Certificate is optional. It is pre-computed for identities with AuthenticationMethod api.AuthenticationMethodTLS. Certificate *x509.Certificate @@ -104,8 +108,8 @@ func (c *Cache) GetByAuthenticationMethod(authenticationMethod string) map[strin return entriesOfAuthMethodCopy } -// ReplaceAll deletes all entries from the cache and replaces them with the given entries. -func (c *Cache) ReplaceAll(entries []CacheEntry) error { +// ReplaceAll deletes all entries and identity provider groups from the cache and replaces them with the given values. +func (c *Cache) ReplaceAll(entries []CacheEntry, idpGroups map[string][]string) error { c.mu.Lock() defer c.mu.Unlock() @@ -124,6 +128,13 @@ func (c *Cache) ReplaceAll(entries []CacheEntry) error { c.entries[entry.AuthenticationMethod][entry.Identifier] = &e } + c.identityProviderGroups = make(map[string]*[]string, len(idpGroups)) + for idpGroupName, authGroupNames := range idpGroups { + authGroupNamesCopy := make([]string, 0, len(authGroupNames)) + authGroupNamesCopy = append(authGroupNamesCopy, authGroupNames...) + c.identityProviderGroups[idpGroupName] = &authGroupNamesCopy + } + return nil } From fee4f54f48b3857110d0e4713a4c48edf8f0e04d Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:23:04 +0000 Subject: [PATCH 18/34] lxd: Update calls to (*identity.Cache).ReplaceAll in tests. Signed-off-by: Mark Laing --- lxd/cluster/membership_test.go | 2 +- lxd/project/permissions_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lxd/cluster/membership_test.go b/lxd/cluster/membership_test.go index c910832f9fc7..60d99f74eab1 100644 --- a/lxd/cluster/membership_test.go +++ b/lxd/cluster/membership_test.go @@ -293,7 +293,7 @@ func TestJoin(t *testing.T) { Identifier: altServerCert.Fingerprint(), Certificate: trustedAltServerCert, }, - }) + }, nil) require.NoError(t, err) for path, handler := range targetGateway.HandlerFuncs(nil, identityCache) { diff --git a/lxd/project/permissions_test.go b/lxd/project/permissions_test.go index 61f08c186a49..bd593cca0374 100644 --- a/lxd/project/permissions_test.go +++ b/lxd/project/permissions_test.go @@ -218,7 +218,7 @@ func TestCheckClusterTargetRestriction_RestrictedTrueWithOverride(t *testing.T) // Certificate has to be non-nil for TLS identities. Certificate: &x509.Certificate{}, }, - }) + }, nil) require.NoError(t, err) authorizer, err := auth.LoadAuthorizer(context.Background(), auth.DriverTLS, logger.Log, identityCache) From 0e15f7945012ec1be515ff4679fc17339f007593 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:24:07 +0000 Subject: [PATCH 19/34] lxd: Update identity cache refresh handlers to include groups. Signed-off-by: Mark Laing --- lxd/identities.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/lxd/identities.go b/lxd/identities.go index d272e84a041d..f2cde14c0b3e 100644 --- a/lxd/identities.go +++ b/lxd/identities.go @@ -24,6 +24,8 @@ func updateIdentityCache(d *Daemon) { var identities []cluster.Identity projects := make(map[int][]string) + groups := make(map[int][]string) + idpGroupMapping := make(map[string][]string) var err error err = s.DB.Cluster.Transaction(d.shutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error { identities, err = cluster.GetIdentitys(ctx, tx.Tx()) @@ -40,7 +42,31 @@ func updateIdentityCache(d *Daemon) { for _, p := range identityProjects { projects[identity.ID] = append(projects[identity.ID], p.Name) } + + identityGroups, err := cluster.GetIdentityGroups(ctx, tx.Tx(), identity.ID) + if err != nil { + return err + } + + for _, g := range identityGroups { + groups[identity.ID] = append(groups[identity.ID], g.Name) + } } + + idpGroups, err := cluster.GetIdentityProviderGroups(ctx, tx.Tx()) + if err != nil { + return err + } + + for _, idpGroup := range idpGroups { + apiIDPGroup, err := idpGroup.ToAPI(ctx, tx.Tx()) + if err != nil { + return err + } + + idpGroupMapping[apiIDPGroup.Name] = apiIDPGroup.Groups + } + return nil }) if err != nil { @@ -57,6 +83,7 @@ func updateIdentityCache(d *Daemon) { AuthenticationMethod: string(id.AuthMethod), IdentityType: string(id.Type), Projects: projects[id.ID], + Groups: groups[id.ID], } if cacheEntry.AuthenticationMethod == api.AuthenticationMethodTLS { @@ -100,7 +127,7 @@ func updateIdentityCache(d *Daemon) { // continue functioning, and hopefully the write will succeed on next update. } - err = d.identityCache.ReplaceAll(identityCacheEntries) + err = d.identityCache.ReplaceAll(identityCacheEntries, idpGroupMapping) if err != nil { logger.Warn("Failed to update identity cache", logger.Ctx{"error": err}) } @@ -149,7 +176,7 @@ func updateIdentityCacheFromLocal(d *Daemon) error { }) } - err = d.identityCache.ReplaceAll(identityCacheEntries) + err = d.identityCache.ReplaceAll(identityCacheEntries, nil) if err != nil { return fmt.Errorf("Failed to update identity cache from local trust store: %w", err) } From 235ca4b117501241dee0435fbf869531cf0c18f6 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:25:31 +0000 Subject: [PATCH 20/34] lxd/auth: Add function to validate an authentication method. The identities API accepts the authentication method as a path argument, so this should be validate before allowing the request to continue. Signed-off-by: Mark Laing --- lxd/auth/util.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lxd/auth/util.go b/lxd/auth/util.go index 54993a4aa795..01a6a6bb3fc8 100644 --- a/lxd/auth/util.go +++ b/lxd/auth/util.go @@ -2,11 +2,23 @@ package auth import ( "fmt" + "net/http" "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/entity" ) +// ValidateAuthenticationMethod returns an api.StatusError with http.StatusBadRequest if the given authentication +// method is not recognised. +func ValidateAuthenticationMethod(authenticationMethod string) error { + if !shared.ValueInSlice(authenticationMethod, []string{api.AuthenticationMethodTLS, api.AuthenticationMethodOIDC}) { + return api.StatusErrorf(http.StatusBadRequest, "Unrecognized authentication method %q", authenticationMethod) + } + + return nil +} + // ValidateEntitlement returns an error if the given Entitlement does not apply to the entity.Type. func ValidateEntitlement(entityType entity.Type, entitlement Entitlement) error { entitlements, err := EntitlementsByEntityType(entityType) From 955bf0b75dc457a7a26b89578da7485bd08c470b Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:31:44 +0000 Subject: [PATCH 21/34] lxd/auth: Update Entitlement validation. Some entity types are not associated with any entitlements these may be legacy entity types "container" or may be used for the warnings API. Signed-off-by: Mark Laing --- lxd/auth/util.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lxd/auth/util.go b/lxd/auth/util.go index 01a6a6bb3fc8..1b7238ed3b97 100644 --- a/lxd/auth/util.go +++ b/lxd/auth/util.go @@ -26,6 +26,10 @@ func ValidateEntitlement(entityType entity.Type, entitlement Entitlement) error return err } + if len(entitlements) == 0 { + return fmt.Errorf("No entitlements can be granted against entities of type %q", entityType) + } + if !shared.ValueInSlice(entitlement, entitlements) { return fmt.Errorf("Entitlement %q not valid for entity type %q", entitlement, entityType) } @@ -40,6 +44,22 @@ func EntitlementsByEntityType(entityType entity.Type) ([]Entitlement, error) { return nil, fmt.Errorf("Entity type %q is not valid: %w", entityType, err) } + // Some entity types do not have entitlements + if shared.ValueInSlice(entityType, []entity.Type{ + entity.TypeContainer, + entity.TypeCertificate, + entity.TypeInstanceBackup, + entity.TypeInstanceSnapshot, + entity.TypeNode, + entity.TypeOperation, + entity.TypeStorageVolumeBackup, + entity.TypeStorageVolumeSnapshot, + entity.TypeWarning, + entity.TypeClusterGroup, + }) { + return []Entitlement{}, nil + } + // With the exception of entity types in the list below. All entity types have EntitlementCanView, // EntitlementCanEdit, and EntitlementCanDelete. if !shared.ValueInSlice(entityType, []entity.Type{entity.TypeStorageVolume, entity.TypeInstance, entity.TypeProject, entity.TypeServer}) { From e1a32f1cdbead13cf16e0235b4fe4c985dffd8cc Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:34:16 +0000 Subject: [PATCH 22/34] lxd/auth: Add `can_view_permissions` entitlement to server. There are many entitlements for creating and editing groups and identities. These are combined into the built-in role `permission_manager`. However, there is no existing entitlement for viewing available permissions only. This commit adds the fine-grained entitlement that will allow a group access to `GET /1.0/permissions`. Signed-off-by: Mark Laing --- lxd/auth/authorization_types.go | 4 ++++ lxd/auth/util.go | 1 + 2 files changed, 5 insertions(+) diff --git a/lxd/auth/authorization_types.go b/lxd/auth/authorization_types.go index af798eb0a125..617a53ee1c11 100644 --- a/lxd/auth/authorization_types.go +++ b/lxd/auth/authorization_types.go @@ -31,6 +31,9 @@ const ( // EntitlementPermissionManager is the `permission_manager` Entitlement. It applies to entity.TypeServer. EntitlementPermissionManager Entitlement = "permission_manager" + // EntitlementCanViewPermissions is the `can_view_permissions` Entitlement. It applies to entity.TypeServer. + EntitlementCanViewPermissions Entitlement = "can_view_permissions" + // EntitlementCanCreateIdentities is the `can_create_identities` Entitlement. It applies to entity.TypeServer. EntitlementCanCreateIdentities Entitlement = "can_create_identities" @@ -283,6 +286,7 @@ var allEntitlements = []Entitlement{ EntitlementServerViewer, EntitlementCanViewConfiguration, EntitlementPermissionManager, + EntitlementCanViewPermissions, EntitlementCanCreateIdentities, EntitlementCanViewIdentities, EntitlementCanEditIdentities, diff --git a/lxd/auth/util.go b/lxd/auth/util.go index 1b7238ed3b97..41c02265ced3 100644 --- a/lxd/auth/util.go +++ b/lxd/auth/util.go @@ -154,6 +154,7 @@ func EntitlementsByEntityType(entityType entity.Type) ([]Entitlement, error) { EntitlementServerViewer, EntitlementCanViewConfiguration, EntitlementPermissionManager, + EntitlementCanViewPermissions, EntitlementCanCreateIdentities, EntitlementCanViewIdentities, EntitlementCanEditIdentities, From 1166f484ac22547ff0afb05f0eedcf233cabaa82 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 26 Feb 2024 19:26:49 +0000 Subject: [PATCH 23/34] shared/api: Define lifecycle events for auth groups and IDP groups. Signed-off-by: Mark Laing --- shared/api/event_lifecycle.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shared/api/event_lifecycle.go b/shared/api/event_lifecycle.go index e51c7b376ad4..8dbac2980077 100644 --- a/shared/api/event_lifecycle.go +++ b/shared/api/event_lifecycle.go @@ -121,4 +121,12 @@ const ( EventLifecycleWarningReset = "warning-reset" EventLifecycleIdentityCreated = "identity-created" EventLifecycleIdentityUpdated = "identity-updated" + EventLifecycleAuthGroupCreated = "auth-group-created" + EventLifecycleAuthGroupUpdated = "auth-group-updated" + EventLifecycleAuthGroupRenamed = "auth-group-renamed" + EventLifecycleAuthGroupDeleted = "auth-group-deleted" + EventLifecycleIdentityProviderGroupCreated = "identity-provider-group-created" + EventLifecycleIdentityProviderGroupUpdated = "identity-provider-group-updated" + EventLifecycleIdentityProviderGroupRenamed = "identity-provider-group-renamed" + EventLifecycleIdentityProviderGroupDeleted = "identity-provider-group-deleted" ) From 375c223448ecbae2e8e957964cb079cc91374401 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 26 Feb 2024 19:27:35 +0000 Subject: [PATCH 24/34] lxd/lifecycle: Define lifecycle event actions for auth and IDP groups. Signed-off-by: Mark Laing --- lxd/lifecycle/auth_group.go | 29 ++++++++++++++++++++++++ lxd/lifecycle/identity_provider_group.go | 29 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 lxd/lifecycle/auth_group.go create mode 100644 lxd/lifecycle/identity_provider_group.go diff --git a/lxd/lifecycle/auth_group.go b/lxd/lifecycle/auth_group.go new file mode 100644 index 000000000000..71cd74b4ccb7 --- /dev/null +++ b/lxd/lifecycle/auth_group.go @@ -0,0 +1,29 @@ +package lifecycle + +import ( + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/entity" +) + +// AuthGroupAction represents a lifecycle event action for auth groups. +type AuthGroupAction string + +// All supported lifecycle events for identities. +const ( + AuthGroupCreated = AuthGroupAction(api.EventLifecycleAuthGroupCreated) + AuthGroupUpdated = AuthGroupAction(api.EventLifecycleAuthGroupUpdated) + AuthGroupRenamed = AuthGroupAction(api.EventLifecycleAuthGroupRenamed) + AuthGroupDeleted = AuthGroupAction(api.EventLifecycleAuthGroupDeleted) +) + +// Event creates the lifecycle event for an action on a Certificate. +func (a AuthGroupAction) Event(groupName string, requestor *api.EventLifecycleRequestor, ctx map[string]any) api.EventLifecycle { + u := entity.AuthGroupURL(groupName) + + return api.EventLifecycle{ + Action: string(a), + Source: u.String(), + Context: ctx, + Requestor: requestor, + } +} diff --git a/lxd/lifecycle/identity_provider_group.go b/lxd/lifecycle/identity_provider_group.go new file mode 100644 index 000000000000..c55da1dac309 --- /dev/null +++ b/lxd/lifecycle/identity_provider_group.go @@ -0,0 +1,29 @@ +package lifecycle + +import ( + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/entity" +) + +// IdentityProviderGroupAction represents a lifecycle event action for auth groups. +type IdentityProviderGroupAction string + +// All supported lifecycle events for identities. +const ( + IdentityProviderGroupCreated = IdentityProviderGroupAction(api.EventLifecycleIdentityProviderGroupCreated) + IdentityProviderGroupUpdated = IdentityProviderGroupAction(api.EventLifecycleIdentityProviderGroupUpdated) + IdentityProviderGroupRenamed = IdentityProviderGroupAction(api.EventLifecycleIdentityProviderGroupRenamed) + IdentityProviderGroupDeleted = IdentityProviderGroupAction(api.EventLifecycleIdentityProviderGroupDeleted) +) + +// Event creates the lifecycle event for an action on a Certificate. +func (a IdentityProviderGroupAction) Event(identityProviderGroupName string, requestor *api.EventLifecycleRequestor, ctx map[string]any) api.EventLifecycle { + u := entity.IdentityProviderGroupURL(identityProviderGroupName) + + return api.EventLifecycle{ + Action: string(a), + Source: u.String(), + Context: ctx, + Requestor: requestor, + } +} From 96e19baf091ece0d4b8b69124199cc7eabaef6d2 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 15:50:04 +0000 Subject: [PATCH 25/34] lxd: Add the identities API handlers. Signed-off-by: Mark Laing --- lxd/api_1.0.go | 3 + lxd/identities.go | 712 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 707 insertions(+), 8 deletions(-) diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index 419dc89c9423..f31398662558 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -124,6 +124,9 @@ var api10 = []APIEndpoint{ warningsCmd, warningCmd, metricsCmd, + identitiesCmd, + identitiesByAuthenticationMethodCmd, + identityCmd, } // swagger:operation GET /1.0?public server server_get_untrusted diff --git a/lxd/identities.go b/lxd/identities.go index f2cde14c0b3e..599248b5bd12 100644 --- a/lxd/identities.go +++ b/lxd/identities.go @@ -3,16 +3,712 @@ package main import ( "context" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" + "net/http" + "net/url" + "github.com/gorilla/mux" + + "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" + "github.com/canonical/lxd/lxd/cluster" "github.com/canonical/lxd/lxd/db" - "github.com/canonical/lxd/lxd/db/cluster" + dbCluster "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/identity" + "github.com/canonical/lxd/lxd/lifecycle" + "github.com/canonical/lxd/lxd/request" + "github.com/canonical/lxd/lxd/response" + "github.com/canonical/lxd/lxd/util" + "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/entity" "github.com/canonical/lxd/shared/logger" ) +var identitiesCmd = APIEndpoint{ + Name: "identities", + Path: "auth/identities", + Get: APIEndpointAction{ + Handler: getIdentities, + AccessHandler: allowAuthenticated, + }, +} + +var identitiesByAuthenticationMethodCmd = APIEndpoint{ + Name: "identities", + Path: "auth/identities/{authenticationMethod}", + Get: APIEndpointAction{ + Handler: getIdentities, + AccessHandler: allowAuthenticated, + }, +} + +var identityCmd = APIEndpoint{ + Name: "identity", + Path: "auth/identities/{authenticationMethod}/{nameOrIdentifier}", + Get: APIEndpointAction{ + Handler: getIdentity, + AccessHandler: identityAccessHandler(auth.EntitlementCanView), + }, + Put: APIEndpointAction{ + Handler: updateIdentity, + AccessHandler: identityAccessHandler(auth.EntitlementCanEdit), + }, + Patch: APIEndpointAction{ + Handler: patchIdentity, + AccessHandler: identityAccessHandler(auth.EntitlementCanEdit), + }, +} + +const ( + // ctxClusterDBIdentity is used in the identityAccessHandler to set a cluster.Identity into the request context. + // The database call is required for authorization and this avoids performing the same query twice. + ctxClusterDBIdentity request.CtxKey = "cluster-db-identity" +) + +// identityAccessHandler performs some initial validation of the request and gets the identity by its name or +// identifier. If one is found, the identifier is used in the URL that is passed to (auth.Authorizer).CheckPermission. +// The cluster.Identity is set in the request context. +func identityAccessHandler(entitlement auth.Entitlement) func(d *Daemon, r *http.Request) response.Response { + return func(d *Daemon, r *http.Request) response.Response { + muxVars := mux.Vars(r) + authenticationMethod := muxVars["authenticationMethod"] + err := auth.ValidateAuthenticationMethod(authenticationMethod) + if err != nil { + return response.SmartError(err) + } + + nameOrID, err := url.PathUnescape(muxVars["nameOrIdentifier"]) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to unescape path argument: %w", err)) + } + + s := d.State() + var id *dbCluster.Identity + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + id, err = dbCluster.GetIdentityByNameOrIdentifier(ctx, tx.Tx(), authenticationMethod, nameOrID) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + err = s.Authorizer.CheckPermission(r.Context(), r, entity.IdentityURL(authenticationMethod, id.Identifier), entitlement) + if err != nil { + return response.SmartError(err) + } + + request.SetCtxValue(r, ctxClusterDBIdentity, id) + return response.EmptySyncResponse + } +} + +// swagger:operation GET /1.0/auth/identities identities identities_get +// +// Get the identities +// +// Returns a list of identities (URLs). +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of endpoints +// items: +// type: string +// example: |- +// [ +// "/1.0/auth/identities/tls/e1e06266e36f67431c996d5678e66d732dfd12fe5073c161e62e6360619fc226", +// "/1.0/auth/identities/oidc/auth0|4daf5e37ce230e455b64b65b" +// ] +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" + +// swagger:operation GET /1.0/auth/identities?recursion=1 identities identities_get_recursion1 +// +// Get the identities +// +// Returns a list of identities. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of identities +// items: +// $ref: "#/definitions/Identity" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" + +// swagger:operation GET /1.0/auth/identities?recursion=2 identities identities_get_recursion2 +// +// Get the identities +// +// Returns a list of identities including group membership. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of identities +// items: +// $ref: "#/definitions/IdentityInfo" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" + +// swagger:operation GET /1.0/auth/identities/{authenticationMethod} identities identities_get_by_auth_method +// +// Get the identities +// +// Returns a list of identities (URLs). +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of endpoints +// items: +// type: string +// example: |- +// [ +// "/1.0/auth/identities/tls/e1e06266e36f67431c996d5678e66d732dfd12fe5073c161e62e6360619fc226", +// "/1.0/auth/identities/oidc/auth0|4daf5e37ce230e455b64b65b" +// ] +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" + +// swagger:operation GET /1.0/auth/identities/{authenticationMethod}?recursion=1 identities identities_get_by_auth_method_recursion1 +// +// Get the identities +// +// Returns a list of identities. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of identities +// items: +// $ref: "#/definitions/Identity" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" + +// swagger:operation GET /1.0/auth/identities/{authenticationMethod}?recursion=2 identities identities_get_by_auth_method_recursion2 +// +// Get the identities +// +// Returns a list of identities including group membership. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of identities +// items: +// $ref: "#/definitions/IdentityInfo" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func getIdentities(d *Daemon, r *http.Request) response.Response { + authenticationMethod, err := url.PathUnescape(mux.Vars(r)["authenticationMethod"]) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to unescape path argument: %w", err)) + } + + if authenticationMethod != "" { + err = auth.ValidateAuthenticationMethod(authenticationMethod) + if err != nil { + return response.SmartError(err) + } + } + + recursion := r.URL.Query().Get("recursion") + s := d.State() + hasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, entity.TypeIdentity) + if err != nil { + return response.SmartError(err) + } + + var identities []dbCluster.Identity + var groupsByIdentityID map[int][]dbCluster.AuthGroup + var apiIdentityInfo *api.IdentityInfo + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + // Get all identities, filter by authentication method if present. + var filters []dbCluster.IdentityFilter + if authenticationMethod != "" { + clusterAuthMethod := dbCluster.AuthMethod(authenticationMethod) + filters = append(filters, dbCluster.IdentityFilter{AuthMethod: &clusterAuthMethod}) + } + + allIdentities, err := dbCluster.GetIdentitys(ctx, tx.Tx(), filters...) + if err != nil { + return err + } + + // Filter results by what the user is allowed to view. + for _, id := range allIdentities { + if hasPermission(entity.IdentityURL(string(id.AuthMethod), id.Identifier)) { + identities = append(identities, id) + } + } + + if len(identities) == 0 { + return nil + } + + if recursion == "2" && len(identities) == 1 { + // It's likely that the user can only view themselves. If so we can optimise here by only getting the + // groups for that user. + apiIdentityInfo, err = identities[0].ToAPIInfo(ctx, tx.Tx()) + if err != nil { + return err + } + } else if recursion == "2" { + // Otherwise, get all groups and populate the identities outside of the transaction. + groupsByIdentityID, err = dbCluster.GetAllAuthGroupsByIdentityIDs(ctx, tx.Tx()) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // Optimisation for user that can only view themselves. + if apiIdentityInfo != nil { + return response.SyncResponse(true, []api.IdentityInfo{*apiIdentityInfo}) + } + + if recursion == "1" { + apiIdentities := make([]api.Identity, 0, len(identities)) + for _, id := range identities { + apiIdentities = append(apiIdentities, api.Identity{ + AuthenticationMethod: string(id.AuthMethod), + Type: string(id.Type), + Identifier: id.Identifier, + Name: id.Name, + }) + } + + return response.SyncResponse(true, apiIdentities) + } + + if recursion == "2" { + // Convert the []cluster.Group in the groupsByIdentityID map to string slices of the group names. + groupNamesByIdentityID := make(map[int][]string, len(groupsByIdentityID)) + for identityID, groups := range groupsByIdentityID { + for _, group := range groups { + groupNamesByIdentityID[identityID] = append(groupNamesByIdentityID[identityID], group.Name) + } + } + + apiIdentityInfos := make([]api.IdentityInfo, 0, len(identities)) + for _, id := range identities { + apiIdentityInfos = append(apiIdentityInfos, api.IdentityInfo{ + Identity: api.Identity{ + AuthenticationMethod: string(id.AuthMethod), + Type: string(id.Type), + Identifier: id.Identifier, + Name: id.Name, + }, + IdentityPut: api.IdentityPut{ + Groups: groupNamesByIdentityID[id.ID], + }, + }) + } + + return response.SyncResponse(true, apiIdentityInfos) + } + + urls := make([]string, 0, len(identities)) + for _, id := range identities { + urls = append(urls, entity.IdentityURL(string(id.AuthMethod), id.Identifier).String()) + } + + return response.SyncResponse(true, urls) +} + +// swagger:operation GET /1.0/auth/identities/{authenticationMethod}/{nameOrIdentifier} identities identity_get +// +// Get the identity +// +// Gets a specific identity. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// $ref: "#/definitions/IdentityInfo" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func getIdentity(d *Daemon, r *http.Request) response.Response { + id, err := request.GetCtxValue[*dbCluster.Identity](r.Context(), ctxClusterDBIdentity) + if err != nil { + return response.SmartError(err) + } + + var apiIdentityInfo *api.IdentityInfo + err = d.State().DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + var err error + apiIdentityInfo, err = id.ToAPIInfo(ctx, tx.Tx()) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + return response.SyncResponseETag(true, apiIdentityInfo, apiIdentityInfo) +} + +// swagger:operation PUT /1.0/auth/identities/{authenticationMethod}/{nameOrIdentifier} identities identity_put +// +// Update the identity +// +// Replaces the editable fields of an identity +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: identity +// description: Update request +// schema: +// $ref: "#/definitions/IdentityPut" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func updateIdentity(d *Daemon, r *http.Request) response.Response { + id, err := request.GetCtxValue[*dbCluster.Identity](r.Context(), ctxClusterDBIdentity) + if err != nil { + return response.SmartError(err) + } + + var identityPut api.IdentityPut + err = json.NewDecoder(r.Body).Decode(&identityPut) + if err != nil { + return response.BadRequest(fmt.Errorf("Failed to unmarshal request body: %w", err)) + } + + if id.AuthMethod == api.AuthenticationMethodTLS && len(identityPut.Groups) > 0 { + return response.NotImplemented(fmt.Errorf("Adding TLS identities to groups is currently not supported")) + } + + s := d.State() + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + apiIdentityInfo, err := id.ToAPIInfo(ctx, tx.Tx()) + if err != nil { + return err + } + + err = util.EtagCheck(r, apiIdentityInfo) + if err != nil { + return err + } + + err = dbCluster.SetIdentityAuthGroups(ctx, tx.Tx(), id.ID, identityPut.Groups) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the identity update. + lc := lifecycle.IdentityUpdated.Event(string(id.AuthMethod), id.Identifier, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + s.UpdateIdentityCache() + + return response.EmptySyncResponse +} + +// swagger:operation PATCH /1.0/auth/identities/{authenticationMethod}/{nameOrIdentifier} identities identity_patch +// +// Partially update the identity +// +// Updates the editable fields of an identity +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: identity +// description: Update request +// schema: +// $ref: "#/definitions/IdentityPut" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func patchIdentity(d *Daemon, r *http.Request) response.Response { + id, err := request.GetCtxValue[*dbCluster.Identity](r.Context(), ctxClusterDBIdentity) + if err != nil { + return response.SmartError(err) + } + + var identityPut api.IdentityPut + err = json.NewDecoder(r.Body).Decode(&identityPut) + if err != nil { + return response.BadRequest(fmt.Errorf("Failed to unmarshal request body: %w", err)) + } + + authenticationMethod := mux.Vars(r)["authenticationMethod"] + if authenticationMethod == api.AuthenticationMethodTLS && len(identityPut.Groups) > 0 { + return response.NotImplemented(fmt.Errorf("Adding TLS identities to groups is currently not supported")) + } + + s := d.State() + var apiIdentityInfo *api.IdentityInfo + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + apiIdentityInfo, err = id.ToAPIInfo(ctx, tx.Tx()) + if err != nil { + return err + } + + err = util.EtagCheck(r, apiIdentityInfo) + if err != nil { + return err + } + + for _, groupName := range identityPut.Groups { + if !shared.ValueInSlice(groupName, apiIdentityInfo.Groups) { + apiIdentityInfo.Groups = append(apiIdentityInfo.Groups, groupName) + } + } + + err = dbCluster.SetIdentityAuthGroups(ctx, tx.Tx(), id.ID, identityPut.Groups) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the identity update. + lc := lifecycle.IdentityUpdated.Event(string(id.AuthMethod), id.Identifier, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + s.UpdateIdentityCache() + + return response.EmptySyncResponse +} + // updateIdentityCache reads all identities from the database and sets them in the identity.Cache. // The certificates in the local database are replaced with identities in the cluster database that // are of type api.IdentityTypeCertificateServer. This ensures that this cluster member is able to @@ -22,19 +718,19 @@ func updateIdentityCache(d *Daemon) { logger.Debug("Refreshing identity cache") - var identities []cluster.Identity + var identities []dbCluster.Identity projects := make(map[int][]string) groups := make(map[int][]string) idpGroupMapping := make(map[string][]string) var err error err = s.DB.Cluster.Transaction(d.shutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error { - identities, err = cluster.GetIdentitys(ctx, tx.Tx()) + identities, err = dbCluster.GetIdentitys(ctx, tx.Tx()) if err != nil { return err } for _, identity := range identities { - identityProjects, err := cluster.GetIdentityProjects(ctx, tx.Tx(), identity.ID) + identityProjects, err := dbCluster.GetIdentityProjects(ctx, tx.Tx(), identity.ID) if err != nil { return err } @@ -43,7 +739,7 @@ func updateIdentityCache(d *Daemon) { projects[identity.ID] = append(projects[identity.ID], p.Name) } - identityGroups, err := cluster.GetIdentityGroups(ctx, tx.Tx(), identity.ID) + identityGroups, err := dbCluster.GetAuthGroupsByIdentityID(ctx, tx.Tx(), identity.ID) if err != nil { return err } @@ -53,7 +749,7 @@ func updateIdentityCache(d *Daemon) { } } - idpGroups, err := cluster.GetIdentityProviderGroups(ctx, tx.Tx()) + idpGroups, err := dbCluster.GetIdentityProviderGroups(ctx, tx.Tx()) if err != nil { return err } @@ -75,7 +771,7 @@ func updateIdentityCache(d *Daemon) { } identityCacheEntries := make([]identity.CacheEntry, 0, len(identities)) - var localServerCerts []cluster.Certificate + var localServerCerts []dbCluster.Certificate for _, id := range identities { cacheEntry := identity.CacheEntry{ Identifier: id.Identifier, @@ -137,7 +833,7 @@ func updateIdentityCache(d *Daemon) { func updateIdentityCacheFromLocal(d *Daemon) error { logger.Debug("Refreshing identity cache with local trusted certificates") - var localServerCerts []cluster.Certificate + var localServerCerts []dbCluster.Certificate var err error err = d.State().DB.Node.Transaction(d.shutdownCtx, func(ctx context.Context, tx *db.NodeTx) error { From 7fd6dd1d20428f23ac2851f1e38e7e9bd389cc0a Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 26 Feb 2024 19:34:45 +0000 Subject: [PATCH 26/34] lxd: Add APIs for auth groups. Signed-off-by: Mark Laing --- lxd/api_1.0.go | 2 + lxd/auth_groups.go | 883 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 885 insertions(+) create mode 100644 lxd/auth_groups.go diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index f31398662558..8f65dbc77501 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -127,6 +127,8 @@ var api10 = []APIEndpoint{ identitiesCmd, identitiesByAuthenticationMethodCmd, identityCmd, + authGroupsCmd, + authGroupCmd, } // swagger:operation GET /1.0?public server server_get_untrusted diff --git a/lxd/auth_groups.go b/lxd/auth_groups.go new file mode 100644 index 000000000000..9051b4304308 --- /dev/null +++ b/lxd/auth_groups.go @@ -0,0 +1,883 @@ +package main + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/gorilla/mux" + + "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" + "github.com/canonical/lxd/lxd/cluster" + "github.com/canonical/lxd/lxd/db" + dbCluster "github.com/canonical/lxd/lxd/db/cluster" + "github.com/canonical/lxd/lxd/lifecycle" + "github.com/canonical/lxd/lxd/request" + "github.com/canonical/lxd/lxd/response" + "github.com/canonical/lxd/lxd/util" + "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/entity" +) + +var authGroupsCmd = APIEndpoint{ + Name: "auth_groups", + Path: "auth/groups", + Get: APIEndpointAction{ + Handler: getAuthGroups, + AccessHandler: allowAuthenticated, + }, + Post: APIEndpointAction{ + Handler: createAuthGroup, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanCreateGroups), + }, +} + +var authGroupCmd = APIEndpoint{ + Name: "auth_group", + Path: "auth/groups/{groupName}", + Get: APIEndpointAction{ + Handler: getAuthGroup, + AccessHandler: allowPermission(entity.TypeAuthGroup, auth.EntitlementCanView, "groupName"), + }, + Put: APIEndpointAction{ + Handler: updateAuthGroup, + AccessHandler: allowPermission(entity.TypeAuthGroup, auth.EntitlementCanEdit, "groupName"), + }, + Post: APIEndpointAction{ + Handler: renameAuthGroup, + AccessHandler: allowPermission(entity.TypeAuthGroup, auth.EntitlementCanEdit, "groupName"), + }, + Delete: APIEndpointAction{ + Handler: deleteAuthGroup, + AccessHandler: allowPermission(entity.TypeAuthGroup, auth.EntitlementCanDelete, "groupName"), + }, + Patch: APIEndpointAction{ + Handler: patchAuthGroup, + AccessHandler: allowPermission(entity.TypeAuthGroup, auth.EntitlementCanEdit, "groupName"), + }, +} + +func validateGroupName(name string) error { + if name == "" { + return api.StatusErrorf(http.StatusBadRequest, "Group name cannot be empty") + } + + if strings.Contains(name, "/") { + return api.StatusErrorf(http.StatusBadRequest, "Group name cannot contain a forward slash") + } + + if strings.Contains(name, ":") { + return api.StatusErrorf(http.StatusBadRequest, "Group name cannot contain a colon") + } + + return nil +} + +// swagger:operation GET /1.0/auth/groups auth_groups auth_groups_get +// +// Get the groups +// +// Returns a list of authorization groups (URLs). +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of endpoints +// items: +// type: string +// example: |- +// [ +// "/1.0/auth/groups/foo", +// "/1.0/auth/groups/bar" +// ] +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" + +// swagger:operation GET /1.0/auth/groups?recursion=1 auth_groups auth_groups_get_recursion1 +// +// Get the groups +// +// Returns a list of authorization groups. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of auth groups +// items: +// $ref: "#/definitions/AuthGroup" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func getAuthGroups(d *Daemon, r *http.Request) response.Response { + recursion := request.QueryParam(r, "recursion") + s := d.State() + + hasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanViewGroups, entity.TypeAuthGroup) + if err != nil { + return response.SmartError(fmt.Errorf("Failed to get a permission checker: %w", err)) + } + + var groups []dbCluster.AuthGroup + groupsPermissions := make(map[int][]dbCluster.Permission) + groupsIdentities := make(map[int][]dbCluster.Identity) + groupsIdentityProviderGroups := make(map[int][]dbCluster.IdentityProviderGroup) + entityURLs := make(map[entity.Type]map[int]*api.URL) + err = d.db.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + allGroups, err := dbCluster.GetAuthGroups(ctx, tx.Tx()) + if err != nil { + return err + } + + groups = make([]dbCluster.AuthGroup, 0, len(groups)) + for _, group := range allGroups { + if hasPermission(entity.AuthGroupURL(group.Name)) { + groups = append(groups, group) + } + } + + if len(groups) == 0 { + return nil + } + + if recursion == "1" { + // If recursing, we need all identities for all groups, all IDP groups for all groups, + // all permissions for all groups, and finally the URLs that those permissions apply to. + groupsIdentities, err = dbCluster.GetAllIdentitiesByAuthGroupIDs(ctx, tx.Tx()) + if err != nil { + return err + } + + groupsIdentityProviderGroups, err = dbCluster.GetAllIdentityProviderGroupsByGroupIDs(ctx, tx.Tx()) + if err != nil { + return err + } + + groupsPermissions, err = dbCluster.GetAllPermissionsByAuthGroupIDs(ctx, tx.Tx()) + if err != nil { + return err + } + + // allGroupPermissions is a de-duplicated slice of permissions. + var allGroupPermissions []dbCluster.Permission + for _, groupPermissions := range groupsPermissions { + for _, permission := range groupPermissions { + if !shared.ValueInSlice(permission, allGroupPermissions) { + allGroupPermissions = append(allGroupPermissions, permission) + } + } + } + + // EntityURLs is a map of entity type, to entity ID, to api.URL. + entityURLs, err = dbCluster.GetPermissionEntityURLs(ctx, tx.Tx(), allGroupPermissions) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + if recursion == "1" { + apiGroups := make([]api.AuthGroup, 0, len(groups)) + for _, group := range groups { + var apiPermissions []api.Permission + + // The group may not have any permissions. + permissions, ok := groupsPermissions[group.ID] + if ok { + apiPermissions = make([]api.Permission, 0, len(permissions)) + for _, permission := range permissions { + // Expect to find any permissions in the entity URL map by its entity type and entity ID. + entityIDToURL, ok := entityURLs[entity.Type(permission.EntityType)] + if !ok { + return response.InternalError(fmt.Errorf("Entity URLs missing for permissions with entity type %q", permission.EntityType)) + } + + apiURL, ok := entityIDToURL[permission.EntityID] + if !ok { + return response.InternalError(fmt.Errorf("Entity URL missing for permission with entity type %q and entity ID `%d`", permission.EntityType, permission.EntityID)) + } + + apiPermissions = append(apiPermissions, api.Permission{ + EntityType: string(permission.EntityType), + EntityReference: apiURL.String(), + Entitlement: string(permission.Entitlement), + }) + } + } + + apiIdentities := make([]api.Identity, 0, len(groupsIdentities[group.ID])) + for _, identity := range groupsIdentities[group.ID] { + apiIdentities = append(apiIdentities, api.Identity{ + AuthenticationMethod: string(identity.AuthMethod), + Type: string(identity.Type), + Identifier: identity.Identifier, + Name: identity.Name, + }) + } + + idpGroups := make([]string, 0, len(groupsIdentityProviderGroups[group.ID])) + for _, idpGroup := range groupsIdentityProviderGroups[group.ID] { + idpGroups = append(idpGroups, idpGroup.Name) + } + + apiGroups = append(apiGroups, api.AuthGroup{ + AuthGroupsPost: api.AuthGroupsPost{ + AuthGroupPost: api.AuthGroupPost{Name: group.Name}, + AuthGroupPut: api.AuthGroupPut{ + Description: group.Description, + Permissions: apiPermissions, + }, + }, + Identities: apiIdentities, + IdentityProviderGroups: idpGroups, + }) + } + + return response.SyncResponse(true, apiGroups) + } + + groupURLs := make([]string, 0, len(groups)) + for _, group := range groups { + groupURLs = append(groupURLs, entity.AuthGroupURL(group.Name).String()) + } + + return response.SyncResponse(true, groupURLs) +} + +// swagger:operation POST /1.0/auth/groups auth_groups auth_groups_post +// +// Create a new authorization group +// +// Creates a new authorization group. +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: group +// description: Group request +// required: true +// schema: +// $ref: "#/definitions/AuthGroupsPost" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func createAuthGroup(d *Daemon, r *http.Request) response.Response { + var group api.AuthGroupsPost + err := json.NewDecoder(r.Body).Decode(&group) + if err != nil { + return response.BadRequest(fmt.Errorf("Invalid request body: %w", err)) + } + + err = validateGroupName(group.Name) + if err != nil { + return response.SmartError(err) + } + + err = validatePermissions(group.Permissions) + if err != nil { + return response.SmartError(err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + s := d.State() + err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { + groupID, err := dbCluster.CreateAuthGroup(ctx, tx.Tx(), dbCluster.AuthGroup{ + Name: group.Name, + Description: group.Description, + }) + if err != nil { + return err + } + + permissionIDs, err := upsertPermissions(ctx, tx.Tx(), group.Permissions) + if err != nil { + return err + } + + err = dbCluster.SetAuthGroupPermissions(ctx, tx.Tx(), int(groupID), permissionIDs) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the group creation + lc := lifecycle.AuthGroupCreated.Event(group.Name, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + return response.SyncResponseLocation(true, nil, entity.AuthGroupURL(group.Name).String()) +} + +// swagger:operation GET /1.0/auth/groups/{groupName} auth_groups auth_group_get +// +// Get the authorization group +// +// Gets a specific authorization group. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// $ref: "#/definitions/AuthGroup" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func getAuthGroup(d *Daemon, r *http.Request) response.Response { + groupName, err := url.PathUnescape(mux.Vars(r)["groupName"]) + if err != nil { + return response.SmartError(err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + var apiGroup *api.AuthGroup + s := d.State() + err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { + group, err := dbCluster.GetAuthGroup(ctx, tx.Tx(), groupName) + if err != nil { + return err + } + + apiGroup, err = group.ToAPI(ctx, tx.Tx()) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + return response.SyncResponseETag(true, *apiGroup, *apiGroup) +} + +// swagger:operation PUT /1.0/auth/groups/{groupName} auth_groups auth_group_put +// +// Update the authorization group +// +// Replaces the editable fields of an authorization group +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: group +// description: Update request +// schema: +// $ref: "#/definitions/AuthGroupPut" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func updateAuthGroup(d *Daemon, r *http.Request) response.Response { + groupName, err := url.PathUnescape(mux.Vars(r)["groupName"]) + if err != nil { + return response.SmartError(err) + } + + var groupPut api.AuthGroupPut + err = json.NewDecoder(r.Body).Decode(&groupPut) + if err != nil { + return response.BadRequest(fmt.Errorf("Invalid request body: %w", err)) + } + + err = validatePermissions(groupPut.Permissions) + if err != nil { + return response.SmartError(err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + s := d.State() + err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { + group, err := dbCluster.GetAuthGroup(ctx, tx.Tx(), groupName) + if err != nil { + return err + } + + apiGroup, err := group.ToAPI(ctx, tx.Tx()) + if err != nil { + return err + } + + err = util.EtagCheck(r, *apiGroup) + if err != nil { + return err + } + + err = dbCluster.UpdateAuthGroup(ctx, tx.Tx(), groupName, dbCluster.AuthGroup{ + Name: groupName, + Description: groupPut.Description, + }) + if err != nil { + return err + } + + permissionIDs, err := upsertPermissions(ctx, tx.Tx(), groupPut.Permissions) + if err != nil { + return err + } + + err = dbCluster.SetAuthGroupPermissions(ctx, tx.Tx(), group.ID, permissionIDs) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the group update + lc := lifecycle.AuthGroupUpdated.Event(groupName, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + return response.EmptySyncResponse +} + +// swagger:operation PATCH /1.0/auth/groups/{groupName} auth_groups auth_group_patch +// +// Partially update the authorization group +// +// Updates the editable fields of an authorization group +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: group +// description: Update request +// schema: +// $ref: "#/definitions/AuthGroupPut" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func patchAuthGroup(d *Daemon, r *http.Request) response.Response { + groupName, err := url.PathUnescape(mux.Vars(r)["groupName"]) + if err != nil { + return response.SmartError(err) + } + + var groupPut api.AuthGroupPut + err = json.NewDecoder(r.Body).Decode(&groupPut) + if err != nil { + return response.BadRequest(fmt.Errorf("Invalid request body: %w", err)) + } + + err = validatePermissions(groupPut.Permissions) + if err != nil { + return response.SmartError(err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + s := d.State() + err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { + group, err := dbCluster.GetAuthGroup(ctx, tx.Tx(), groupName) + if err != nil { + return err + } + + apiGroup, err := group.ToAPI(ctx, tx.Tx()) + if err != nil { + return err + } + + err = util.EtagCheck(r, *apiGroup) + if err != nil { + return err + } + + if groupPut.Description != "" { + err = dbCluster.UpdateAuthGroup(ctx, tx.Tx(), groupName, dbCluster.AuthGroup{ + Name: groupName, + Description: groupPut.Description, + }) + if err != nil { + return err + } + } + + newPermissions := make([]api.Permission, 0, len(groupPut.Permissions)) + for _, permission := range groupPut.Permissions { + if !shared.ValueInSlice(permission, apiGroup.Permissions) { + newPermissions = append(newPermissions, permission) + } + } + + permissionIDs, err := upsertPermissions(ctx, tx.Tx(), newPermissions) + if err != nil { + return err + } + + err = dbCluster.SetAuthGroupPermissions(ctx, tx.Tx(), group.ID, permissionIDs) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the group update + lc := lifecycle.AuthGroupUpdated.Event(groupName, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + return response.EmptySyncResponse +} + +// swagger:operation POST /1.0/auth/groups/{groupName} auth_groups auth_group_post +// +// Rename the authorization group +// +// Renames the authorization group +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: group +// description: Update request +// schema: +// $ref: "#/definitions/AuthGroupPost" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func renameAuthGroup(d *Daemon, r *http.Request) response.Response { + groupName, err := url.PathUnescape(mux.Vars(r)["groupName"]) + if err != nil { + return response.SmartError(err) + } + + var groupPost api.AuthGroupPost + err = json.NewDecoder(r.Body).Decode(&groupPost) + if err != nil { + return response.BadRequest(fmt.Errorf("Invalid request body: %w", err)) + } + + err = validateGroupName(groupPost.Name) + if err != nil { + return response.SmartError(err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + s := d.State() + err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { + err = dbCluster.RenameAuthGroup(ctx, tx.Tx(), groupName, groupPost.Name) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // When a group is renamed we need to update the list of group names associated with each identity in the cache. + // When a group is otherwise modified, the name is unchanged, so the cache doesn't need to be updated. + // When a group is created, no identities are a member of it yet, so the cache doesn't need to be updated. + s.UpdateIdentityCache() + + // Send a lifecycle event for the group rename + lc := lifecycle.AuthGroupRenamed.Event(groupPost.Name, request.CreateRequestor(r), map[string]any{"old_name": groupName}) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + return response.SyncResponseLocation(true, nil, entity.AuthGroupURL(groupPost.Name).String()) +} + +// swagger:operation DELETE /1.0/auth/groups/{groupName} auth_groups auth_group_delete +// +// Delete the authorization group +// +// Deletes the authorization group +// +// --- +// produces: +// - application/json +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func deleteAuthGroup(d *Daemon, r *http.Request) response.Response { + groupName, err := url.PathUnescape(mux.Vars(r)["groupName"]) + if err != nil { + return response.SmartError(err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + s := d.State() + err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { + return dbCluster.DeleteAuthGroup(ctx, tx.Tx(), groupName) + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // When a group is deleted we need to remove it from the list of groups names associated with each identity in the cache. + // (When a group is created, nobody is a member of it yet, so the cache doesn't need to be updated). + s.UpdateIdentityCache() + + // Send a lifecycle event for the group deletion + lc := lifecycle.AuthGroupDeleted.Event(groupName, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + return response.EmptySyncResponse +} + +// validatePermissions checks that a) the entity type exists, b) the entitlement exists, c) then entity type matches the +// entity reference (URL), and d) that the entitlement is valid for the entity type. +func validatePermissions(permissions []api.Permission) error { + for _, permission := range permissions { + entityType := entity.Type(permission.EntityType) + err := entityType.Validate() + if err != nil { + return api.StatusErrorf(http.StatusBadRequest, "Failed to validate entity type for permission with entity reference %q and entitlement %q: %v", permission.EntityReference, permission.Entitlement, err) + } + + entitlement := auth.Entitlement(permission.Entitlement) + err = auth.Validate(entitlement) + if err != nil { + return api.StatusErrorf(http.StatusBadRequest, "Failed to validate entitlement for permission with entity reference %q and entitlement %q: %v", permission.EntityReference, permission.Entitlement, err) + } + + u, err := url.Parse(permission.EntityReference) + if err != nil { + return api.StatusErrorf(http.StatusBadRequest, "Failed to parse permission with entity reference %q and entitlement %q: %v", permission.EntityReference, permission.Entitlement, err) + } + + referenceEntityType, _, _, _, err := entity.ParseURL(*u) + if err != nil { + return api.StatusErrorf(http.StatusBadRequest, "Failed to parse permission with entity reference %q and entitlement %q: %v", permission.EntityReference, permission.Entitlement, err) + } + + if entityType != referenceEntityType { + return api.StatusErrorf(http.StatusBadRequest, "Failed to parse permission with entity reference %q and entitlement %q: Entity type does not correspond to entity reference", permission.EntityReference, permission.Entitlement) + } + + err = auth.ValidateEntitlement(entityType, entitlement) + if err != nil { + return api.StatusErrorf(http.StatusBadRequest, "Failed to validate group permission with entity reference %q and entitlement %q: %v", permission.EntityReference, permission.Entitlement, err) + } + } + + return nil +} + +// upsertPermissions resolves the URLs of each permission to an entity ID and checks if the permission already +// exists (it may be assigned to another group already). If the permission does not already exist, it is created. +// A slice of permission IDs is returned that can be used to associate these permissions to a group. +func upsertPermissions(ctx context.Context, tx *sql.Tx, permissions []api.Permission) ([]int, error) { + entityReferences := make(map[*api.URL]*dbCluster.EntityRef, len(permissions)) + permissionToURL := make(map[api.Permission]*api.URL, len(permissions)) + for _, permission := range permissions { + u, err := url.Parse(permission.EntityReference) + if err != nil { + return nil, fmt.Errorf("Failed to parse permission entity reference: %w", err) + } + + apiURL := &api.URL{URL: *u} + entityReferences[apiURL] = &dbCluster.EntityRef{} + permissionToURL[permission] = apiURL + } + + err := dbCluster.PopulateEntityReferencesFromURLs(ctx, tx, entityReferences) + if err != nil { + return nil, err + } + + var permissionIDs []int + for permission, apiURL := range permissionToURL { + entitlement := auth.Entitlement(permission.Entitlement) + entityType := dbCluster.EntityType(permission.EntityType) + entityRef, ok := entityReferences[apiURL] + if !ok { + return nil, fmt.Errorf("Missing entity ID for permission with URL %q", permission.EntityReference) + } + + // Get the permission, if one is found, append its ID to the slice. + existingPermission, err := dbCluster.GetPermission(ctx, tx, entitlement, entityType, entityRef.EntityID) + if err == nil { + permissionIDs = append(permissionIDs, existingPermission.ID) + continue + } else if !api.StatusErrorCheck(err, http.StatusNotFound) { + return nil, fmt.Errorf("Failed to check if permission with entitlement %q and URL %q already exists: %w", entitlement, permission.EntityReference, err) + } + + // Generated "create" methods call cluster.GetPermission again to check if it exists. We already know that it doesn't exist, so create it directly. + res, err := tx.ExecContext(ctx, `INSERT INTO permissions (entitlement, entity_type, entity_id) VALUES (?, ?, ?)`, entitlement, entityType, entityRef.EntityID) + if err != nil { + return nil, fmt.Errorf("Failed to insert new permission: %w", err) + } + + lastInsertID, err := res.LastInsertId() + if err != nil { + return nil, fmt.Errorf("Failed to get last insert ID of new permission: %w", err) + } + + permissionIDs = append(permissionIDs, int(lastInsertID)) + } + + return permissionIDs, nil +} From 6fd833eed3ebd7faddf582ddfa116ddc85e492aa Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 26 Feb 2024 19:35:20 +0000 Subject: [PATCH 27/34] lxd: Add APIs for identity provider groups. Signed-off-by: Mark Laing --- lxd/api_1.0.go | 2 + lxd/identity_provider_groups.go | 619 ++++++++++++++++++++++++++++++++ 2 files changed, 621 insertions(+) create mode 100644 lxd/identity_provider_groups.go diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index 8f65dbc77501..cea762a8f8de 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -129,6 +129,8 @@ var api10 = []APIEndpoint{ identityCmd, authGroupsCmd, authGroupCmd, + identityProviderGroupsCmd, + identityProviderGroupCmd, } // swagger:operation GET /1.0?public server server_get_untrusted diff --git a/lxd/identity_provider_groups.go b/lxd/identity_provider_groups.go new file mode 100644 index 000000000000..cc5ea623b64c --- /dev/null +++ b/lxd/identity_provider_groups.go @@ -0,0 +1,619 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/gorilla/mux" + + "github.com/canonical/lxd/client" + "github.com/canonical/lxd/lxd/auth" + "github.com/canonical/lxd/lxd/cluster" + "github.com/canonical/lxd/lxd/db" + dbCluster "github.com/canonical/lxd/lxd/db/cluster" + "github.com/canonical/lxd/lxd/lifecycle" + "github.com/canonical/lxd/lxd/request" + "github.com/canonical/lxd/lxd/response" + "github.com/canonical/lxd/lxd/util" + "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/entity" +) + +var identityProviderGroupsCmd = APIEndpoint{ + Name: "identity_provider_groups", + Path: "auth/identity-provider-groups", + Get: APIEndpointAction{ + Handler: getIdentityProviderGroups, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanViewGroups), + }, + Post: APIEndpointAction{ + Handler: createIdentityProviderGroup, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanEditGroups), + }, +} + +var identityProviderGroupCmd = APIEndpoint{ + Name: "identity_provider_group", + Path: "auth/identity-provider-groups/{idpGroupName}", + Get: APIEndpointAction{ + Handler: getIdentityProviderGroup, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanViewGroups), + }, + Put: APIEndpointAction{ + Handler: updateIdentityProviderGroup, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanEditGroups), + }, + Post: APIEndpointAction{ + Handler: renameIdentityProviderGroup, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanEditGroups), + }, + Delete: APIEndpointAction{ + Handler: deleteIdentityProviderGroup, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanEditGroups), + }, + Patch: APIEndpointAction{ + Handler: patchIdentityProviderGroup, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanEditGroups), + }, +} + +// swagger:operation GET /1.0/auth/identity-provider-groups identity_provider_groups identity_provider_groups_get +// +// Get the identity provider groups +// +// Returns a list of identity provider groups (URLs). +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of endpoints +// items: +// type: string +// example: |- +// [ +// "/1.0/auth/identity-provider-groups/sales", +// "/1.0/auth/identity-provider-groups/operations" +// ] +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" + +// swagger:operation GET /1.0/auth/identity-provider-groups?recursion=1 identity_provider_groups identity_provider_groups_get_recursion1 +// +// Get the groups +// +// Returns a list of identity provider groups. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of identity provider groups +// items: +// $ref: "#/definitions/IdentityProviderGroup" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func getIdentityProviderGroups(d *Daemon, r *http.Request) response.Response { + recursion := r.URL.Query().Get("recursion") + s := d.State() + var apiIDPGroups []*api.IdentityProviderGroup + var idpGroups []dbCluster.IdentityProviderGroup + err := s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + var err error + idpGroups, err = dbCluster.GetIdentityProviderGroups(ctx, tx.Tx()) + if err != nil { + return err + } + + if recursion == "1" { + apiIDPGroups = make([]*api.IdentityProviderGroup, 0, len(idpGroups)) + for _, idpGroup := range idpGroups { + apiIDPGroup, err := idpGroup.ToAPI(ctx, tx.Tx()) + if err != nil { + return err + } + + apiIDPGroups = append(apiIDPGroups, apiIDPGroup) + } + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + if recursion == "1" { + return response.SyncResponse(true, apiIDPGroups) + } + + idpGroupURLs := make([]string, 0, len(idpGroups)) + for _, idpGroup := range idpGroups { + idpGroupURLs = append(idpGroupURLs, entity.IdentityProviderGroupURL(idpGroup.Name).String()) + } + + return response.SyncResponse(true, idpGroupURLs) +} + +// swagger:operation GET /1.0/auth/identity-provider-groups/{idpGroupName} identity_provider_groups identity_provider_group_get +// +// Get the identity provider group +// +// Gets a specific identity provider group. +// +// --- +// produces: +// - application/json +// responses: +// "200": +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// $ref: "#/definitions/IdentityProviderGroup" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func getIdentityProviderGroup(d *Daemon, r *http.Request) response.Response { + idpGroupName, err := url.PathUnescape(mux.Vars(r)["idpGroupName"]) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to unescape identity provider group name path parameter: %w", err)) + } + + s := d.State() + var apiIDPGroup *api.IdentityProviderGroup + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + idpGroup, err := dbCluster.GetIdentityProviderGroup(ctx, tx.Tx(), idpGroupName) + if err != nil { + return err + } + + apiIDPGroup, err = idpGroup.ToAPI(ctx, tx.Tx()) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + return response.SyncResponseETag(true, apiIDPGroup, apiIDPGroup) +} + +// swagger:operation POST /1.0/auth/identity-provider-groups identity_provider_groups identity_provider_groups_post +// +// Create a new identity provider group +// +// Creates a new identity provider group. +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: group +// description: Identity provider request +// required: true +// schema: +// $ref: "#/definitions/IdentityProviderGroup" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func createIdentityProviderGroup(d *Daemon, r *http.Request) response.Response { + var idpGroup api.IdentityProviderGroup + err := json.NewDecoder(r.Body).Decode(&idpGroup) + if err != nil { + return response.BadRequest(fmt.Errorf("Failed to unmarshal request body: %w", err)) + } + + s := d.State() + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + id, err := dbCluster.CreateIdentityProviderGroup(ctx, tx.Tx(), dbCluster.IdentityProviderGroup{Name: idpGroup.Name}) + if err != nil { + return err + } + + err = dbCluster.SetIdentityProviderGroupMapping(ctx, tx.Tx(), int(id), idpGroup.Groups) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + s.UpdateIdentityCache() + + // Send a lifecycle event for the IDP group creation. + lc := lifecycle.IdentityProviderGroupCreated.Event(idpGroup.Name, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + return response.SyncResponseLocation(true, nil, entity.IdentityProviderGroupURL(idpGroup.Name).String()) +} + +// swagger:operation POST /1.0/auth/identity-provider-groups/{idpGroupName} identity_provider_groups identity_provider_group_post +// +// Rename the identity provider group +// +// Renames the identity provider group +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: group +// description: Update request +// schema: +// $ref: "#/definitions/IdentityProviderGroupPost" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func renameIdentityProviderGroup(d *Daemon, r *http.Request) response.Response { + idpGroupName, err := url.PathUnescape(mux.Vars(r)["idpGroupName"]) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to unescape path argument: %w", err)) + } + + var idpGroupPost api.IdentityProviderGroupPost + err = json.NewDecoder(r.Body).Decode(&idpGroupPost) + if err != nil { + return response.BadRequest(fmt.Errorf("Failed to unmarshal request body: %w", err)) + } + + s := d.State() + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + return dbCluster.RenameIdentityProviderGroup(ctx, tx.Tx(), idpGroupName, idpGroupPost.Name) + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the IDP group rename. + lc := lifecycle.IdentityProviderGroupRenamed.Event(idpGroupPost.Name, request.CreateRequestor(r), map[string]any{"old_name": idpGroupName}) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + s.UpdateIdentityCache() + + return response.SyncResponseLocation(true, nil, entity.IdentityProviderGroupURL(idpGroupPost.Name).String()) +} + +// swagger:operation PUT /1.0/auth/identity-provider-groups/{idpGroupName} identity_provider_groups identity_provider_group_put +// +// Update the identity provider group +// +// Replaces the editable fields of an identity provider group +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: group +// description: Update request +// schema: +// $ref: "#/definitions/IdentityProviderGroupPut" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func updateIdentityProviderGroup(d *Daemon, r *http.Request) response.Response { + idpGroupName, err := url.PathUnescape(mux.Vars(r)["idpGroupName"]) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to unescape path argument: %w", err)) + } + + var idpGroupPut api.IdentityProviderGroupPut + err = json.NewDecoder(r.Body).Decode(&idpGroupPut) + if err != nil { + return response.BadRequest(fmt.Errorf("Failed to unmarshal request body: %w", err)) + } + + s := d.State() + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + idpGroup, err := dbCluster.GetIdentityProviderGroup(ctx, tx.Tx(), idpGroupName) + if err != nil { + return err + } + + apiIDPGroup, err := idpGroup.ToAPI(ctx, tx.Tx()) + if err != nil { + return err + } + + err = util.EtagCheck(r, apiIDPGroup) + if err != nil { + return err + } + + return dbCluster.SetIdentityProviderGroupMapping(ctx, tx.Tx(), idpGroup.ID, idpGroupPut.Groups) + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the IDP group update. + lc := lifecycle.IdentityProviderGroupUpdated.Event(idpGroupName, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + s.UpdateIdentityCache() + + return response.EmptySyncResponse +} + +// swagger:operation PATCH /1.0/auth/identity-provider-groups/{idpGroupName} identity_provider_groups identity_provider_group_patch +// +// Partially update the identity provider group +// +// Updates the editable fields of an identity provider group +// +// --- +// consumes: +// - application/json +// produces: +// - application/json +// parameters: +// - in: body +// name: group +// description: Update request +// schema: +// $ref: "#/definitions/IdentityProviderGroupPut" +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func patchIdentityProviderGroup(d *Daemon, r *http.Request) response.Response { + idpGroupName, err := url.PathUnescape(mux.Vars(r)["idpGroupName"]) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to unescape path argument: %w", err)) + } + + var idpGroupPut api.IdentityProviderGroupPut + err = json.NewDecoder(r.Body).Decode(&idpGroupPut) + if err != nil { + return response.BadRequest(fmt.Errorf("Failed to unmarshal request body: %w", err)) + } + + s := d.State() + var apiIDPGroup *api.IdentityProviderGroup + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + idpGroup, err := dbCluster.GetIdentityProviderGroup(ctx, tx.Tx(), idpGroupName) + if err != nil { + return err + } + + apiIDPGroup, err = idpGroup.ToAPI(ctx, tx.Tx()) + if err != nil { + return err + } + + err = util.EtagCheck(r, apiIDPGroup) + if err != nil { + return err + } + + for _, newGroup := range idpGroupPut.Groups { + if !shared.ValueInSlice(newGroup, apiIDPGroup.Groups) { + apiIDPGroup.Groups = append(apiIDPGroup.Groups, newGroup) + } + } + + return dbCluster.SetIdentityProviderGroupMapping(ctx, tx.Tx(), idpGroup.ID, apiIDPGroup.Groups) + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the IDP group update. + lc := lifecycle.IdentityProviderGroupUpdated.Event(idpGroupName, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + s.UpdateIdentityCache() + + return response.EmptySyncResponse +} + +// swagger:operation DELETE /1.0/auth/identity-provider-groups/{idpGroupName} identity_provider_groups identity_provider_group_delete +// +// Delete the identity provider group +// +// Deletes the identity provider group +// +// --- +// produces: +// - application/json +// responses: +// "200": +// $ref: "#/responses/EmptySyncResponse" +// "400": +// $ref: "#/responses/BadRequest" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func deleteIdentityProviderGroup(d *Daemon, r *http.Request) response.Response { + idpGroupName, err := url.PathUnescape(mux.Vars(r)["idpGroupName"]) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to unescape path argument: %w", err)) + } + + s := d.State() + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + return dbCluster.DeleteIdentityProviderGroup(ctx, tx.Tx(), idpGroupName) + }) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Send a lifecycle event for the IDP group deletion. + lc := lifecycle.IdentityProviderGroupDeleted.Event(idpGroupName, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + s.UpdateIdentityCache() + + return response.EmptySyncResponse +} From 85635957768022a2a6a0886d34d6865d21ad8d62 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 17:39:04 +0000 Subject: [PATCH 28/34] lxd: Add the permissions handler. Signed-off-by: Mark Laing --- lxd/api_1.0.go | 1 + lxd/permissions.go | 241 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 lxd/permissions.go diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index cea762a8f8de..dd05fc446f69 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -131,6 +131,7 @@ var api10 = []APIEndpoint{ authGroupCmd, identityProviderGroupsCmd, identityProviderGroupCmd, + permissionsCmd, } // swagger:operation GET /1.0?public server server_get_untrusted diff --git a/lxd/permissions.go b/lxd/permissions.go new file mode 100644 index 000000000000..b5d871c9c52f --- /dev/null +++ b/lxd/permissions.go @@ -0,0 +1,241 @@ +package main + +import ( + "context" + "fmt" + "net/http" + + "github.com/canonical/lxd/lxd/auth" + "github.com/canonical/lxd/lxd/db" + "github.com/canonical/lxd/lxd/db/cluster" + "github.com/canonical/lxd/lxd/response" + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/entity" +) + +var permissionsCmd = APIEndpoint{ + Name: "permissions", + Path: "auth/permissions", + Get: APIEndpointAction{ + Handler: getPermissions, + AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanViewPermissions), + }, +} + +// swagger:operation GET /1.0/auth/permissions?recursion=1 permissions permissions_get_recursion1 +// +// Get the permissions +// +// Returns a list of available permissions (including groups that have those permissions). +// +// --- +// produces: +// - application/json +// parameters: +// - in: query +// name: project +// description: Project name +// type: string +// example: default +// - in: query +// name: entity-type +// description: Type of entity +// type: string +// example: instance +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of permissions +// items: +// $ref: "#/definitions/PermissionInfo" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" + +// swagger:operation GET /1.0/auth/permissions permissions permissions_get +// +// Get the permissions +// +// Returns a list of available permissions. +// +// --- +// produces: +// - application/json +// parameters: +// - in: query +// name: project +// description: Project name +// type: string +// example: default +// - in: query +// name: entityType +// description: Type of entity +// type: string +// example: instance +// responses: +// "200": +// description: API endpoints +// schema: +// type: object +// description: Sync response +// properties: +// type: +// type: string +// description: Response type +// example: sync +// status: +// type: string +// description: Status description +// example: Success +// status_code: +// type: integer +// description: Status code +// example: 200 +// metadata: +// type: array +// description: List of permissions +// items: +// $ref: "#/definitions/Permission" +// "403": +// $ref: "#/responses/Forbidden" +// "500": +// $ref: "#/responses/InternalServerError" +func getPermissions(d *Daemon, r *http.Request) response.Response { + projectNameFilter := r.URL.Query().Get("project") + entityTypeFilter := r.URL.Query().Get("entity-type") + recursion := r.URL.Query().Get("recursion") + var entityTypes []entity.Type + if entityTypeFilter != "" { + entityType := entity.Type(entityTypeFilter) + err := entityType.Validate() + if err != nil { + return response.BadRequest(fmt.Errorf("Invalid `entity-type` query parameter %q: %w", entityTypeFilter, err)) + } + + entityTypes = append(entityTypes, entityType) + } + + var entityURLs map[entity.Type]map[int]*api.URL + var permissions []cluster.Permission + var groupsByPermissionID map[int][]cluster.AuthGroup + err := d.State().DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + var err error + if projectNameFilter != "" { + // Validate that the project exists first. + _, err = cluster.GetProject(ctx, tx.Tx(), projectNameFilter) + if err != nil { + return err + } + } + + if recursion == "1" { + permissions, err = cluster.GetPermissions(ctx, tx.Tx()) + if err != nil { + return fmt.Errorf("Failed to get currently assigned permissions: %w", err) + } + + groupsByPermissionID, err = cluster.GetAllAuthGroupsByPermissionID(ctx, tx.Tx()) + if err != nil { + return fmt.Errorf("Failed to get groups by permission mapping: %w", err) + } + } + + entityURLs, err = cluster.GetEntityURLs(ctx, tx.Tx(), projectNameFilter, entityTypes...) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return response.SmartError(err) + } + + // If we're recursing, convert the groupsByPermissionID map into a map of cluster.Permission to list of group names. + assignedPermissions := make(map[cluster.Permission][]string, len(groupsByPermissionID)) + if recursion == "1" { + for permissionID, groups := range groupsByPermissionID { + var perm cluster.Permission + for _, p := range permissions { + if permissionID == p.ID { + perm = p + + // A permission is unique via its entity ID, entity type, and entitlement. Set the ID to zero + // so we can create a map key from the entityURL map below. + perm.ID = 0 + break + } + } + + groupNames := make([]string, 0, len(groups)) + for _, g := range groups { + groupNames = append(groupNames, g.Name) + } + + assignedPermissions[perm] = groupNames + } + } + + var apiPermissions []api.Permission + var apiPermissionInfos []api.PermissionInfo + for entityType, entities := range entityURLs { + for entityID, entityURL := range entities { + entitlements, err := auth.EntitlementsByEntityType(entityType) + if err != nil { + return response.InternalError(fmt.Errorf("Failed to list available entitlements for entity type %q: %w", entityType, err)) + } + + for _, entitlement := range entitlements { + if recursion == "1" { + permissionInfo := api.PermissionInfo{ + Permission: api.Permission{ + EntityType: string(entityType), + EntityReference: entityURL.String(), + Entitlement: string(entitlement), + }, + // Get the groups from the assigned permissions map. We don't have the permission ID in scope + // here. Thats why we set it to zero above. + Groups: assignedPermissions[cluster.Permission{ + Entitlement: entitlement, + EntityType: cluster.EntityType(entityType), + EntityID: entityID, + }], + } + + apiPermissionInfos = append(apiPermissionInfos, permissionInfo) + } else { + apiPermissions = append(apiPermissions, api.Permission{ + EntityType: string(entityType), + EntityReference: entityURL.String(), + Entitlement: string(entitlement), + }) + } + } + } + } + + if recursion == "1" { + return response.SyncResponse(true, apiPermissionInfos) + } + + return response.SyncResponse(true, apiPermissions) +} From df05ddf7b253939eaf17e0cf888e9a8218623b01 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 18:14:21 +0000 Subject: [PATCH 29/34] doc: Runs make update-api. Signed-off-by: Mark Laing --- doc/rest-api.yaml | 1130 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1130 insertions(+) diff --git a/doc/rest-api.yaml b/doc/rest-api.yaml index 73b5396c7b44..da705d3b9a5d 100644 --- a/doc/rest-api.yaml +++ b/doc/rest-api.yaml @@ -1,4 +1,89 @@ definitions: + AuthGroup: + properties: + description: + description: Description is a short description of the group. + example: Viewers of instance c1 in the default project. + type: string + x-go-name: Description + identities: + description: Identities are the identities that are members of the group. + items: + $ref: '#/definitions/Identity' + type: array + x-go-name: Identities + identity_provider_groups: + description: |- + IdentityProviderGroups are a list of groups from the IdP whose mapping + includes this group. + example: + - sales + - operations + items: + type: string + type: array + x-go-name: IdentityProviderGroups + name: + description: Name is the name of the group. + example: default-c1-viewers + type: string + x-go-name: Name + permissions: + description: Permissions are a list of permissions. + items: + $ref: '#/definitions/Permission' + type: array + x-go-name: Permissions + title: AuthGroup is the type for a LXD group. + type: object + x-go-package: github.com/canonical/lxd/shared/api + AuthGroupPost: + properties: + name: + description: Name is the name of the group. + example: default-c1-viewers + type: string + x-go-name: Name + title: AuthGroupPost is used for renaming a group. + type: object + x-go-package: github.com/canonical/lxd/shared/api + AuthGroupPut: + properties: + description: + description: Description is a short description of the group. + example: Viewers of instance c1 in the default project. + type: string + x-go-name: Description + permissions: + description: Permissions are a list of permissions. + items: + $ref: '#/definitions/Permission' + type: array + x-go-name: Permissions + title: AuthGroupPut contains the editable fields of a group. + type: object + x-go-package: github.com/canonical/lxd/shared/api + AuthGroupsPost: + properties: + description: + description: Description is a short description of the group. + example: Viewers of instance c1 in the default project. + type: string + x-go-name: Description + name: + description: Name is the name of the group. + example: default-c1-viewers + type: string + x-go-name: Name + permissions: + description: Permissions are a list of permissions. + items: + $ref: '#/definitions/Permission' + type: array + x-go-name: Permissions + title: AuthGroupsPost is used for creating a new group. + type: object + x-go-package: github.com/canonical/lxd/shared/api Certificate: description: Certificate represents a LXD certificate properties: @@ -616,6 +701,128 @@ definitions: x-go-name: Type type: object x-go-package: github.com/canonical/lxd/shared/api + Identity: + properties: + authentication_method: + description: |- + AuthenticationMethod is the authentication method that the identity + authenticates to LXD with. + example: tls + type: string + x-go-name: AuthenticationMethod + id: + description: Identifier is a unique identifier for the identity (e.g. certificate fingerprint or email for OIDC). + example: jane.doe@example.com + type: string + x-go-name: Identifier + name: + description: |- + 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 + type: string + x-go-name: Name + type: + description: Type is the type of identity. + example: oidc-service-account + type: string + x-go-name: Type + title: Identity is the type for an authenticated party that can make requests to the HTTPS API. + type: object + x-go-package: github.com/canonical/lxd/shared/api + IdentityInfo: + properties: + authentication_method: + description: |- + AuthenticationMethod is the authentication method that the identity + authenticates to LXD with. + example: tls + type: string + x-go-name: AuthenticationMethod + groups: + description: Groups is the list of groups for which the identity is a member. + example: + - foo + - bar + items: + type: string + type: array + x-go-name: Groups + id: + description: Identifier is a unique identifier for the identity (e.g. certificate fingerprint or email for OIDC). + example: jane.doe@example.com + type: string + x-go-name: Identifier + name: + description: |- + 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 + type: string + x-go-name: Name + type: + description: Type is the type of identity. + example: oidc-service-account + type: string + x-go-name: Type + title: IdentityInfo expands an Identity to include group membership. + type: object + x-go-package: github.com/canonical/lxd/shared/api + IdentityProviderGroup: + properties: + groups: + description: Groups are the groups the IdP group resolves to. + example: + - foo + - bar + items: + type: string + type: array + x-go-name: Groups + name: + description: Name is the name of the IdP group. + type: string + x-go-name: Name + title: IdentityProviderGroup represents a mapping between LXD groups and groups defined by an identity provider. + type: object + x-go-package: github.com/canonical/lxd/shared/api + IdentityProviderGroupPost: + properties: + name: + description: Name is the name of the IdP group. + type: string + x-go-name: Name + title: IdentityProviderGroupPost is used for renaming an IdentityProviderGroup. + type: object + x-go-package: github.com/canonical/lxd/shared/api + IdentityProviderGroupPut: + properties: + groups: + description: Groups are the groups the IdP group resolves to. + example: + - foo + - bar + items: + type: string + type: array + x-go-name: Groups + title: IdentityProviderGroupPut contains the editable fields of an IdentityProviderGroup. + type: object + x-go-package: github.com/canonical/lxd/shared/api + IdentityPut: + properties: + groups: + description: Groups is the list of groups for which the identity is a member. + example: + - foo + - bar + items: + type: string + type: array + x-go-name: Groups + title: IdentityPut contains the editable fields of an IdentityInfo. + type: object + x-go-package: github.com/canonical/lxd/shared/api Image: description: Image represents a LXD image properties: @@ -3726,6 +3933,55 @@ definitions: x-go-name: UpdatedAt type: object x-go-package: github.com/canonical/lxd/shared/api + Permission: + properties: + entitlement: + description: Entitlement is the entitlement define for the entity type. + example: can_view + type: string + x-go-name: Entitlement + entity_type: + description: EntityType is the string representation of the entity type. + example: instance + type: string + x-go-name: EntityType + url: + description: EntityReference is the URL of the entity that the permission applies to. + example: /1.0/instances/c1?project=default + type: string + x-go-name: EntityReference + title: Permission represents a permission that may be granted to a group. + type: object + x-go-package: github.com/canonical/lxd/shared/api + PermissionInfo: + properties: + entitlement: + description: Entitlement is the entitlement define for the entity type. + example: can_view + type: string + x-go-name: Entitlement + entity_type: + description: EntityType is the string representation of the entity type. + example: instance + type: string + x-go-name: EntityType + groups: + description: Groups is a list of groups that have the Entitlement on the Entity. + example: + - foo + - bar + items: + type: string + type: array + x-go-name: Groups + url: + description: EntityReference is the URL of the entity that the permission applies to. + example: /1.0/instances/c1?project=default + type: string + x-go-name: EntityReference + title: PermissionInfo expands a Permission to include any groups that may have the specified Permission. + type: object + x-go-package: github.com/canonical/lxd/shared/api Profile: description: Profile represents a LXD profile properties: @@ -6489,6 +6745,880 @@ paths: summary: Update the server configuration tags: - server + /1.0/auth/groups: + get: + description: Returns a list of authorization groups (URLs). + operationId: auth_groups_get + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of endpoints + example: |- + [ + "/1.0/auth/groups/foo", + "/1.0/auth/groups/bar" + ] + items: + type: string + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the groups + tags: + - auth_groups + post: + consumes: + - application/json + description: Creates a new authorization group. + operationId: auth_groups_post + parameters: + - description: Group request + in: body + name: group + required: true + schema: + $ref: '#/definitions/AuthGroupsPost' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Create a new authorization group + tags: + - auth_groups + /1.0/auth/groups/{groupName}: + delete: + description: Deletes the authorization group + operationId: auth_group_delete + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Delete the authorization group + tags: + - auth_groups + get: + description: Gets a specific authorization group. + operationId: auth_group_get + produces: + - application/json + responses: + "200": + description: "" + schema: + description: Sync response + properties: + metadata: + $ref: '#/definitions/AuthGroup' + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the authorization group + tags: + - auth_groups + patch: + consumes: + - application/json + description: Updates the editable fields of an authorization group + operationId: auth_group_patch + parameters: + - description: Update request + in: body + name: group + schema: + $ref: '#/definitions/AuthGroupPut' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Partially update the authorization group + tags: + - auth_groups + post: + consumes: + - application/json + description: Renames the authorization group + operationId: auth_group_post + parameters: + - description: Update request + in: body + name: group + schema: + $ref: '#/definitions/AuthGroupPost' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Rename the authorization group + tags: + - auth_groups + put: + consumes: + - application/json + description: Replaces the editable fields of an authorization group + operationId: auth_group_put + parameters: + - description: Update request + in: body + name: group + schema: + $ref: '#/definitions/AuthGroupPut' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Update the authorization group + tags: + - auth_groups + /1.0/auth/groups?recursion=1: + get: + description: Returns a list of authorization groups. + operationId: auth_groups_get_recursion1 + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of auth groups + items: + $ref: '#/definitions/AuthGroup' + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the groups + tags: + - auth_groups + /1.0/auth/identities: + get: + description: Returns a list of identities (URLs). + operationId: identities_get + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of endpoints + example: |- + [ + "/1.0/auth/identities/tls/e1e06266e36f67431c996d5678e66d732dfd12fe5073c161e62e6360619fc226", + "/1.0/auth/identities/oidc/auth0|4daf5e37ce230e455b64b65b" + ] + items: + type: string + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identities + tags: + - identities + /1.0/auth/identities/{authenticationMethod}: + get: + description: Returns a list of identities (URLs). + operationId: identities_get_by_auth_method + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of endpoints + example: |- + [ + "/1.0/auth/identities/tls/e1e06266e36f67431c996d5678e66d732dfd12fe5073c161e62e6360619fc226", + "/1.0/auth/identities/oidc/auth0|4daf5e37ce230e455b64b65b" + ] + items: + type: string + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identities + tags: + - identities + /1.0/auth/identities/{authenticationMethod}/{nameOrIdentifier}: + get: + description: Gets a specific identity. + operationId: identity_get + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + $ref: '#/definitions/IdentityInfo' + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identity + tags: + - identities + patch: + consumes: + - application/json + description: Updates the editable fields of an identity + operationId: identity_patch + parameters: + - description: Update request + in: body + name: identity + schema: + $ref: '#/definitions/IdentityPut' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Partially update the identity + tags: + - identities + put: + consumes: + - application/json + description: Replaces the editable fields of an identity + operationId: identity_put + parameters: + - description: Update request + in: body + name: identity + schema: + $ref: '#/definitions/IdentityPut' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Update the identity + tags: + - identities + /1.0/auth/identities/{authenticationMethod}?recursion=1: + get: + description: Returns a list of identities. + operationId: identities_get_by_auth_method_recursion1 + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of identities + items: + $ref: '#/definitions/Identity' + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identities + tags: + - identities + /1.0/auth/identities/{authenticationMethod}?recursion=2: + get: + description: Returns a list of identities including group membership. + operationId: identities_get_by_auth_method_recursion2 + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of identities + items: + $ref: '#/definitions/IdentityInfo' + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identities + tags: + - identities + /1.0/auth/identities?recursion=1: + get: + description: Returns a list of identities. + operationId: identities_get_recursion1 + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of identities + items: + $ref: '#/definitions/Identity' + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identities + tags: + - identities + /1.0/auth/identities?recursion=2: + get: + description: Returns a list of identities including group membership. + operationId: identities_get_recursion2 + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of identities + items: + $ref: '#/definitions/IdentityInfo' + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identities + tags: + - identities + /1.0/auth/identity-provider-groups: + get: + description: Returns a list of identity provider groups (URLs). + operationId: identity_provider_groups_get + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of endpoints + example: |- + [ + "/1.0/auth/identity-provider-groups/sales", + "/1.0/auth/identity-provider-groups/operations" + ] + items: + type: string + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identity provider groups + tags: + - identity_provider_groups + post: + consumes: + - application/json + description: Creates a new identity provider group. + operationId: identity_provider_groups_post + parameters: + - description: Identity provider request + in: body + name: group + required: true + schema: + $ref: '#/definitions/IdentityProviderGroup' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Create a new identity provider group + tags: + - identity_provider_groups + /1.0/auth/identity-provider-groups/{idpGroupName}: + delete: + description: Deletes the identity provider group + operationId: identity_provider_group_delete + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Delete the identity provider group + tags: + - identity_provider_groups + get: + description: Gets a specific identity provider group. + operationId: identity_provider_group_get + produces: + - application/json + responses: + "200": + description: "" + schema: + description: Sync response + properties: + metadata: + $ref: '#/definitions/IdentityProviderGroup' + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the identity provider group + tags: + - identity_provider_groups + patch: + consumes: + - application/json + description: Updates the editable fields of an identity provider group + operationId: identity_provider_group_patch + parameters: + - description: Update request + in: body + name: group + schema: + $ref: '#/definitions/IdentityProviderGroupPut' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Partially update the identity provider group + tags: + - identity_provider_groups + post: + consumes: + - application/json + description: Renames the identity provider group + operationId: identity_provider_group_post + parameters: + - description: Update request + in: body + name: group + schema: + $ref: '#/definitions/IdentityProviderGroupPost' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Rename the identity provider group + tags: + - identity_provider_groups + put: + consumes: + - application/json + description: Replaces the editable fields of an identity provider group + operationId: identity_provider_group_put + parameters: + - description: Update request + in: body + name: group + schema: + $ref: '#/definitions/IdentityProviderGroupPut' + produces: + - application/json + responses: + "200": + $ref: '#/responses/EmptySyncResponse' + "400": + $ref: '#/responses/BadRequest' + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Update the identity provider group + tags: + - identity_provider_groups + /1.0/auth/identity-provider-groups?recursion=1: + get: + description: Returns a list of identity provider groups. + operationId: identity_provider_groups_get_recursion1 + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of identity provider groups + items: + $ref: '#/definitions/IdentityProviderGroup' + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the groups + tags: + - identity_provider_groups + /1.0/auth/permissions: + get: + description: Returns a list of available permissions. + operationId: permissions_get + parameters: + - description: Project name + example: default + in: query + name: project + type: string + - description: Type of entity + example: instance + in: query + name: entityType + type: string + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of permissions + items: + $ref: '#/definitions/Permission' + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the permissions + tags: + - permissions + /1.0/auth/permissions?recursion=1: + get: + description: Returns a list of available permissions (including groups that have those permissions). + operationId: permissions_get_recursion1 + parameters: + - description: Project name + example: default + in: query + name: project + type: string + - description: Type of entity + example: instance + in: query + name: entity-type + type: string + produces: + - application/json + responses: + "200": + description: API endpoints + schema: + description: Sync response + properties: + metadata: + description: List of permissions + items: + $ref: '#/definitions/PermissionInfo' + type: array + status: + description: Status description + example: Success + type: string + status_code: + description: Status code + example: 200 + type: integer + type: + description: Response type + example: sync + type: string + type: object + "403": + $ref: '#/responses/Forbidden' + "500": + $ref: '#/responses/InternalServerError' + summary: Get the permissions + tags: + - permissions /1.0/certificates: get: description: Returns a list of trusted certificates (URLs). From 4505203fe58992e308072e781431c5332526a0c8 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 17:39:52 +0000 Subject: [PATCH 30/34] client: Add client methods for auth APIs. Signed-off-by: Mark Laing --- client/interfaces.go | 38 ++++ client/lxd_auth.go | 438 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 476 insertions(+) create mode 100644 client/lxd_auth.go diff --git a/client/interfaces.go b/client/interfaces.go index 859568e59a4c..b49cbd1a2d1e 100644 --- a/client/interfaces.go +++ b/client/interfaces.go @@ -425,6 +425,32 @@ type InstanceServer interface { UpdateWarning(UUID string, warning api.WarningPut, ETag string) (err error) DeleteWarning(UUID string) (err error) + // Authorization functions + GetAuthGroupNames() (groupNames []string, err error) + GetAuthGroups() (groups []api.AuthGroup, err error) + GetAuthGroup(groupName string) (group *api.AuthGroup, ETag string, err error) + CreateAuthGroup(groupsPost api.AuthGroupsPost) error + UpdateAuthGroup(groupName string, groupPut api.AuthGroupPut, ETag string) error + RenameAuthGroup(groupName string, groupPost api.AuthGroupPost) error + DeleteAuthGroup(groupName string) error + GetIdentityAuthenticationMethodsIdentifiers() (authMethodsIdentifiers map[string][]string, err error) + GetIdentityIdentifiersByAuthenticationMethod(authenticationMethod string) (identifiers []string, err error) + GetIdentities() (identities []api.Identity, err error) + GetIdentitiesByAuthenticationMethod(authenticationMethod string) (identities []api.Identity, err error) + GetIdentitiesInfo() (identityInfos []api.IdentityInfo, err error) + GetIdentitiesInfoByAuthenticationMethod(authenticationMethod string) (identityInfos []api.IdentityInfo, err error) + GetIdentity(authenticationMethod string, nameOrIdentifier string) (identityInfo *api.IdentityInfo, ETag string, err error) + UpdateIdentity(authenticationMethod string, nameOrIdentifier string, identityPut api.IdentityPut, ETag string) error + GetIdentityProviderGroupNames() (identityProviderGroupNames []string, err error) + GetIdentityProviderGroups() (identityProviderGroups []api.IdentityProviderGroup, err error) + GetIdentityProviderGroup(identityProviderGroupName string) (identityProviderGroup *api.IdentityProviderGroup, ETag string, err error) + CreateIdentityProviderGroup(identityProviderGroup api.IdentityProviderGroup) error + UpdateIdentityProviderGroup(identityProviderGroupName string, identityProviderGroupPut api.IdentityProviderGroupPut, ETag string) error + RenameIdentityProviderGroup(identityProviderGroupName string, identityProviderGroupPost api.IdentityProviderGroupPost) error + DeleteIdentityProviderGroup(identityProviderGroupName string) error + GetPermissions(args GetPermissionsArgs) (permissions []api.Permission, err error) + GetPermissionsInfo(args GetPermissionsArgs) (permissions []api.PermissionInfo, err error) + // Internal functions (for internal use) RawQuery(method string, path string, data any, queryETag string) (resp *api.Response, ETag string, err error) RawWebsocket(path string) (conn *websocket.Conn, err error) @@ -698,3 +724,15 @@ type InstanceFileResponse struct { // If a directory, the list of files inside it Entries []string } + +// GetPermissionsArgs is used in the call to GetPermissions to specify filtering behaviour. +type GetPermissionsArgs struct { + // EntityType is the type of entity to filter against. + // If left unspecified, permissions will be returned for all entity types. + EntityType string + + // ProjectName is the project to filter against. + // If the project name is specified, only permissions for resources in the given project will be returned and server + // level permissions will not be returned. + ProjectName string +} diff --git a/client/lxd_auth.go b/client/lxd_auth.go new file mode 100644 index 000000000000..5260d42b7bb0 --- /dev/null +++ b/client/lxd_auth.go @@ -0,0 +1,438 @@ +package lxd + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/canonical/lxd/shared/api" +) + +// GetAuthGroupNames returns a slice of all group names. +func (r *ProtocolLXD) GetAuthGroupNames() ([]string, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + urls := []string{} + baseURL := "auth/groups" + _, err = r.queryStruct(http.MethodGet, baseURL, nil, "", &urls) + if err != nil { + return nil, err + } + + return urlsToResourceNames(baseURL, urls...) +} + +// GetAuthGroup returns a single group by its name. +func (r *ProtocolLXD) GetAuthGroup(groupName string) (*api.AuthGroup, string, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, "", err + } + + group := api.AuthGroup{} + etag, err := r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "groups", groupName).String(), nil, "", &group) + if err != nil { + return nil, "", err + } + + return &group, etag, nil +} + +// GetAuthGroups returns a list of all groups. +func (r *ProtocolLXD) GetAuthGroups() ([]api.AuthGroup, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + var groups []api.AuthGroup + _, err = r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "groups").WithQuery("recursion", "1").String(), nil, "", &groups) + if err != nil { + return nil, err + } + + return groups, nil +} + +// CreateAuthGroup creates a new group. +func (r *ProtocolLXD) CreateAuthGroup(group api.AuthGroupsPost) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodPost, api.NewURL().Path("auth", "groups").String(), group, "") + if err != nil { + return err + } + + return nil +} + +// UpdateAuthGroup replaces the editable fields of the group with the given name. +func (r *ProtocolLXD) UpdateAuthGroup(groupName string, groupPut api.AuthGroupPut, ETag string) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodPut, api.NewURL().Path("auth", "groups", groupName).String(), groupPut, ETag) + if err != nil { + return err + } + + return nil +} + +// RenameAuthGroup renames the group with the given name. +func (r *ProtocolLXD) RenameAuthGroup(groupName string, groupPost api.AuthGroupPost) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodPost, api.NewURL().Path("auth", "groups", groupName).String(), groupPost, "") + if err != nil { + return err + } + + return nil +} + +// DeleteAuthGroup deletes the group with the given name. +func (r *ProtocolLXD) DeleteAuthGroup(groupName string) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodDelete, api.NewURL().Path("auth", "groups", groupName).String(), nil, "") + if err != nil { + return err + } + + return nil +} + +// GetIdentityAuthenticationMethodsIdentifiers returns a map of authentication method to list of identifiers (e.g. certificate fingerprint, email address) +// for all identities. +func (r *ProtocolLXD) GetIdentityAuthenticationMethodsIdentifiers() (map[string][]string, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + urls := []string{} + baseURL := "auth/identities" + _, err = r.queryStruct(http.MethodGet, baseURL, nil, "", &urls) + if err != nil { + return nil, err + } + + authMethodSlashIdentifiers, err := urlsToResourceNames(baseURL, urls...) + if err != nil { + return nil, err + } + + authMethodIdentifiers := make(map[string][]string) + for _, authMethodSlashIdentifier := range authMethodSlashIdentifiers { + authMethod, escapedIdentifier, ok := strings.Cut(authMethodSlashIdentifier, "/") + if !ok { + return nil, fmt.Errorf("Invalid identity URL suffix %q", authMethodSlashIdentifier) + } + + identifier, err := url.PathUnescape(escapedIdentifier) + if err != nil { + return nil, fmt.Errorf("Failed to unescape identity identifier: %w", err) + } + + _, ok = authMethodIdentifiers[authMethod] + if !ok { + authMethodIdentifiers[authMethod] = []string{identifier} + continue + } + + authMethodIdentifiers[authMethod] = append(authMethodIdentifiers[authMethod], identifier) + } + + return authMethodIdentifiers, nil +} + +// GetIdentityIdentifiersByAuthenticationMethod returns a list of identifiers (e.g. certificate fingerprint, email address) of +// identities that authenticate with the given authentication method. +func (r *ProtocolLXD) GetIdentityIdentifiersByAuthenticationMethod(authenticationMethod string) ([]string, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + urls := []string{} + baseURL := fmt.Sprintf("auth/identities/%s", authenticationMethod) + _, err = r.queryStruct(http.MethodGet, baseURL, nil, "", &urls) + if err != nil { + return nil, err + } + + return urlsToResourceNames(baseURL, urls...) +} + +// GetIdentities returns a list of identities. +func (r *ProtocolLXD) GetIdentities() ([]api.Identity, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + var identities []api.Identity + _, err = r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "identities").WithQuery("recursion", "1").String(), nil, "", &identities) + if err != nil { + return nil, err + } + + return identities, nil +} + +// GetIdentitiesByAuthenticationMethod returns a list of identities that authenticate with the given authentication method. +func (r *ProtocolLXD) GetIdentitiesByAuthenticationMethod(authenticationMethod string) ([]api.Identity, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + var identities []api.Identity + _, err = r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "identities", authenticationMethod).WithQuery("recursion", "1").String(), nil, "", &identities) + if err != nil { + return nil, err + } + + return identities, nil +} + +// GetIdentitiesInfo returns a list of identities and populates the groups that each identity is a member of. +func (r *ProtocolLXD) GetIdentitiesInfo() ([]api.IdentityInfo, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + var identities []api.IdentityInfo + _, err = r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "identities").WithQuery("recursion", "2").String(), nil, "", &identities) + if err != nil { + return nil, err + } + + return identities, nil +} + +// GetIdentitiesInfoByAuthenticationMethod returns a list of identities that authenticate with the given authentication method +// and populates the groups that each identity is a member of. +func (r *ProtocolLXD) GetIdentitiesInfoByAuthenticationMethod(authenticationMethod string) ([]api.IdentityInfo, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + var identities []api.IdentityInfo + _, err = r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "identities", authenticationMethod).WithQuery("recursion", "2").String(), nil, "", &identities) + if err != nil { + return nil, err + } + + return identities, nil +} + +// GetIdentity returns the identity with the given authentication method and identifier. A name may be supplied in place +// of the identifier if the name is unique within the authentication method. +func (r *ProtocolLXD) GetIdentity(authenticationMethod string, nameOrIdentifier string) (*api.IdentityInfo, string, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, "", err + } + + identityInfo := api.IdentityInfo{} + etag, err := r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "identities", authenticationMethod, nameOrIdentifier).WithQuery("recursion", "1").String(), nil, "", &identityInfo) + if err != nil { + return nil, "", err + } + + return &identityInfo, etag, nil +} + +// UpdateIdentity replaces the editable fields of an identity with the given input. +func (r *ProtocolLXD) UpdateIdentity(authenticationMethod string, nameOrIdentifer string, identityPut api.IdentityPut, ETag string) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodPut, api.NewURL().Path("auth", "identities", authenticationMethod, nameOrIdentifer).String(), identityPut, ETag) + if err != nil { + return err + } + + return nil +} + +// GetIdentityProviderGroupNames returns a list of identity provider group names. +func (r *ProtocolLXD) GetIdentityProviderGroupNames() ([]string, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + urls := []string{} + baseURL := "auth/identity-provider-groups" + _, err = r.queryStruct(http.MethodGet, baseURL, nil, "", &urls) + if err != nil { + return nil, err + } + + return urlsToResourceNames(baseURL, urls...) +} + +// GetIdentityProviderGroups returns all identity provider groups defined on the server. +func (r *ProtocolLXD) GetIdentityProviderGroups() ([]api.IdentityProviderGroup, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + var idpGroups []api.IdentityProviderGroup + _, err = r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "identity-provider-groups").WithQuery("recursion", "1").String(), nil, "", &idpGroups) + if err != nil { + return nil, err + } + + return idpGroups, nil +} + +// GetIdentityProviderGroup returns the identity provider group with the given name. +func (r *ProtocolLXD) GetIdentityProviderGroup(identityProviderGroupName string) (*api.IdentityProviderGroup, string, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, "", err + } + + idpGroup := api.IdentityProviderGroup{} + etag, err := r.queryStruct(http.MethodGet, api.NewURL().Path("auth", "identity-provider-groups", identityProviderGroupName).String(), nil, "", &idpGroup) + if err != nil { + return nil, "", err + } + + return &idpGroup, etag, nil +} + +// CreateIdentityProviderGroup creates a new identity provider group. +func (r *ProtocolLXD) CreateIdentityProviderGroup(identityProviderGroup api.IdentityProviderGroup) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodPost, api.NewURL().Path("auth", "identity-provider-groups").String(), identityProviderGroup, "") + if err != nil { + return err + } + + return nil +} + +// UpdateIdentityProviderGroup replaces the groups that are mapped to the identity provider group with the given name. +func (r *ProtocolLXD) UpdateIdentityProviderGroup(identityProviderGroupName string, identityProviderGroupPut api.IdentityProviderGroupPut, ETag string) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodPut, api.NewURL().Path("auth", "identity-provider-groups", identityProviderGroupName).String(), identityProviderGroupPut, ETag) + if err != nil { + return err + } + + return nil +} + +// RenameIdentityProviderGroup renames the identity provider group with the given name. +func (r *ProtocolLXD) RenameIdentityProviderGroup(identityProviderGroupName string, identityProviderGroupPost api.IdentityProviderGroupPost) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodPost, api.NewURL().Path("auth", "identity-provider-groups", identityProviderGroupName).String(), identityProviderGroupPost, "") + if err != nil { + return err + } + + return nil +} + +// DeleteIdentityProviderGroup deletes the identity provider group with the given name. +func (r *ProtocolLXD) DeleteIdentityProviderGroup(identityProviderGroupName string) error { + err := r.CheckExtension("authorization_apis") + if err != nil { + return err + } + + _, _, err = r.query(http.MethodDelete, api.NewURL().Path("auth", "identity-provider-groups", identityProviderGroupName).String(), nil, "") + if err != nil { + return err + } + + return nil +} + +// GetPermissions returns all permissions available on the server. It does not return information on whether these +// permissions are assigned to groups. +func (r *ProtocolLXD) GetPermissions(args GetPermissionsArgs) ([]api.Permission, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + u := api.NewURL().Path("auth", "permissions") + if args.ProjectName != "" { + u = u.WithQuery("project", args.ProjectName) + } + + if args.EntityType != "" { + u = u.WithQuery("entity-type", args.EntityType) + } + + var permissions []api.Permission + _, err = r.queryStruct(http.MethodGet, u.String(), nil, "", &permissions) + if err != nil { + return nil, err + } + + return permissions, nil +} + +// GetPermissionsInfo returns all permissions available on the server and includes the groups that are assigned each permission. +func (r *ProtocolLXD) GetPermissionsInfo(args GetPermissionsArgs) ([]api.PermissionInfo, error) { + err := r.CheckExtension("authorization_apis") + if err != nil { + return nil, err + } + + u := api.NewURL().Path("auth", "permissions").WithQuery("recursion", "1") + if args.ProjectName != "" { + u = u.WithQuery("project", args.ProjectName) + } + + if args.EntityType != "" { + u = u.WithQuery("entity-type", args.EntityType) + } + + var permissions []api.PermissionInfo + _, err = r.queryStruct(http.MethodGet, u.String(), nil, "", &permissions) + if err != nil { + return nil, err + } + + return permissions, nil +} From ac50d77d2aa70bcfd28706df074c998afb7709c7 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 18:12:19 +0000 Subject: [PATCH 31/34] shared/entity: Export (entity.Type).RequiresProject method. This is required for checking if a project= argument is required when adding a permission to a group. Signed-off-by: Mark Laing --- shared/entity/type.go | 8 ++++---- shared/entity/type_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shared/entity/type.go b/shared/entity/type.go index b104c3768d46..eea8f3ddc53c 100644 --- a/shared/entity/type.go +++ b/shared/entity/type.go @@ -140,9 +140,9 @@ func (t Type) Validate() error { return nil } -// requiresProject returns true if an entity of the Type can only exist within the context of a project. Operations and +// RequiresProject returns true if an entity of the Type can only exist within the context of a project. Operations and // warnings may still be project specific but it is not an absolute requirement. -func (t Type) requiresProject() (bool, error) { +func (t Type) RequiresProject() (bool, error) { err := t.Validate() if err != nil { return false, err @@ -180,7 +180,7 @@ func (t Type) nRequiredPathArguments() (int, error) { // // Warning: All arguments to this function will be URL encoded. They must not be URL encoded before calling this method. func (t Type) URL(projectName string, location string, pathArguments ...string) (*api.URL, error) { - requiresProject, err := t.requiresProject() + requiresProject, err := t.RequiresProject() if err != nil { return nil, fmt.Errorf("Failed to check if entity type %q is project specific: %w", t, err) } @@ -347,7 +347,7 @@ entityTypeLoop: return "", "", "", nil, fmt.Errorf("Failed to match entity URL %q", u.String()) } - requiresProject, _ := entityType.requiresProject() + requiresProject, _ := entityType.RequiresProject() projectName = "" if requiresProject { projectName = u.Query().Get("project") diff --git a/shared/entity/type_test.go b/shared/entity/type_test.go index 3b834bbe11e0..e33863a72663 100644 --- a/shared/entity/type_test.go +++ b/shared/entity/type_test.go @@ -182,7 +182,7 @@ func TestURL(t *testing.T) { assert.Equal(t, tt.expectedErr, actualErr) - requiresProject, err := actualEntityType.requiresProject() + requiresProject, err := actualEntityType.RequiresProject() assert.NoError(t, err) if u.Query().Get("project") != "" || !requiresProject { // Assert that we can convert back to the same value. From c0718aa6cd99a2c667df5e868d3ea84d0c10bab4 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 18:12:50 +0000 Subject: [PATCH 32/34] lxc/auth: Add authorization commands to CLI. Signed-off-by: Mark Laing --- lxc/auth.go | 1912 +++++++++++++++++++++++++++++++++++++++++++++++++++ lxc/main.go | 3 + 2 files changed, 1915 insertions(+) create mode 100644 lxc/auth.go diff --git a/lxc/auth.go b/lxc/auth.go new file mode 100644 index 000000000000..160aca8c8228 --- /dev/null +++ b/lxc/auth.go @@ -0,0 +1,1912 @@ +package main + +import ( + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + + "github.com/canonical/lxd/client" + "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" + cli "github.com/canonical/lxd/shared/cmd" + "github.com/canonical/lxd/shared/entity" + "github.com/canonical/lxd/shared/i18n" + "github.com/canonical/lxd/shared/termios" +) + +type cmdAuth struct { + global *cmdGlobal +} + +func (c *cmdAuth) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("auth") + cmd.Short = i18n.G("Manage user authorization") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Manage user authorization`)) + + groupCmd := cmdGroup{global: c.global} + cmd.AddCommand(groupCmd.command()) + + permissionCmd := cmdPermission{global: c.global} + cmd.AddCommand(permissionCmd.command()) + + identityCmd := cmdIdentity{global: c.global} + cmd.AddCommand(identityCmd.command()) + + identityProviderGroupCmd := cmdIdentityProviderGroup{global: c.global} + cmd.AddCommand(identityProviderGroupCmd.command()) + + // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706 + cmd.Args = cobra.NoArgs + cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() } + return cmd +} + +type cmdGroup struct { + global *cmdGlobal +} + +func (c *cmdGroup) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("group") + cmd.Short = i18n.G("Manage groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Manage groups`)) + + groupCreateCmd := cmdGroupCreate{global: c.global} + cmd.AddCommand(groupCreateCmd.command()) + + groupDeleteCmd := cmdGroupDelete{global: c.global} + cmd.AddCommand(groupDeleteCmd.command()) + + groupEditCmd := cmdGroupEdit{global: c.global} + cmd.AddCommand(groupEditCmd.command()) + + groupShowCmd := cmdGroupShow{global: c.global} + cmd.AddCommand(groupShowCmd.command()) + + groupListCmd := cmdGroupList{global: c.global} + cmd.AddCommand(groupListCmd.command()) + + groupRenameCmd := cmdGroupRename{global: c.global} + cmd.AddCommand(groupRenameCmd.command()) + + permissionCmd := cmdGroupPermission{global: c.global} + cmd.AddCommand(permissionCmd.command()) + + // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706 + cmd.Args = cobra.NoArgs + cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() } + return cmd +} + +type cmdGroupCreate struct { + global *cmdGlobal + flagDescription string +} + +func (c *cmdGroupCreate) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("create", i18n.G("[:]")) + cmd.Short = i18n.G("Create groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Create groups`)) + cmd.Flags().StringVarP(&c.flagDescription, "description", "d", "", "Group description") + cmd.RunE = c.run + + return cmd +} + +func (c *cmdGroupCreate) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing group name")) + } + + // Create the group + group := api.AuthGroupsPost{} + group.Name = resource.name + group.Description = c.flagDescription + + err = resource.server.CreateAuthGroup(group) + if err != nil { + return err + } + + if !c.global.flagQuiet { + fmt.Printf(i18n.G("Group %s created")+"\n", resource.name) + } + + return nil +} + +// Delete. +type cmdGroupDelete struct { + global *cmdGlobal +} + +func (c *cmdGroupDelete) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("delete", i18n.G("[:]")) + cmd.Aliases = []string{"rm"} + cmd.Short = i18n.G("Delete groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Delete groups`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdGroupDelete) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing group name")) + } + + // Delete the group + err = resource.server.DeleteAuthGroup(resource.name) + if err != nil { + return err + } + + if !c.global.flagQuiet { + fmt.Printf(i18n.G("Group %s deleted")+"\n", resource.name) + } + + return nil +} + +// Edit. +type cmdGroupEdit struct { + global *cmdGlobal +} + +func (c *cmdGroupEdit) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("edit", i18n.G("[:]")) + cmd.Short = i18n.G("Edit groups as YAML") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Edit groups as YAML`)) + cmd.Example = cli.FormatSection("", i18n.G( + `lxc auth group edit < group.yaml + Update a group using the content of group.yaml`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdGroupEdit) helpTemplate() string { + return i18n.G( + `### This is a YAML representation of the group. +### Any line starting with a '# will be ignored. +### +### A group has the following format: +### name: my-first-group +### description: My first group. +### permissions: +### - entity_type: project +### url: /1.0/projects/default +### entitlement: can_view +### identities: +### - authentication_method: oidc +### type: OIDC client +### identifier: jane.doe@example.com +### name: Jane Doe +### metadata: +### subject: auth0|123456789 +### identity_provider_groups: +### - sales +### - operations +### +### Note that all group information is shown but only the description and permissions can be modified`) +} + +func (c *cmdGroupEdit) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing group name")) + } + + // If stdin isn't a terminal, read text from it + if !termios.IsTerminal(getStdinFd()) { + contents, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + + newdata := api.AuthGroupPut{} + err = yaml.Unmarshal(contents, &newdata) + if err != nil { + return err + } + + return resource.server.UpdateAuthGroup(resource.name, newdata, "") + } + + // Extract the current value + group, etag, err := resource.server.GetAuthGroup(resource.name) + if err != nil { + return err + } + + data, err := yaml.Marshal(&group) + if err != nil { + return err + } + + // Spawn the editor + content, err := shared.TextEditor("", []byte(c.helpTemplate()+"\n\n"+string(data))) + if err != nil { + return err + } + + for { + // Parse the text received from the editor + newdata := api.AuthGroupPut{} + err = yaml.Unmarshal(content, &newdata) + if err == nil { + err = resource.server.UpdateAuthGroup(resource.name, newdata, etag) + } + + // Respawn the editor + if err != nil { + fmt.Fprintf(os.Stderr, i18n.G("Could not parse group: %s")+"\n", err) + fmt.Println(i18n.G("Press enter to open the editor again or ctrl+c to abort change")) + + _, err := os.Stdin.Read(make([]byte, 1)) + if err != nil { + return err + } + + content, err = shared.TextEditor("", content) + if err != nil { + return err + } + + continue + } + + break + } + + return nil +} + +type cmdGroupList struct { + global *cmdGlobal + flagFormat string +} + +func (c *cmdGroupList) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("list", i18n.G("[:]")) + cmd.Aliases = []string{"ls"} + cmd.Short = i18n.G("List groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `List groups`)) + + cmd.RunE = c.run + cmd.Flags().StringVarP(&c.flagFormat, "format", "f", "table", i18n.G("Format (csv|json|table|yaml|compact)")+"``") + + return cmd +} + +func (c *cmdGroupList) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 0, 1) + if exit { + return err + } + + // Parse remote + remote := "" + if len(args) > 0 { + remote = args[0] + } + + resources, err := c.global.ParseServers(remote) + if err != nil { + return err + } + + resource := resources[0] + + // List groups + groups, err := resource.server.GetAuthGroups() + if err != nil { + return err + } + + data := [][]string{} + for _, group := range groups { + data = append(data, []string{group.Name, group.Description}) + } + + sort.Sort(cli.SortColumnsNaturally(data)) + + header := []string{ + i18n.G("NAME"), + i18n.G("DESCRIPTION"), + } + + return cli.RenderTable(c.flagFormat, header, data, groups) +} + +// Rename. +type cmdGroupRename struct { + global *cmdGlobal +} + +func (c *cmdGroupRename) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("rename", i18n.G("[:] ")) + cmd.Aliases = []string{"mv"} + cmd.Short = i18n.G("Rename groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Rename groups`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdGroupRename) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 2, 2) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing group name")) + } + + // Rename the group + err = resource.server.RenameAuthGroup(resource.name, api.AuthGroupPost{Name: args[1]}) + if err != nil { + return err + } + + if !c.global.flagQuiet { + fmt.Printf(i18n.G("Group %s renamed to %s")+"\n", resource.name, args[1]) + } + + return nil +} + +// Show. +type cmdGroupShow struct { + global *cmdGlobal +} + +func (c *cmdGroupShow) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("show", i18n.G("[:]")) + cmd.Short = i18n.G("Show group configurations") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Show group configurations`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdGroupShow) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing group name")) + } + + // Show the group + group, _, err := resource.server.GetAuthGroup(resource.name) + if err != nil { + return err + } + + data, err := yaml.Marshal(&group) + if err != nil { + return err + } + + fmt.Printf("%s", data) + + return nil +} + +type cmdGroupPermission struct { + global *cmdGlobal +} + +func (c *cmdGroupPermission) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("permission") + cmd.Aliases = []string{"perm"} + cmd.Short = i18n.G("Manage permissions") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Manage permissions`)) + + groupCreateCmd := cmdGroupPermissionAdd{global: c.global} + cmd.AddCommand(groupCreateCmd.command()) + + groupDeleteCmd := cmdGroupPermissionRemove{global: c.global} + cmd.AddCommand(groupDeleteCmd.command()) + + // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706 + cmd.Args = cobra.NoArgs + cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() } + return cmd +} + +type cmdGroupPermissionAdd struct { + global *cmdGlobal +} + +func (c *cmdGroupPermissionAdd) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("add", i18n.G("[:] [] [=...]")) + cmd.Short = i18n.G("Add permissions to groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Add permissions to groups`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdGroupPermissionAdd) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 3, -1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing group name")) + } + + group, eTag, err := resource.server.GetAuthGroup(resource.name) + if err != nil { + return err + } + + permission, err := parsePermissionArgs(args) + if err != nil { + return err + } + + added := false + if !shared.ValueInSlice(*permission, group.Permissions) { + group.Permissions = append(group.Permissions, *permission) + added = true + } + + if !added { + return fmt.Errorf("Group %q already has entitlement %q on entity %q", resource.name, permission.Entitlement, permission.EntityReference) + } + + return resource.server.UpdateAuthGroup(resource.name, group.AuthGroupPut, eTag) +} + +type cmdGroupPermissionRemove struct { + global *cmdGlobal +} + +func (c *cmdGroupPermissionRemove) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("remove", i18n.G("[:] [] [=...]")) + cmd.Aliases = []string{"rm"} + cmd.Short = i18n.G("Remove permissions from groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Remove permissions from groups`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdGroupPermissionRemove) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 3, -1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing group name")) + } + + group, eTag, err := resource.server.GetAuthGroup(resource.name) + if err != nil { + return err + } + + permission, err := parsePermissionArgs(args) + if err != nil { + return err + } + + if len(group.Permissions) == 0 { + return fmt.Errorf("Group %q does not have any permissions", resource.name) + } + + permissions := make([]api.Permission, 0, len(group.Permissions)-1) + removed := false + for _, existingPermission := range group.Permissions { + if *permission == existingPermission { + removed = true + continue + } + + permissions = append(permissions, existingPermission) + } + + if !removed { + return fmt.Errorf("Group %q does not have entitlement %q on entity %q", resource.name, permission.Entitlement, permission.EntityReference) + } + + group.AuthGroupPut.Permissions = permissions + return resource.server.UpdateAuthGroup(resource.name, group.AuthGroupPut, eTag) +} + +// parsePermissionArgs parses the ` [] [=...]` arguments of +// `lxc auth group permission add/remove` and returns an api.Permission that can be appended/removed from the list of +// permissions belonging to a group. +func parsePermissionArgs(args []string) (*api.Permission, error) { + entityType := entity.Type(args[1]) + err := entityType.Validate() + if err != nil { + return nil, err + } + + if entityType == entity.TypeServer { + if len(args) != 3 { + return nil, fmt.Errorf("Expected three arguments: `lxc auth group grant [:] server `") + } + + return &api.Permission{ + EntityType: string(entityType), + EntityReference: entity.ServerURL().String(), + Entitlement: args[2], + }, nil + } + + if len(args) < 4 { + return nil, fmt.Errorf("Expected at least four arguments: `lxc auth group grant [:] [=...]`") + } + + entityName := args[2] + entitlement := args[3] + + kv := make(map[string]string) + if len(args) > 4 { + for _, arg := range args[4:] { + k, v, ok := strings.Cut(arg, "=") + if !ok { + return nil, fmt.Errorf("Supplementary arguments must be of the form =") + } + + kv[k] = v + } + } + + pathArgs := []string{entityName} + if entityType == entity.TypeIdentity { + authenticationMethod, identifier, ok := strings.Cut(entityName, "/") + if !ok { + return nil, fmt.Errorf("Malformed identity argument, expected `/`, got %q", entityName) + } + + pathArgs = []string{authenticationMethod, identifier} + } + + projectName, ok := kv["project"] + requiresProject, _ := entityType.RequiresProject() + if requiresProject && !ok { + return nil, fmt.Errorf("Entities of type %q require a supplementary project argument `project=`", entityType) + } + + if entityType == entity.TypeStorageVolume { + storageVolumeType, ok := kv["type"] + if !ok { + return nil, fmt.Errorf("Entities of type %q require a supplementary storage volume type argument `type=`", entityType) + } + + pathArgs = append([]string{storageVolumeType}, pathArgs...) + } + + if entityType == entity.TypeStorageVolume || entityType == entity.TypeStorageBucket { + storagePool, ok := kv["pool"] + if !ok { + return nil, fmt.Errorf("Entities of type %q require a supplementary storage pool argument `pool=`", entityType) + } + + pathArgs = append([]string{storagePool}, pathArgs...) + } + + entityURL, err := entityType.URL(projectName, kv["location"], pathArgs...) + if err != nil { + return nil, err + } + + return &api.Permission{ + EntityType: string(entityType), + EntityReference: entityURL.String(), + Entitlement: entitlement, + }, nil +} + +type cmdIdentity struct { + global *cmdGlobal +} + +func (c *cmdIdentity) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("identity") + cmd.Aliases = []string{"user"} + cmd.Short = i18n.G("Manage identities") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Manage identities`)) + + identityListCmd := cmdIdentityList{global: c.global} + cmd.AddCommand(identityListCmd.command()) + identityShowCmd := cmdIdentityShow{global: c.global} + cmd.AddCommand(identityShowCmd.command()) + identityEditCmd := cmdIdentityEdit{global: c.global} + cmd.AddCommand(identityEditCmd.command()) + identityGroupCmd := cmdIdentityGroup{global: c.global} + cmd.AddCommand(identityGroupCmd.command()) + + // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706 + cmd.Args = cobra.NoArgs + cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() } + return cmd +} + +type cmdIdentityList struct { + global *cmdGlobal + flagFormat string +} + +func (c *cmdIdentityList) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("list", i18n.G("[:]")) + cmd.Aliases = []string{"ls"} + cmd.Short = i18n.G("List identities") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `List identities`)) + + cmd.RunE = c.run + cmd.Flags().StringVarP(&c.flagFormat, "format", "f", "table", i18n.G("Format (csv|json|table|yaml|compact)")+"``") + + return cmd +} + +func (c *cmdIdentityList) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 0, 1) + if exit { + return err + } + + // Parse remote + remote := "" + if len(args) > 0 { + remote = args[0] + } + + resources, err := c.global.ParseServers(remote) + if err != nil { + return err + } + + resource := resources[0] + + // List identities + identities, err := resource.server.GetIdentitiesInfo() + if err != nil { + return err + } + + data := [][]string{} + delimiter := "\n" + if c.flagFormat == cli.TableFormatCSV { + delimiter = "," + } + + for _, identity := range identities { + data = append(data, []string{identity.AuthenticationMethod, identity.Type, identity.Name, identity.Identifier, strings.Join(identity.Groups, delimiter)}) + } + + sort.Sort(cli.SortColumnsNaturally(data)) + + header := []string{ + i18n.G("AUTHENTICATION METHOD"), + i18n.G("TYPE"), + i18n.G("NAME"), + i18n.G("IDENTIFIER"), + i18n.G("GROUPS"), + } + + return cli.RenderTable(c.flagFormat, header, data, identities) +} + +// Show. +type cmdIdentityShow struct { + global *cmdGlobal +} + +func (c *cmdIdentityShow) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("show", i18n.G("[:]/")) + cmd.Short = i18n.G("View an identity") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Show identity configurations + +The argument must be a concatenation of the authentication method and either the +name or identifier of the identity, delimited by a forward slash. This command +will fail if an identity name is used that is not unique within the authentication +method. Use the identifier instead if this occurs. +`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityShow) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity argument")) + } + + authenticationMethod, nameOrID, ok := strings.Cut(resource.name, "/") + if !ok { + return fmt.Errorf("Malformed argument, expected `[:]/`, got %q", args[0]) + } + + // Show the identity + identity, _, err := resource.server.GetIdentity(authenticationMethod, nameOrID) + if err != nil { + return err + } + + data, err := yaml.Marshal(&identity) + if err != nil { + return err + } + + fmt.Printf("%s", data) + + return nil +} + +// Edit. +type cmdIdentityEdit struct { + global *cmdGlobal +} + +func (c *cmdIdentityEdit) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("edit", i18n.G("[:]")) + cmd.Short = i18n.G("Edit an identity as YAML") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Edit an identity as YAML`)) + cmd.Example = cli.FormatSection("", i18n.G( + `lxc auth identity edit / < identity.yaml + Update an identity using the content of identity.yaml`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityEdit) helpTemplate() string { + return i18n.G( + `### This is a YAML representation of the group. +### Any line starting with a '# will be ignored. +### +### An identity has the following format: +### authentication_method: oidc +### type: OIDC client +### identifier: jane.doe@example.com +### name: Jane Doe +### metadata: +### subject: auth0|123456789 +### projects: +### - default +### groups: +### - my-first-group +### +### Note that all identity information is shown but only the projects and groups can be modified`) +} + +func (c *cmdIdentityEdit) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity argument")) + } + + authenticationMethod, nameOrID, ok := strings.Cut(resource.name, "/") + if !ok { + return fmt.Errorf("Malformed argument, expected `[:]/`, got %q", args[0]) + } + + // If stdin isn't a terminal, read text from it + if !termios.IsTerminal(getStdinFd()) { + contents, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + + newdata := api.IdentityPut{} + err = yaml.Unmarshal(contents, &newdata) + if err != nil { + return err + } + + return resource.server.UpdateIdentity(authenticationMethod, nameOrID, newdata, "") + } + + // Extract the current value + identity, etag, err := resource.server.GetIdentity(authenticationMethod, nameOrID) + if err != nil { + return err + } + + data, err := yaml.Marshal(&identity) + if err != nil { + return err + } + + // Spawn the editor + content, err := shared.TextEditor("", []byte(c.helpTemplate()+"\n\n"+string(data))) + if err != nil { + return err + } + + for { + // Parse the text received from the editor + newdata := api.IdentityPut{} + err = yaml.Unmarshal(content, &newdata) + if err == nil { + err = resource.server.UpdateIdentity(authenticationMethod, nameOrID, newdata, etag) + } + + // Respawn the editor + if err != nil { + fmt.Fprintf(os.Stderr, i18n.G("Could not parse identity: %s")+"\n", err) + fmt.Println(i18n.G("Press enter to open the editor again or ctrl+c to abort change")) + + _, err := os.Stdin.Read(make([]byte, 1)) + if err != nil { + return err + } + + content, err = shared.TextEditor("", content) + if err != nil { + return err + } + + continue + } + + break + } + + return nil +} + +type cmdIdentityGroup struct { + global *cmdGlobal +} + +func (c *cmdIdentityGroup) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("group") + cmd.Short = i18n.G("Manage groups for the identity") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Manage groups for the identity`)) + + identityGroupAddCmd := cmdIdentityGroupAdd{global: c.global} + cmd.AddCommand(identityGroupAddCmd.command()) + + identityGroupRemoveCmd := cmdIdentityGroupRemove{global: c.global} + cmd.AddCommand(identityGroupRemoveCmd.command()) + + // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706 + cmd.Args = cobra.NoArgs + cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() } + return cmd +} + +type cmdIdentityGroupAdd struct { + global *cmdGlobal +} + +func (c *cmdIdentityGroupAdd) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("add", i18n.G("[:]/ ")) + cmd.Short = i18n.G("Add a group to an identity") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Add a group to an identity`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityGroupAdd) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 2, 2) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity argument")) + } + + authenticationMethod, nameOrID, ok := strings.Cut(resource.name, "/") + if !ok { + return fmt.Errorf("Malformed argument, expected `[:]/`, got %q", args[0]) + } + + identity, eTag, err := resource.server.GetIdentity(authenticationMethod, nameOrID) + if err != nil { + return err + } + + added := false + if !shared.ValueInSlice(args[1], identity.Groups) { + identity.Groups = append(identity.Groups, args[1]) + added = true + } + + if !added { + return fmt.Errorf("Identity %q is already a member of group %q", resource.name, args[1]) + } + + return resource.server.UpdateIdentity(authenticationMethod, nameOrID, identity.IdentityPut, eTag) +} + +type cmdIdentityGroupRemove struct { + global *cmdGlobal +} + +func (c *cmdIdentityGroupRemove) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("remove", i18n.G("[:]/ ")) + cmd.Short = i18n.G("Remove a group from an identity") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Remove a group from an identity`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityGroupRemove) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 2, 2) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity argument")) + } + + authenticationMethod, nameOrID, ok := strings.Cut(resource.name, "/") + if !ok { + return fmt.Errorf("Malformed argument, expected `[:]/`, got %q", args[0]) + } + + identity, eTag, err := resource.server.GetIdentity(authenticationMethod, nameOrID) + if err != nil { + return err + } + + if len(identity.Groups) == 0 { + return fmt.Errorf("Identity %q is not a member of any groups", resource.name) + } + + groups := make([]string, 0, len(identity.Groups)-1) + removed := false + for _, existingGroup := range identity.Groups { + if args[1] == existingGroup { + removed = true + continue + } + + groups = append(groups, existingGroup) + } + + if !removed { + return fmt.Errorf("Identity %q is not a member of group %q", resource.name, args[0]) + } + + identity.IdentityPut.Groups = groups + return resource.server.UpdateIdentity(authenticationMethod, nameOrID, identity.IdentityPut, eTag) +} + +type cmdPermission struct { + global *cmdGlobal +} + +func (c *cmdPermission) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("permission") + cmd.Aliases = []string{"perm"} + cmd.Short = i18n.G("Inspect permissions") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Inspect permissions`)) + + permissionListCmd := cmdPermissionList{global: c.global} + cmd.AddCommand(permissionListCmd.command()) + + // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706 + cmd.Args = cobra.NoArgs + cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() } + return cmd +} + +type cmdPermissionList struct { + global *cmdGlobal + flagMaxEntitlements int + flagFormat string +} + +func (c *cmdPermissionList) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("list", i18n.G("[:] [project=] [entity_type=]")) + cmd.Short = i18n.G("List permissions") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `List permissions`)) + + cmd.Flags().IntVar(&c.flagMaxEntitlements, "max-entitlements", 3, "Maximum number of unassigned entitlements to display before overflowing (set to zero to display all)") + cmd.Flags().StringVarP(&c.flagFormat, "format", "f", cli.TableFormatTable, "Display format (json, yaml, table, compact, csv)") + cmd.RunE = c.run + + return cmd +} + +func (c *cmdPermissionList) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 0, 3) + if exit { + return err + } + + remote := "" + if len(args) > 0 { + remote = args[0] + } + + if len(args) > 1 { + args = args[1:] + } + + // Parse remote + resources, err := c.global.ParseServers(remote) + if err != nil { + return err + } + + resource := resources[0] + + projectName := "" + entityType := entity.Type("") + for _, arg := range args { + k, v, ok := strings.Cut(arg, "=") + if !ok { + return fmt.Errorf("Badly formatted supplementary argument %q", arg) + } + + if k == "project" { + projectName = v + } else if k == "entity_type" { + entityType = entity.Type(v) + err = entityType.Validate() + if err != nil { + return fmt.Errorf("Invalid entity type in supplementary argument %q: %w", arg, err) + } + } else { + return fmt.Errorf("Available filters are `entity_type` and `project`, got %q", arg) + } + } + + permissionsInfo, err := resource.server.GetPermissionsInfo(lxd.GetPermissionsArgs{ + EntityType: string(entityType), + ProjectName: projectName, + }) + if err != nil { + return err + } + + // If we're displaying with JSON or YAML, display the raw data now. + if c.flagFormat == cli.TableFormatJSON || c.flagFormat == cli.TableFormatYAML { + return cli.RenderTable(c.flagFormat, nil, nil, permissionsInfo) + } + + // Otherwise, data returned from the permissions API can be condensed into a more easily viewable format. + // We'll group entitlements together by the API resource they are defined on, and separate the entitlements that + // are assigned to groups from the ones that are not assigned. + type displayPermission struct { + entityType string + url string + entitlementsAssigned map[string][]string + entitlementsNotAssigned []string + } + + i := 0 + var displayPermissions []*displayPermission + displayPermissionIdx := make(map[string]int) + for _, perm := range permissionsInfo { + idx, ok := displayPermissionIdx[perm.EntityReference] + if ok { + dp := displayPermissions[idx] + if len(perm.Groups) > 0 { + dp.entitlementsAssigned[perm.Entitlement] = perm.Groups + } else { + dp.entitlementsNotAssigned = append(dp.entitlementsNotAssigned, perm.Entitlement) + } + + continue + } + + dp := displayPermission{ + entityType: perm.EntityType, + url: perm.EntityReference, + entitlementsAssigned: make(map[string][]string), + } + + if len(perm.Groups) > 0 { + dp.entitlementsAssigned[perm.Entitlement] = perm.Groups + } else { + dp.entitlementsNotAssigned = append(dp.entitlementsNotAssigned, perm.Entitlement) + } + + displayPermissions = append(displayPermissions, &dp) + displayPermissionIdx[perm.EntityReference] = i + i++ + } + + columns := map[rune]cli.Column{ + 't': { + Header: "ENTITY TYPE", + DataFunc: func(a any) (string, error) { + p, _ := a.(*displayPermission) + return p.entityType, nil + }, + }, + 'u': { + Header: "URL", + DataFunc: func(a any) (string, error) { + p, _ := a.(*displayPermission) + return p.url, nil + }, + }, + 'e': { + Header: "ENTITLEMENTS ==> (GROUPS)", + DataFunc: func(a any) (string, error) { + p, _ := a.(*displayPermission) + var rowsAssigned []string + for k, v := range p.entitlementsAssigned { + // Pretty format for tables. + assignedRow := fmt.Sprintf("%s ==> (%s)", k, strings.Join(v, ", ")) + if c.flagFormat == cli.TableFormatCSV { + // Machine readable format for CSV. + assignedRow = fmt.Sprintf("%s:(%s)", k, strings.Join(v, ",")) + } + + rowsAssigned = append(rowsAssigned, assignedRow) + } + + // Sort the entitlements alphabetically, and put the assigned entitlements first. + sort.Strings(rowsAssigned) + sort.Strings(p.entitlementsNotAssigned) + + // Only show unassigned entitlements up to and including `--max-entitlements` + if c.flagMaxEntitlements > 0 && len(p.entitlementsNotAssigned) > c.flagMaxEntitlements { + p.entitlementsNotAssigned = p.entitlementsNotAssigned[:c.flagMaxEntitlements] + p.entitlementsNotAssigned = append(p.entitlementsNotAssigned[:c.flagMaxEntitlements], "...") + } + + rows := append(rowsAssigned, p.entitlementsNotAssigned...) + delimiter := "\n" + if c.flagFormat == cli.TableFormatCSV { + // Don't use newlines for CSV. We can use a comma because the field will be wrapped in quotes. + delimiter = "," + } + + return strings.Join(rows, delimiter), nil + }, + }, + } + + return cli.RenderSlice(displayPermissions, c.flagFormat, "tue", "u", columns) +} + +type cmdIdentityProviderGroup struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroup) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("identity-provider-group") + cmd.Aliases = []string{"idp-group"} + cmd.Short = i18n.G("Manage groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Manage groups`)) + + idpGroupCreateCmd := cmdIdentityProviderGroupCreate{global: c.global} + cmd.AddCommand(idpGroupCreateCmd.command()) + + idpGroupDeleteCmd := cmdIdentityProviderGroupDelete{global: c.global} + cmd.AddCommand(idpGroupDeleteCmd.command()) + + idpGroupEditCmd := cmdIdentityProviderGroupEdit{global: c.global} + cmd.AddCommand(idpGroupEditCmd.command()) + + idpGroupShowCmd := cmdIdentityProviderGroupShow{global: c.global} + cmd.AddCommand(idpGroupShowCmd.command()) + + idpGroupListCmd := cmdIdentityProviderGroupList{global: c.global} + cmd.AddCommand(idpGroupListCmd.command()) + + idpGroupRenameCmd := cmdIdentityProviderGroupRename{global: c.global} + cmd.AddCommand(idpGroupRenameCmd.command()) + + idpGroupGroupCmd := cmdIdentityProviderGroupGroup{global: c.global} + cmd.AddCommand(idpGroupGroupCmd.command()) + + // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706 + cmd.Args = cobra.NoArgs + cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() } + return cmd +} + +type cmdIdentityProviderGroupCreate struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroupCreate) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("create", i18n.G("[:]")) + cmd.Short = i18n.G("Create identity provider groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Create identity provider groups`)) + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityProviderGroupCreate) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity provider group name")) + } + + // Create the identity provider group + group := api.IdentityProviderGroup{} + group.Name = resource.name + + err = resource.server.CreateIdentityProviderGroup(group) + if err != nil { + return err + } + + if !c.global.flagQuiet { + fmt.Printf(i18n.G("Identity provider group %s created")+"\n", resource.name) + } + + return nil +} + +// Delete. +type cmdIdentityProviderGroupDelete struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroupDelete) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("delete", i18n.G("[:]")) + cmd.Aliases = []string{"rm"} + cmd.Short = i18n.G("Delete identity provider groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Delete identity provider groups`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityProviderGroupDelete) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity provider group name")) + } + + // Delete the identity provider group + err = resource.server.DeleteIdentityProviderGroup(resource.name) + if err != nil { + return err + } + + if !c.global.flagQuiet { + fmt.Printf(i18n.G("Group %s deleted")+"\n", resource.name) + } + + return nil +} + +// Edit. +type cmdIdentityProviderGroupEdit struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroupEdit) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("edit", i18n.G("[:]")) + cmd.Short = i18n.G("Edit identity provider groups as YAML") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Edit identity provider groups as YAML`)) + cmd.Example = cli.FormatSection("", i18n.G( + `lxc auth identity-provider-group edit < identity-provider-group.yaml + Update an identity provider group using the content of identity-provider-group.yaml`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityProviderGroupEdit) helpTemplate() string { + return i18n.G( + `### This is a YAML representation of the identity provider group. +### Any line starting with a '# will be ignored. +### +### An identity provider group has the following format: +### name: operations +### groups: +### - foo +### - bar +### +### Note that the name is shown but cannot be modified`) +} + +func (c *cmdIdentityProviderGroupEdit) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity provider group name")) + } + + // If stdin isn't a terminal, read text from it + if !termios.IsTerminal(getStdinFd()) { + contents, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + + newdata := api.IdentityProviderGroupPut{} + err = yaml.Unmarshal(contents, &newdata) + if err != nil { + return err + } + + return resource.server.UpdateIdentityProviderGroup(resource.name, newdata, "") + } + + // Extract the current value + group, etag, err := resource.server.GetIdentityProviderGroup(resource.name) + if err != nil { + return err + } + + data, err := yaml.Marshal(&group) + if err != nil { + return err + } + + // Spawn the editor + content, err := shared.TextEditor("", []byte(c.helpTemplate()+"\n\n"+string(data))) + if err != nil { + return err + } + + for { + // Parse the text received from the editor + newdata := api.IdentityProviderGroupPut{} + err = yaml.Unmarshal(content, &newdata) + if err == nil { + err = resource.server.UpdateIdentityProviderGroup(resource.name, newdata, etag) + } + + // Respawn the editor + if err != nil { + fmt.Fprintf(os.Stderr, i18n.G("Could not parse group: %s")+"\n", err) + fmt.Println(i18n.G("Press enter to open the editor again or ctrl+c to abort change")) + + _, err := os.Stdin.Read(make([]byte, 1)) + if err != nil { + return err + } + + content, err = shared.TextEditor("", content) + if err != nil { + return err + } + + continue + } + + break + } + + return nil +} + +type cmdIdentityProviderGroupList struct { + global *cmdGlobal + flagFormat string +} + +func (c *cmdIdentityProviderGroupList) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("list", i18n.G("[:]")) + cmd.Aliases = []string{"ls"} + cmd.Short = i18n.G("List identity provider groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `List identity provider groups`)) + + cmd.RunE = c.run + cmd.Flags().StringVarP(&c.flagFormat, "format", "f", "table", i18n.G("Format (csv|json|table|yaml|compact)")+"``") + + return cmd +} + +func (c *cmdIdentityProviderGroupList) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 0, 1) + if exit { + return err + } + + // Parse remote + remote := "" + if len(args) > 0 { + remote = args[0] + } + + resources, err := c.global.ParseServers(remote) + if err != nil { + return err + } + + resource := resources[0] + + // List identity provider groups + groups, err := resource.server.GetIdentityProviderGroups() + if err != nil { + return err + } + + data := [][]string{} + for _, group := range groups { + data = append(data, []string{group.Name, strings.Join(group.Groups, "\n")}) + } + + sort.Sort(cli.SortColumnsNaturally(data)) + + header := []string{ + i18n.G("NAME"), + i18n.G("GROUPS"), + } + + return cli.RenderTable(c.flagFormat, header, data, groups) +} + +// Rename. +type cmdIdentityProviderGroupRename struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroupRename) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("rename", i18n.G("[:] ")) + cmd.Aliases = []string{"mv"} + cmd.Short = i18n.G("Rename identity provider groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Rename identity provider groups`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityProviderGroupRename) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 2, 2) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity provider group name")) + } + + // Rename the group + err = resource.server.RenameIdentityProviderGroup(resource.name, api.IdentityProviderGroupPost{Name: args[1]}) + if err != nil { + return err + } + + if !c.global.flagQuiet { + fmt.Printf(i18n.G("Group %s renamed to %s")+"\n", resource.name, args[1]) + } + + return nil +} + +// Show. +type cmdIdentityProviderGroupShow struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroupShow) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("show", i18n.G("[:]")) + cmd.Short = i18n.G("Show an identity provider group") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Show an identity provider group`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityProviderGroupShow) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 1, 1) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing group name")) + } + + // Show the group + group, _, err := resource.server.GetIdentityProviderGroup(resource.name) + if err != nil { + return err + } + + data, err := yaml.Marshal(&group) + if err != nil { + return err + } + + fmt.Printf("%s", data) + + return nil +} + +type cmdIdentityProviderGroupGroup struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroupGroup) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("group") + cmd.Short = i18n.G("Manage identity provider group mappings") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Manage identity provider group mappings`)) + + identityProviderGroupGroupAddCmd := cmdIdentityProviderGroupGroupAdd{global: c.global} + cmd.AddCommand(identityProviderGroupGroupAddCmd.command()) + + identityProviderGroupGroupRemoveCmd := cmdIdentityProviderGroupGroupRemove{global: c.global} + cmd.AddCommand(identityProviderGroupGroupRemoveCmd.command()) + + // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706 + cmd.Args = cobra.NoArgs + cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() } + return cmd +} + +type cmdIdentityProviderGroupGroupAdd struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroupGroupAdd) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("add", i18n.G("[:] ")) + cmd.Short = i18n.G("Add a group to an identity provider group") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Add a group to an identity provider group`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityProviderGroupGroupAdd) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 2, 2) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity provider group name argument")) + } + + idpGroup, eTag, err := resource.server.GetIdentityProviderGroup(resource.name) + if err != nil { + return err + } + + added := false + if !shared.ValueInSlice(args[1], idpGroup.Groups) { + idpGroup.Groups = append(idpGroup.Groups, args[1]) + added = true + } + + if !added { + return fmt.Errorf("Identity group %q is already mapped to group %q", resource.name, args[1]) + } + + return resource.server.UpdateIdentityProviderGroup(resource.name, idpGroup.IdentityProviderGroupPut, eTag) +} + +type cmdIdentityProviderGroupGroupRemove struct { + global *cmdGlobal +} + +func (c *cmdIdentityProviderGroupGroupRemove) command() *cobra.Command { + cmd := &cobra.Command{} + cmd.Use = usage("remove", i18n.G("[:]/ ")) + cmd.Short = i18n.G("Remove identities from groups") + cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( + `Remove identities from groups`)) + + cmd.RunE = c.run + + return cmd +} + +func (c *cmdIdentityProviderGroupGroupRemove) run(cmd *cobra.Command, args []string) error { + // Quick checks. + exit, err := c.global.CheckArgs(cmd, args, 2, 2) + if exit { + return err + } + + // Parse remote + resources, err := c.global.ParseServers(args[0]) + if err != nil { + return err + } + + resource := resources[0] + + if resource.name == "" { + return fmt.Errorf(i18n.G("Missing identity provider group name argument")) + } + + idpGroup, eTag, err := resource.server.GetIdentityProviderGroup(resource.name) + if err != nil { + return err + } + + if len(idpGroup.Groups) == 0 { + return fmt.Errorf("Identity provider group %q is not mapped to any groups", resource.name) + } + + groups := make([]string, 0, len(idpGroup.Groups)-1) + removed := false + for _, existingGroup := range idpGroup.Groups { + if args[1] == existingGroup { + removed = true + continue + } + + groups = append(groups, existingGroup) + } + + if !removed { + return fmt.Errorf("Identity provider group %q is not mapped to group %q", resource.name, args[1]) + } + + idpGroup.IdentityProviderGroupPut.Groups = groups + return resource.server.UpdateIdentityProviderGroup(resource.name, idpGroup.IdentityProviderGroupPut, eTag) +} diff --git a/lxc/main.go b/lxc/main.go index b939f60817ee..54fc862a6a85 100644 --- a/lxc/main.go +++ b/lxc/main.go @@ -254,6 +254,9 @@ For help with any of those, simply call them with --help.`)) warningCmd := cmdWarning{global: &globalCmd} app.AddCommand(warningCmd.Command()) + authCmd := cmdAuth{global: &globalCmd} + app.AddCommand(authCmd.command()) + // Get help command app.InitDefaultHelpCmd() var help *cobra.Command From ca11d031da25819b7121b65623eb82d62b5069e5 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 18:14:36 +0000 Subject: [PATCH 33/34] i18n: Runs make i18n. Signed-off-by: Mark Laing --- po/ar.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/ber.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/bg.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/ca.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/cs.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/de.po | 492 ++++++++++++++++++++++++++++++++++++++++++------- po/el.po | 445 +++++++++++++++++++++++++++++++++++++++------ po/eo.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/es.po | 458 ++++++++++++++++++++++++++++++++++++++++------ po/fa.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/fi.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/fr.po | 494 +++++++++++++++++++++++++++++++++++++++++++------- po/he.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/hi.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/id.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/it.po | 459 ++++++++++++++++++++++++++++++++++++++++------ po/ja.po | 489 ++++++++++++++++++++++++++++++++++++++++++------- po/ka.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/ko.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/lxd.pot | 331 ++++++++++++++++++++++++++++++--- po/mr.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/nb_NO.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/nl.po | 442 ++++++++++++++++++++++++++++++++++++++------ po/pa.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/pl.po | 446 +++++++++++++++++++++++++++++++++++++++------ po/pt.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/pt_BR.po | 469 ++++++++++++++++++++++++++++++++++++++++------- po/ru.po | 487 ++++++++++++++++++++++++++++++++++++++++++------- po/si.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/sl.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/sr.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/sv.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/te.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/th.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/tr.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/tzm.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/ug.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/uk.po | 435 ++++++++++++++++++++++++++++++++++++++------ po/zh_Hans.po | 446 +++++++++++++++++++++++++++++++++++++++------ po/zh_Hant.po | 435 ++++++++++++++++++++++++++++++++++++++------ 40 files changed, 15278 insertions(+), 2360 deletions(-) diff --git a/po/ar.po b/po/ar.po index 7b982ff3fed5..0bc45eaf638e 100644 --- a/po/ar.po +++ b/po/ar.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -130,6 +130,68 @@ msgid "" "### Note that the name is shown but cannot be changed" msgstr "" +#: lxc/auth.go:213 +msgid "" +"### This is a YAML representation of the group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### A group has the following format:\n" +"### name: my-first-group\n" +"### description: My first group.\n" +"### permissions:\n" +"### - entity_type: project\n" +"### url: /1.0/projects/default\n" +"### entitlement: can_view\n" +"### identities:\n" +"### - authentication_method: oidc\n" +"### type: OIDC client\n" +"### identifier: jane.doe@example.com\n" +"### name: Jane Doe\n" +"### metadata:\n" +"### subject: auth0|123456789\n" +"### identity_provider_groups:\n" +"### - sales\n" +"### - operations\n" +"###\n" +"### Note that all group information is shown but only the description and " +"permissions can be modified" +msgstr "" + +#: lxc/auth.go:901 +msgid "" +"### This is a YAML representation of the group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### An identity has the following format:\n" +"### authentication_method: oidc\n" +"### type: OIDC client\n" +"### identifier: jane.doe@example.com\n" +"### name: Jane Doe\n" +"### metadata:\n" +"### subject: auth0|123456789\n" +"### projects:\n" +"### - default\n" +"### groups:\n" +"### - my-first-group\n" +"###\n" +"### Note that all identity information is shown but only the projects and " +"groups can be modified" +msgstr "" + +#: lxc/auth.go:1516 +msgid "" +"### This is a YAML representation of the identity provider group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### An identity provider group has the following format:\n" +"### name: operations\n" +"### groups:\n" +"### - foo\n" +"### - bar\n" +"###\n" +"### Note that the name is shown but cannot be modified" +msgstr "" + #: lxc/image.go:378 msgid "" "### This is a YAML representation of the image properties.\n" @@ -495,6 +557,10 @@ msgstr "" msgid "AUTH TYPE" msgstr "" +#: lxc/auth.go:807 +msgid "AUTHENTICATION METHOD" +msgstr "" + #: lxc/remote.go:99 msgid "Accept certificate" msgstr "" @@ -529,6 +595,14 @@ msgstr "" msgid "Add a cluster member to a cluster group" msgstr "" +#: lxc/auth.go:1039 lxc/auth.go:1040 +msgid "Add a group to an identity" +msgstr "" + +#: lxc/auth.go:1805 lxc/auth.go:1806 +msgid "Add a group to an identity provider group" +msgstr "" + #: lxc/network_zone.go:1239 msgid "Add a network zone record entry" msgstr "" @@ -594,6 +668,10 @@ msgid "" "restricted to one or more projects.\n" msgstr "" +#: lxc/auth.go:515 lxc/auth.go:516 +msgid "Add permissions to groups" +msgstr "" + #: lxc/network_forward.go:744 lxc/network_forward.go:745 msgid "Add ports to a forward" msgstr "" @@ -1269,6 +1347,16 @@ msgstr "" msgid "Could not find certificate key file path: %s" msgstr "" +#: lxc/auth.go:300 lxc/auth.go:1591 +#, c-format +msgid "Could not parse group: %s" +msgstr "" + +#: lxc/auth.go:987 +#, c-format +msgid "Could not parse identity: %s" +msgstr "" + #: lxc/cluster.go:1113 #, c-format msgid "Could not read certificate file: %s with error: %v" @@ -1317,6 +1405,14 @@ msgstr "" msgid "Create any directories necessary" msgstr "" +#: lxc/auth.go:97 lxc/auth.go:98 +msgid "Create groups" +msgstr "" + +#: lxc/auth.go:1402 lxc/auth.go:1403 +msgid "Create identity provider groups" +msgstr "" + #: lxc/snapshot.go:27 msgid "Create instance snapshots" msgstr "" @@ -1416,8 +1512,8 @@ msgstr "" msgid "DEFAULT TARGET ADDRESS" msgstr "" -#: lxc/cluster.go:188 lxc/cluster_group.go:438 lxc/image.go:1058 -#: lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 +#: lxc/auth.go:376 lxc/cluster.go:188 lxc/cluster_group.go:438 +#: lxc/image.go:1058 lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 #: lxc/network_acl.go:148 lxc/network_forward.go:145 #: lxc/network_load_balancer.go:148 lxc/network_peer.go:140 #: lxc/network_zone.go:139 lxc/network_zone.go:742 lxc/operation.go:172 @@ -1463,6 +1559,14 @@ msgstr "" msgid "Delete files in instances" msgstr "" +#: lxc/auth.go:151 lxc/auth.go:152 +msgid "Delete groups" +msgstr "" + +#: lxc/auth.go:1454 lxc/auth.go:1455 +msgid "Delete identity provider groups" +msgstr "" + #: lxc/image_alias.go:106 lxc/image_alias.go:107 msgid "Delete image aliases" msgstr "" @@ -1537,18 +1641,26 @@ msgstr "" #: lxc/action.go:32 lxc/action.go:53 lxc/action.go:75 lxc/action.go:98 #: lxc/alias.go:23 lxc/alias.go:60 lxc/alias.go:110 lxc/alias.go:159 -#: lxc/alias.go:214 lxc/cluster.go:29 lxc/cluster.go:122 lxc/cluster.go:206 -#: lxc/cluster.go:255 lxc/cluster.go:306 lxc/cluster.go:367 lxc/cluster.go:439 -#: lxc/cluster.go:471 lxc/cluster.go:521 lxc/cluster.go:604 lxc/cluster.go:689 -#: lxc/cluster.go:804 lxc/cluster.go:880 lxc/cluster.go:982 lxc/cluster.go:1061 -#: lxc/cluster.go:1168 lxc/cluster.go:1190 lxc/cluster_group.go:30 -#: lxc/cluster_group.go:84 lxc/cluster_group.go:157 lxc/cluster_group.go:214 -#: lxc/cluster_group.go:266 lxc/cluster_group.go:382 lxc/cluster_group.go:456 -#: lxc/cluster_group.go:529 lxc/cluster_group.go:577 lxc/cluster_group.go:631 -#: lxc/cluster_role.go:23 lxc/cluster_role.go:50 lxc/cluster_role.go:106 -#: lxc/config.go:32 lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 -#: lxc/config.go:734 lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 -#: lxc/config.go:988 lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 +#: lxc/alias.go:214 lxc/auth.go:30 lxc/auth.go:59 lxc/auth.go:98 +#: lxc/auth.go:152 lxc/auth.go:201 lxc/auth.go:332 lxc/auth.go:392 +#: lxc/auth.go:441 lxc/auth.go:493 lxc/auth.go:516 lxc/auth.go:575 +#: lxc/auth.go:731 lxc/auth.go:759 lxc/auth.go:826 lxc/auth.go:889 +#: lxc/auth.go:1017 lxc/auth.go:1040 lxc/auth.go:1098 lxc/auth.go:1167 +#: lxc/auth.go:1189 lxc/auth.go:1365 lxc/auth.go:1403 lxc/auth.go:1455 +#: lxc/auth.go:1504 lxc/auth.go:1623 lxc/auth.go:1683 lxc/auth.go:1732 +#: lxc/auth.go:1783 lxc/auth.go:1806 lxc/auth.go:1859 lxc/cluster.go:29 +#: lxc/cluster.go:122 lxc/cluster.go:206 lxc/cluster.go:255 lxc/cluster.go:306 +#: lxc/cluster.go:367 lxc/cluster.go:439 lxc/cluster.go:471 lxc/cluster.go:521 +#: lxc/cluster.go:604 lxc/cluster.go:689 lxc/cluster.go:804 lxc/cluster.go:880 +#: lxc/cluster.go:982 lxc/cluster.go:1061 lxc/cluster.go:1168 +#: lxc/cluster.go:1190 lxc/cluster_group.go:30 lxc/cluster_group.go:84 +#: lxc/cluster_group.go:157 lxc/cluster_group.go:214 lxc/cluster_group.go:266 +#: lxc/cluster_group.go:382 lxc/cluster_group.go:456 lxc/cluster_group.go:529 +#: lxc/cluster_group.go:577 lxc/cluster_group.go:631 lxc/cluster_role.go:23 +#: lxc/cluster_role.go:50 lxc/cluster_role.go:106 lxc/config.go:32 +#: lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 lxc/config.go:734 +#: lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 lxc/config.go:988 +#: lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 #: lxc/config_device.go:24 lxc/config_device.go:78 lxc/config_device.go:208 #: lxc/config_device.go:285 lxc/config_device.go:356 lxc/config_device.go:450 #: lxc/config_device.go:548 lxc/config_device.go:555 lxc/config_device.go:668 @@ -1566,7 +1678,7 @@ msgstr "" #: lxc/image.go:651 lxc/image.go:884 lxc/image.go:1019 lxc/image.go:1338 #: lxc/image.go:1418 lxc/image.go:1477 lxc/image.go:1529 lxc/image.go:1585 #: lxc/image_alias.go:24 lxc/image_alias.go:60 lxc/image_alias.go:107 -#: lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:28 +#: lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:29 #: lxc/info.go:33 lxc/init.go:42 lxc/launch.go:24 lxc/list.go:49 lxc/main.go:82 #: lxc/manpage.go:22 lxc/monitor.go:33 lxc/move.go:37 lxc/network.go:32 #: lxc/network.go:135 lxc/network.go:220 lxc/network.go:293 lxc/network.go:372 @@ -1797,6 +1909,10 @@ msgstr "" msgid "Edit a cluster group" msgstr "" +#: lxc/auth.go:888 lxc/auth.go:889 +msgid "Edit an identity as YAML" +msgstr "" + #: lxc/cluster.go:688 lxc/cluster.go:689 msgid "Edit cluster member configurations as YAML" msgstr "" @@ -1805,6 +1921,14 @@ msgstr "" msgid "Edit files in instances" msgstr "" +#: lxc/auth.go:200 lxc/auth.go:201 +msgid "Edit groups as YAML" +msgstr "" + +#: lxc/auth.go:1503 lxc/auth.go:1504 +msgid "Edit identity provider groups as YAML" +msgstr "" + #: lxc/image.go:362 lxc/image.go:363 msgid "Edit image properties" msgstr "" @@ -2272,15 +2396,16 @@ msgid "" "Are you really sure you want to force removing %s? (yes/no): " msgstr "" -#: lxc/alias.go:112 lxc/cluster.go:124 lxc/cluster.go:881 -#: lxc/cluster_group.go:384 lxc/config_template.go:242 lxc/config_trust.go:352 -#: lxc/config_trust.go:434 lxc/image.go:1045 lxc/image_alias.go:157 -#: lxc/list.go:133 lxc/network.go:916 lxc/network.go:1007 lxc/network_acl.go:97 -#: lxc/network_allocations.go:57 lxc/network_forward.go:89 -#: lxc/network_load_balancer.go:93 lxc/network_peer.go:84 -#: lxc/network_zone.go:88 lxc/network_zone.go:692 lxc/operation.go:108 -#: lxc/profile.go:617 lxc/project.go:412 lxc/project.go:804 lxc/remote.go:666 -#: lxc/storage.go:588 lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 +#: lxc/alias.go:112 lxc/auth.go:336 lxc/auth.go:763 lxc/auth.go:1627 +#: lxc/cluster.go:124 lxc/cluster.go:881 lxc/cluster_group.go:384 +#: lxc/config_template.go:242 lxc/config_trust.go:352 lxc/config_trust.go:434 +#: lxc/image.go:1045 lxc/image_alias.go:157 lxc/list.go:133 lxc/network.go:916 +#: lxc/network.go:1007 lxc/network_acl.go:97 lxc/network_allocations.go:57 +#: lxc/network_forward.go:89 lxc/network_load_balancer.go:93 +#: lxc/network_peer.go:84 lxc/network_zone.go:88 lxc/network_zone.go:692 +#: lxc/operation.go:108 lxc/profile.go:617 lxc/project.go:412 +#: lxc/project.go:804 lxc/remote.go:666 lxc/storage.go:588 +#: lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 #: lxc/storage_volume.go:1422 lxc/warning.go:93 msgid "Format (csv|json|table|yaml|compact)" msgstr "" @@ -2329,6 +2454,10 @@ msgstr "" msgid "GPUs:" msgstr "" +#: lxc/auth.go:811 lxc/auth.go:1667 +msgid "GROUPS" +msgstr "" + #: lxc/manpage.go:21 lxc/manpage.go:22 msgid "Generate manpages for all commands" msgstr "" @@ -2474,6 +2603,21 @@ msgstr "" msgid "Given target %q does not match source volume location %q" msgstr "" +#: lxc/auth.go:136 +#, c-format +msgid "Group %s created" +msgstr "" + +#: lxc/auth.go:186 lxc/auth.go:1489 +#, c-format +msgid "Group %s deleted" +msgstr "" + +#: lxc/auth.go:426 lxc/auth.go:1717 +#, c-format +msgid "Group %s renamed to %s" +msgstr "" + #: lxc/exec.go:60 msgid "Group ID to run the command as (default 0)" msgstr "" @@ -2528,6 +2672,10 @@ msgstr "" msgid "ID: %s" msgstr "" +#: lxc/auth.go:810 +msgid "IDENTIFIER" +msgstr "" + #: lxc/project.go:499 msgid "IMAGES" msgstr "" @@ -2556,6 +2704,11 @@ msgstr "" msgid "ISSUE DATE" msgstr "" +#: lxc/auth.go:1439 +#, c-format +msgid "Identity provider group %s created" +msgstr "" + #: lxc/rebuild.go:32 msgid "If an instance is running, stop it and then rebuild it" msgstr "" @@ -2568,7 +2721,7 @@ msgstr "" msgid "If the snapshot name already exists, delete and create a new one" msgstr "" -#: lxc/main.go:395 +#: lxc/main.go:398 msgid "" "If this is your first time running LXD on this machine, you should also run: " "lxd init" @@ -2632,7 +2785,7 @@ msgstr "" msgid "Import backups of custom volumes including their snapshots." msgstr "" -#: lxc/import.go:28 +#: lxc/import.go:29 msgid "Import backups of instances including their snapshots." msgstr "" @@ -2651,7 +2804,7 @@ msgstr "" msgid "Import images into the image store" msgstr "" -#: lxc/import.go:27 +#: lxc/import.go:28 msgid "Import instance backups" msgstr "" @@ -2664,7 +2817,7 @@ msgstr "" msgid "Importing custom volume: %s" msgstr "" -#: lxc/import.go:94 +#: lxc/import.go:96 #, c-format msgid "Importing instance: %s" msgstr "" @@ -2677,6 +2830,10 @@ msgstr "" msgid "Input data" msgstr "" +#: lxc/auth.go:1166 lxc/auth.go:1167 +msgid "Inspect permissions" +msgstr "" + #: lxc/info.go:684 msgid "Instance Only" msgstr "" @@ -2793,7 +2950,7 @@ msgstr "" msgid "Invalid new snapshot name, parent volume must be the same as source" msgstr "" -#: lxc/main.go:495 +#: lxc/main.go:498 msgid "Invalid number of arguments" msgstr "" @@ -2963,6 +3120,18 @@ msgstr "" msgid "List background operations" msgstr "" +#: lxc/auth.go:331 lxc/auth.go:332 +msgid "List groups" +msgstr "" + +#: lxc/auth.go:758 lxc/auth.go:759 +msgid "List identities" +msgstr "" + +#: lxc/auth.go:1622 lxc/auth.go:1623 +msgid "List identity provider groups" +msgstr "" + #: lxc/image_alias.go:151 msgid "List image aliases" msgstr "" @@ -3112,6 +3281,10 @@ msgstr "" msgid "List operations from all projects" msgstr "" +#: lxc/auth.go:1188 lxc/auth.go:1189 +msgid "List permissions" +msgstr "" + #: lxc/profile.go:612 lxc/profile.go:613 msgid "List profiles" msgstr "" @@ -3303,6 +3476,22 @@ msgstr "" msgid "Manage files in instances" msgstr "" +#: lxc/auth.go:58 lxc/auth.go:59 lxc/auth.go:1364 lxc/auth.go:1365 +msgid "Manage groups" +msgstr "" + +#: lxc/auth.go:1016 lxc/auth.go:1017 +msgid "Manage groups for the identity" +msgstr "" + +#: lxc/auth.go:730 lxc/auth.go:731 +msgid "Manage identities" +msgstr "" + +#: lxc/auth.go:1782 lxc/auth.go:1783 +msgid "Manage identity provider group mappings" +msgstr "" + #: lxc/image_alias.go:23 lxc/image_alias.go:24 msgid "Manage image aliases" msgstr "" @@ -3390,6 +3579,10 @@ msgstr "" msgid "Manage network zones" msgstr "" +#: lxc/auth.go:492 lxc/auth.go:493 +msgid "Manage permissions" +msgstr "" + #: lxc/profile.go:28 lxc/profile.go:29 msgid "Manage profiles" msgstr "" @@ -3438,6 +3631,10 @@ msgstr "" msgid "Manage trusted clients" msgstr "" +#: lxc/auth.go:29 lxc/auth.go:30 +msgid "Manage user authorization" +msgstr "" + #: lxc/warning.go:28 lxc/warning.go:29 msgid "Manage warnings" msgstr "" @@ -3531,6 +3728,23 @@ msgstr "" msgid "Missing cluster member name" msgstr "" +#: lxc/auth.go:122 lxc/auth.go:176 lxc/auth.go:254 lxc/auth.go:416 +#: lxc/auth.go:465 lxc/auth.go:540 lxc/auth.go:599 lxc/auth.go:1756 +msgid "Missing group name" +msgstr "" + +#: lxc/auth.go:856 lxc/auth.go:936 lxc/auth.go:1064 lxc/auth.go:1122 +msgid "Missing identity argument" +msgstr "" + +#: lxc/auth.go:1426 lxc/auth.go:1479 lxc/auth.go:1545 lxc/auth.go:1707 +msgid "Missing identity provider group name" +msgstr "" + +#: lxc/auth.go:1830 lxc/auth.go:1883 +msgid "Missing identity provider group name argument" +msgstr "" + #: lxc/config_metadata.go:103 lxc/config_metadata.go:204 #: lxc/config_template.go:91 lxc/config_template.go:134 #: lxc/config_template.go:176 lxc/config_template.go:265 @@ -3749,13 +3963,13 @@ msgstr "" msgid "Must supply instance name for: " msgstr "" -#: lxc/cluster.go:183 lxc/cluster.go:964 lxc/cluster_group.go:437 -#: lxc/config_trust.go:409 lxc/config_trust.go:514 lxc/list.go:565 -#: lxc/network.go:980 lxc/network_acl.go:147 lxc/network_peer.go:139 -#: lxc/network_zone.go:138 lxc/network_zone.go:741 lxc/profile.go:657 -#: lxc/project.go:498 lxc/remote.go:724 lxc/storage.go:638 -#: lxc/storage_bucket.go:506 lxc/storage_bucket.go:826 -#: lxc/storage_volume.go:1510 +#: lxc/auth.go:375 lxc/auth.go:809 lxc/auth.go:1666 lxc/cluster.go:183 +#: lxc/cluster.go:964 lxc/cluster_group.go:437 lxc/config_trust.go:409 +#: lxc/config_trust.go:514 lxc/list.go:565 lxc/network.go:980 +#: lxc/network_acl.go:147 lxc/network_peer.go:139 lxc/network_zone.go:138 +#: lxc/network_zone.go:741 lxc/profile.go:657 lxc/project.go:498 +#: lxc/remote.go:724 lxc/storage.go:638 lxc/storage_bucket.go:506 +#: lxc/storage_bucket.go:826 lxc/storage_volume.go:1510 msgid "NAME" msgstr "" @@ -3938,7 +4152,7 @@ msgstr "" msgid "New aliases to add to the image" msgstr "" -#: lxc/copy.go:54 lxc/init.go:51 lxc/move.go:59 +#: lxc/copy.go:54 lxc/import.go:37 lxc/init.go:51 lxc/move.go:59 msgid "New key/value to apply to a specific device" msgstr "" @@ -4097,7 +4311,7 @@ msgstr "" msgid "Partitions:" msgstr "" -#: lxc/main.go:358 +#: lxc/main.go:361 #, c-format msgid "Password for %s: " msgstr "" @@ -4139,10 +4353,11 @@ msgstr "" msgid "Press ctrl+c to finish" msgstr "" -#: lxc/cluster.go:771 lxc/cluster_group.go:340 lxc/config.go:273 -#: lxc/config.go:348 lxc/config.go:1278 lxc/config_metadata.go:148 -#: lxc/config_template.go:206 lxc/config_trust.go:315 lxc/image.go:457 -#: lxc/network.go:687 lxc/network_acl.go:621 lxc/network_forward.go:636 +#: lxc/auth.go:301 lxc/auth.go:988 lxc/auth.go:1592 lxc/cluster.go:771 +#: lxc/cluster_group.go:340 lxc/config.go:273 lxc/config.go:348 +#: lxc/config.go:1278 lxc/config_metadata.go:148 lxc/config_template.go:206 +#: lxc/config_trust.go:315 lxc/image.go:457 lxc/network.go:687 +#: lxc/network_acl.go:621 lxc/network_forward.go:636 #: lxc/network_load_balancer.go:638 lxc/network_peer.go:611 #: lxc/network_zone.go:552 lxc/network_zone.go:1144 lxc/profile.go:519 #: lxc/project.go:316 lxc/storage.go:311 lxc/storage_bucket.go:344 @@ -4489,6 +4704,10 @@ msgstr "" msgid "Remove a cluster member from a cluster group" msgstr "" +#: lxc/auth.go:1097 lxc/auth.go:1098 +msgid "Remove a group from an identity" +msgstr "" + #: lxc/cluster.go:520 lxc/cluster.go:521 msgid "Remove a member from the cluster" msgstr "" @@ -4521,6 +4740,10 @@ msgstr "" msgid "Remove entries from a network zone record" msgstr "" +#: lxc/auth.go:1858 lxc/auth.go:1859 +msgid "Remove identities from groups" +msgstr "" + #: lxc/config_device.go:449 lxc/config_device.go:450 msgid "Remove instance devices" msgstr "" @@ -4529,6 +4752,10 @@ msgstr "" msgid "Remove member from group" msgstr "" +#: lxc/auth.go:574 lxc/auth.go:575 +msgid "Remove permissions from groups" +msgstr "" + #: lxc/network_forward.go:809 lxc/network_forward.go:810 msgid "Remove ports from a forward" msgstr "" @@ -4570,6 +4797,14 @@ msgstr "" msgid "Rename aliases" msgstr "" +#: lxc/auth.go:391 lxc/auth.go:392 +msgid "Rename groups" +msgstr "" + +#: lxc/auth.go:1682 lxc/auth.go:1683 +msgid "Rename identity provider groups" +msgstr "" + #: lxc/rename.go:20 lxc/rename.go:21 msgid "Rename instances and snapshots" msgstr "" @@ -5075,6 +5310,10 @@ msgstr "" msgid "Show all information messages" msgstr "" +#: lxc/auth.go:1731 lxc/auth.go:1732 +msgid "Show an identity provider group" +msgstr "" + #: lxc/cluster_group.go:576 lxc/cluster_group.go:577 msgid "Show cluster group configurations" msgstr "" @@ -5099,6 +5338,23 @@ msgstr "" msgid "Show full device configuration" msgstr "" +#: lxc/auth.go:440 lxc/auth.go:441 +msgid "Show group configurations" +msgstr "" + +#: lxc/auth.go:826 +msgid "" +"Show identity configurations\n" +"\n" +"The argument must be a concatenation of the authentication method and either " +"the \n" +"name or identifier of the identity, delimited by a forward slash. This " +"command\n" +"will fail if an identity name is used that is not unique within the " +"authentication\n" +"method. Use the identifier instead if this occurs.\n" +msgstr "" + #: lxc/image.go:1417 lxc/image.go:1418 msgid "Show image properties" msgstr "" @@ -5119,7 +5375,7 @@ msgstr "" msgid "Show instance or server information" msgstr "" -#: lxc/main.go:268 lxc/main.go:269 +#: lxc/main.go:271 lxc/main.go:272 msgid "Show less common commands" msgstr "" @@ -5350,7 +5606,7 @@ msgstr "" msgid "Storage pool %s pending on member %s" msgstr "" -#: lxc/copy.go:60 lxc/import.go:35 lxc/init.go:54 lxc/move.go:65 +#: lxc/copy.go:60 lxc/import.go:36 lxc/init.go:54 lxc/move.go:65 msgid "Storage pool name" msgstr "" @@ -5415,9 +5671,9 @@ msgstr "" msgid "TOKEN" msgstr "" -#: lxc/config_trust.go:408 lxc/image.go:1062 lxc/image_alias.go:236 -#: lxc/list.go:571 lxc/network.go:981 lxc/network.go:1055 -#: lxc/network_allocations.go:26 lxc/operation.go:171 +#: lxc/auth.go:808 lxc/config_trust.go:408 lxc/image.go:1062 +#: lxc/image_alias.go:236 lxc/list.go:571 lxc/network.go:981 +#: lxc/network.go:1055 lxc/network_allocations.go:26 lxc/operation.go:171 #: lxc/storage_volume.go:1509 lxc/warning.go:215 msgid "TYPE" msgstr "" @@ -5617,7 +5873,7 @@ msgstr "" msgid "This LXD server is not available on the network" msgstr "" -#: lxc/main.go:290 +#: lxc/main.go:293 msgid "" "This client hasn't been configured to use a remote LXD server yet.\n" "As your platform can't run native Linux instances, you must connect to a " @@ -5653,7 +5909,7 @@ msgstr "" msgid "To detach from the console, press: +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6055,6 +6311,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6122,15 +6382,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6154,6 +6415,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6211,6 +6476,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6219,15 +6492,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6758,6 +7054,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6851,7 +7168,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/ber.po b/po/ber.po index afecea21e10f..af94e8dcb042 100644 --- a/po/ber.po +++ b/po/ber.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:10+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Berber +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/bg.po b/po/bg.po index e9e2ba6f666c..3c3d8245c3c1 100644 --- a/po/bg.po +++ b/po/bg.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:09+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Bulgarian +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/ca.po b/po/ca.po index 8d3249235a73..c54dcbe4dc6e 100644 --- a/po/ca.po +++ b/po/ca.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:10+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Catalan +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/cs.po b/po/cs.po index 995dbb935cdc..d151178ea180 100644 --- a/po/cs.po +++ b/po/cs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:11+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Czech +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/de.po b/po/de.po index cf198e2c793a..cb258cd44520 100644 --- a/po/de.po +++ b/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: LXD\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:06+0000\n" "Last-Translator: Krombel \n" "Language-Team: German +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6709,6 +6993,10 @@ msgstr "" "Optionen:\n" "\n" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6793,11 +7081,12 @@ msgstr "" "\n" "lxd %s \n" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 #, fuzzy msgid "[:]" msgstr "" @@ -6805,7 +7094,7 @@ msgstr "" "\n" "lxd %s \n" -#: lxc/import.go:26 +#: lxc/import.go:27 #, fuzzy msgid "[:] []" msgstr "" @@ -6856,6 +7145,10 @@ msgstr "" "Entfernt einen Container (oder Sicherungspunkt) und alle dazugehörigen\n" "Daten (Konfiguration, Sicherungspunkte, ...).\n" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 #, fuzzy @@ -6972,6 +7265,14 @@ msgstr "" "\n" "lxd %s \n" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 #, fuzzy msgid "[:]" @@ -6990,8 +7291,9 @@ msgstr "" "Entfernt einen Container (oder Sicherungspunkt) und alle dazugehörigen\n" "Daten (Konfiguration, Sicherungspunkte, ...).\n" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 #, fuzzy msgid "[:]" msgstr "" @@ -6999,6 +7301,16 @@ msgstr "" "\n" "lxd %s \n" +#: lxc/auth.go:514 lxc/auth.go:572 +#, fuzzy +msgid "" +"[:] [] " +"[=...]" +msgstr "" +"Ändert den Laufzustand eines Containers in %s.\n" +"\n" +"lxd %s \n" + #: lxc/cluster_group.go:526 #, fuzzy msgid "[:] " @@ -7007,6 +7319,39 @@ msgstr "" "\n" "lxd %s \n" +#: lxc/auth.go:389 +#, fuzzy +msgid "[:] " +msgstr "" +"Ändert den Laufzustand eines Containers in %s.\n" +"\n" +"lxd %s \n" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +#, fuzzy +msgid "[:]" +msgstr "" +"Ändert den Laufzustand eines Containers in %s.\n" +"\n" +"lxd %s \n" + +#: lxc/auth.go:1804 +#, fuzzy +msgid "[:] " +msgstr "" +"Löscht einen Container oder Container Sicherungspunkt.\n" +"\n" +"Entfernt einen Container (oder Sicherungspunkt) und alle dazugehörigen\n" +"Daten (Konfiguration, Sicherungspunkte, ...).\n" + +#: lxc/auth.go:1680 +#, fuzzy +msgid "[:] " +msgstr "" +"Ändert den Laufzustand eines Containers in %s.\n" +"\n" +"lxd %s \n" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 #, fuzzy msgid "[:]" @@ -8021,6 +8366,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -8114,7 +8480,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." @@ -8469,14 +8835,6 @@ msgstr "" #~ "Entfernt einen Container (oder Sicherungspunkt) und alle dazugehörigen\n" #~ "Daten (Konfiguration, Sicherungspunkte, ...).\n" -#, fuzzy -#~ msgid "Creates a new cluster groups" -#~ msgstr "Anhalten des Containers fehlgeschlagen!" - -#, fuzzy -#~ msgid "Rename a cluster groups" -#~ msgstr "Kein Zertifikat für diese Verbindung" - #, fuzzy #~ msgid "Manage network zone record entriess" #~ msgstr "Kein Zertifikat für diese Verbindung" diff --git a/po/el.po b/po/el.po index dc1a7f4446c5..0b2823e45052 100644 --- a/po/el.po +++ b/po/el.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:06+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Greek +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6160,6 +6426,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6227,15 +6497,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6259,6 +6530,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6316,6 +6591,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6324,15 +6607,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6863,6 +7169,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6956,7 +7283,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/eo.po b/po/eo.po index 46352698e157..781df71050f1 100644 --- a/po/eo.po +++ b/po/eo.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:11+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Esperanto +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/es.po b/po/es.po index a902426f0e35..376ff6e4a619 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2023-06-16 20:55+0000\n" "Last-Translator: Francisco Serrador \n" "Language-Team: Spanish +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6471,6 +6745,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "Cacheado: %s" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6540,16 +6818,17 @@ msgstr "No se puede proveer el nombre del container a la lista" msgid "[] [] []" msgstr "No se puede proveer el nombre del container a la lista" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 #, fuzzy msgid "[:]" msgstr "No se puede proveer el nombre del container a la lista" -#: lxc/import.go:26 +#: lxc/import.go:27 #, fuzzy msgid "[:] []" msgstr "No se puede proveer el nombre del container a la lista" @@ -6579,6 +6858,10 @@ msgstr "No se puede proveer el nombre del container a la lista" msgid "[:] [...]" msgstr "No se puede proveer el nombre del container a la lista" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 #, fuzzy @@ -6650,6 +6933,14 @@ msgstr "No se puede proveer el nombre del container a la lista" msgid "[:] " msgstr "No se puede proveer el nombre del container a la lista" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 #, fuzzy msgid "[:]" @@ -6660,17 +6951,45 @@ msgstr "No se puede proveer el nombre del container a la lista" msgid "[:]" msgstr "No se puede proveer el nombre del container a la lista" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 #, fuzzy msgid "[:]" msgstr "No se puede proveer el nombre del container a la lista" +#: lxc/auth.go:514 lxc/auth.go:572 +#, fuzzy +msgid "" +"[:] [] " +"[=...]" +msgstr "No se puede proveer el nombre del container a la lista" + #: lxc/cluster_group.go:526 #, fuzzy msgid "[:] " msgstr "No se puede proveer el nombre del container a la lista" +#: lxc/auth.go:389 +#, fuzzy +msgid "[:] " +msgstr "No se puede proveer el nombre del container a la lista" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +#, fuzzy +msgid "[:]" +msgstr "No se puede proveer el nombre del container a la lista" + +#: lxc/auth.go:1804 +#, fuzzy +msgid "[:] " +msgstr "No se puede proveer el nombre del container a la lista" + +#: lxc/auth.go:1680 +#, fuzzy +msgid "[:] " +msgstr "No se puede proveer el nombre del container a la lista" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 #, fuzzy msgid "[:]" @@ -7313,6 +7632,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -7406,7 +7746,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/fa.po b/po/fa.po index 72470a50d669..b8d64e573836 100644 --- a/po/fa.po +++ b/po/fa.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:09+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Persian +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/fi.po b/po/fi.po index ae608ca4a2b4..ef2da5a9ea43 100644 --- a/po/fi.po +++ b/po/fi.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:08+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Finnish +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/fr.po b/po/fr.po index c6e0939dd107..626b717966d4 100644 --- a/po/fr.po +++ b/po/fr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: LXD\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:06+0000\n" "Last-Translator: Wivik \n" "Language-Team: French +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 #, fuzzy msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" @@ -6850,6 +7133,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "Créé : %s" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6934,16 +7221,17 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 #, fuzzy msgid "[:]" msgstr "Serveur distant : %s" -#: lxc/import.go:26 +#: lxc/import.go:27 #, fuzzy msgid "[:] []" msgstr "" @@ -7003,6 +7291,10 @@ msgstr "" "Détruit les conteneurs ou les instantanés ainsi que toute donnée associée " "(configuration, instantanés, …)." +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 #, fuzzy @@ -7125,6 +7417,14 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 #, fuzzy msgid "[:]" @@ -7149,8 +7449,9 @@ msgstr "" "Détruit les conteneurs ou les instantanés ainsi que toute donnée associée " "(configuration, instantanés, …)." -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 #, fuzzy msgid "[:]" msgstr "" @@ -7158,6 +7459,16 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" +#: lxc/auth.go:514 lxc/auth.go:572 +#, fuzzy +msgid "" +"[:] [] " +"[=...]" +msgstr "" +"Change l'état d'un ou plusieurs conteneurs à %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + #: lxc/cluster_group.go:526 #, fuzzy msgid "[:] " @@ -7166,6 +7477,42 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" +#: lxc/auth.go:389 +#, fuzzy +msgid "[:] " +msgstr "" +"Change l'état d'un ou plusieurs conteneurs à %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +#, fuzzy +msgid "[:]" +msgstr "" +"Change l'état d'un ou plusieurs conteneurs à %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + +#: lxc/auth.go:1804 +#, fuzzy +msgid "[:] " +msgstr "" +"Supprimer des conteneurs ou des instantanés.\n" +"\n" +"lxc delete [:][/] [:][[/" +"]...]\n" +"\n" +"Détruit les conteneurs ou les instantanés ainsi que toute donnée associée " +"(configuration, instantanés, …)." + +#: lxc/auth.go:1680 +#, fuzzy +msgid "[:] " +msgstr "" +"Change l'état d'un ou plusieurs conteneurs à %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 #, fuzzy msgid "[:]" @@ -8274,6 +8621,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -8367,7 +8735,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." @@ -8759,14 +9127,6 @@ msgstr "oui" #~ "Détruit les conteneurs ou les instantanés ainsi que toute donnée associée " #~ "(configuration, instantanés, …)." -#, fuzzy -#~ msgid "Creates a new cluster groups" -#~ msgstr "Copie de l'image : %s" - -#, fuzzy -#~ msgid "Rename a cluster groups" -#~ msgstr "Copie de l'image : %s" - #, fuzzy #~ msgid "Manage network zone record entriess" #~ msgstr "Nom du réseau" diff --git a/po/he.po b/po/he.po index fa2585812bfd..476a2dc129a8 100644 --- a/po/he.po +++ b/po/he.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:11+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Hebrew +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6059,6 +6315,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6126,15 +6386,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6158,6 +6419,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6215,6 +6480,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6223,15 +6496,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6762,6 +7058,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6855,7 +7172,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/hi.po b/po/hi.po index 6ec58ccab5e6..684aa66c54f3 100644 --- a/po/hi.po +++ b/po/hi.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:08+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Hindi +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/id.po b/po/id.po index 01c63880f475..77135991a860 100644 --- a/po/id.po +++ b/po/id.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:07+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Indonesian +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/it.po b/po/it.po index d81ee68114ad..aa30fe3ad6a2 100644 --- a/po/it.po +++ b/po/it.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:07+0000\n" "Last-Translator: Luigi Operoso \n" "Language-Team: Italian +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6464,6 +6739,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6536,16 +6815,17 @@ msgstr "Creazione del container in corso" msgid "[] [] []" msgstr "Creazione del container in corso" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 #, fuzzy msgid "[:]" msgstr "Creazione del container in corso" -#: lxc/import.go:26 +#: lxc/import.go:27 #, fuzzy msgid "[:] []" msgstr "Creazione del container in corso" @@ -6575,6 +6855,10 @@ msgstr "Creazione del container in corso" msgid "[:] [...]" msgstr "Creazione del container in corso" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 #, fuzzy @@ -6646,6 +6930,14 @@ msgstr "Creazione del container in corso" msgid "[:] " msgstr "Creazione del container in corso" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 #, fuzzy msgid "[:]" @@ -6656,17 +6948,45 @@ msgstr "Creazione del container in corso" msgid "[:]" msgstr "Creazione del container in corso" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 #, fuzzy msgid "[:]" msgstr "Creazione del container in corso" +#: lxc/auth.go:514 lxc/auth.go:572 +#, fuzzy +msgid "" +"[:] [] " +"[=...]" +msgstr "Creazione del container in corso" + #: lxc/cluster_group.go:526 #, fuzzy msgid "[:] " msgstr "Creazione del container in corso" +#: lxc/auth.go:389 +#, fuzzy +msgid "[:] " +msgstr "Creazione del container in corso" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +#, fuzzy +msgid "[:]" +msgstr "Creazione del container in corso" + +#: lxc/auth.go:1804 +#, fuzzy +msgid "[:] " +msgstr "Creazione del container in corso" + +#: lxc/auth.go:1680 +#, fuzzy +msgid "[:] " +msgstr "Creazione del container in corso" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 #, fuzzy msgid "[:]" @@ -7309,6 +7629,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -7402,7 +7743,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/ja.po b/po/ja.po index f39f6bdfde53..8d789f432069 100644 --- a/po/ja.po +++ b/po/ja.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: LXD\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2023-03-10 15:14+0000\n" "Last-Translator: KATOH Yasufumi \n" "Language-Team: Japanese +a q" msgstr "コンソールから切り離すには +a q を押します" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6737,6 +7024,10 @@ msgstr "ベンダ: %v (%v)" msgid "Verb: %s (%s)" msgstr "Verb: %s (%s)" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "Volume Only" @@ -6809,15 +7100,16 @@ msgstr "[] []" msgid "[] [] []" msgstr "[] [] []" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "[:]" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "[:] []" @@ -6841,6 +7133,10 @@ msgstr "[:] [...]" msgid "[:] [...]" msgstr "[:] [...]" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6898,6 +7194,14 @@ msgstr "[:] " msgid "[:] " msgstr "[:] " +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "[:]" @@ -6906,15 +7210,43 @@ msgstr "[:]" msgid "[:]" msgstr "[:]" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "[:]" +#: lxc/auth.go:514 lxc/auth.go:572 +#, fuzzy +msgid "" +"[:] [] " +"[=...]" +msgstr "[:] =..." + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "[:] " +#: lxc/auth.go:389 +#, fuzzy +msgid "[:] " +msgstr "[:] " + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +#, fuzzy +msgid "[:]" +msgstr "[:]" + +#: lxc/auth.go:1804 +#, fuzzy +msgid "[:] " +msgstr "[:] " + +#: lxc/auth.go:1680 +#, fuzzy +msgid "[:] " +msgstr "[:] " + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "[:]" @@ -7474,6 +7806,36 @@ msgstr "" "lxc alias rename list my-list\n" " エイリアス名 \"list\" を \"my-list\" に変更します。" +#: lxc/auth.go:203 +#, fuzzy +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" +"lxc profile edit < profile.yaml\n" +" profile.yaml の内容でプロファイルを更新します" + +#: lxc/auth.go:891 +#, fuzzy +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" +"lxc profile edit < profile.yaml\n" +" profile.yaml の内容でプロファイルを更新します" + +#: lxc/auth.go:1506 +#, fuzzy +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" +"lxc profile edit < profile.yaml\n" +" profile.yaml の内容でプロファイルを更新します" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -7611,7 +7973,7 @@ msgstr "" "lxc image edit < image.yaml\n" " YAML ファイルからイメージプロパティをロードします" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." @@ -8089,12 +8451,6 @@ msgstr "yes" #~ msgid "[:]/ " #~ msgstr "[:]/" -#~ msgid "Creates a new cluster groups" -#~ msgstr "新たにクラスターグループを作成します" - -#~ msgid "Rename a cluster groups" -#~ msgstr "クラスタグループの名前を変更します" - #~ msgid "Manage network zone record entriess" #~ msgstr "ネットワークゾーンレコードエントリを管理します" @@ -10248,9 +10604,6 @@ msgstr "yes" #~ " L - インスタンスの場所 (例: its cluster member)\n" #~ " U - 現在のディスク使用量" -#~ msgid "Deletes a new cluster groups" -#~ msgstr "クラスタグループを削除します" - #~ msgid "Edits a cluster group" #~ msgstr "クラスタグループを編集します" diff --git a/po/ka.po b/po/ka.po index 94a367587d4a..15eac0ad26c5 100644 --- a/po/ka.po +++ b/po/ka.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -130,6 +130,68 @@ msgid "" "### Note that the name is shown but cannot be changed" msgstr "" +#: lxc/auth.go:213 +msgid "" +"### This is a YAML representation of the group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### A group has the following format:\n" +"### name: my-first-group\n" +"### description: My first group.\n" +"### permissions:\n" +"### - entity_type: project\n" +"### url: /1.0/projects/default\n" +"### entitlement: can_view\n" +"### identities:\n" +"### - authentication_method: oidc\n" +"### type: OIDC client\n" +"### identifier: jane.doe@example.com\n" +"### name: Jane Doe\n" +"### metadata:\n" +"### subject: auth0|123456789\n" +"### identity_provider_groups:\n" +"### - sales\n" +"### - operations\n" +"###\n" +"### Note that all group information is shown but only the description and " +"permissions can be modified" +msgstr "" + +#: lxc/auth.go:901 +msgid "" +"### This is a YAML representation of the group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### An identity has the following format:\n" +"### authentication_method: oidc\n" +"### type: OIDC client\n" +"### identifier: jane.doe@example.com\n" +"### name: Jane Doe\n" +"### metadata:\n" +"### subject: auth0|123456789\n" +"### projects:\n" +"### - default\n" +"### groups:\n" +"### - my-first-group\n" +"###\n" +"### Note that all identity information is shown but only the projects and " +"groups can be modified" +msgstr "" + +#: lxc/auth.go:1516 +msgid "" +"### This is a YAML representation of the identity provider group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### An identity provider group has the following format:\n" +"### name: operations\n" +"### groups:\n" +"### - foo\n" +"### - bar\n" +"###\n" +"### Note that the name is shown but cannot be modified" +msgstr "" + #: lxc/image.go:378 msgid "" "### This is a YAML representation of the image properties.\n" @@ -495,6 +557,10 @@ msgstr "" msgid "AUTH TYPE" msgstr "" +#: lxc/auth.go:807 +msgid "AUTHENTICATION METHOD" +msgstr "" + #: lxc/remote.go:99 msgid "Accept certificate" msgstr "" @@ -529,6 +595,14 @@ msgstr "" msgid "Add a cluster member to a cluster group" msgstr "" +#: lxc/auth.go:1039 lxc/auth.go:1040 +msgid "Add a group to an identity" +msgstr "" + +#: lxc/auth.go:1805 lxc/auth.go:1806 +msgid "Add a group to an identity provider group" +msgstr "" + #: lxc/network_zone.go:1239 msgid "Add a network zone record entry" msgstr "" @@ -594,6 +668,10 @@ msgid "" "restricted to one or more projects.\n" msgstr "" +#: lxc/auth.go:515 lxc/auth.go:516 +msgid "Add permissions to groups" +msgstr "" + #: lxc/network_forward.go:744 lxc/network_forward.go:745 msgid "Add ports to a forward" msgstr "" @@ -1269,6 +1347,16 @@ msgstr "" msgid "Could not find certificate key file path: %s" msgstr "" +#: lxc/auth.go:300 lxc/auth.go:1591 +#, c-format +msgid "Could not parse group: %s" +msgstr "" + +#: lxc/auth.go:987 +#, c-format +msgid "Could not parse identity: %s" +msgstr "" + #: lxc/cluster.go:1113 #, c-format msgid "Could not read certificate file: %s with error: %v" @@ -1317,6 +1405,14 @@ msgstr "" msgid "Create any directories necessary" msgstr "" +#: lxc/auth.go:97 lxc/auth.go:98 +msgid "Create groups" +msgstr "" + +#: lxc/auth.go:1402 lxc/auth.go:1403 +msgid "Create identity provider groups" +msgstr "" + #: lxc/snapshot.go:27 msgid "Create instance snapshots" msgstr "" @@ -1416,8 +1512,8 @@ msgstr "" msgid "DEFAULT TARGET ADDRESS" msgstr "" -#: lxc/cluster.go:188 lxc/cluster_group.go:438 lxc/image.go:1058 -#: lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 +#: lxc/auth.go:376 lxc/cluster.go:188 lxc/cluster_group.go:438 +#: lxc/image.go:1058 lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 #: lxc/network_acl.go:148 lxc/network_forward.go:145 #: lxc/network_load_balancer.go:148 lxc/network_peer.go:140 #: lxc/network_zone.go:139 lxc/network_zone.go:742 lxc/operation.go:172 @@ -1463,6 +1559,14 @@ msgstr "" msgid "Delete files in instances" msgstr "" +#: lxc/auth.go:151 lxc/auth.go:152 +msgid "Delete groups" +msgstr "" + +#: lxc/auth.go:1454 lxc/auth.go:1455 +msgid "Delete identity provider groups" +msgstr "" + #: lxc/image_alias.go:106 lxc/image_alias.go:107 msgid "Delete image aliases" msgstr "" @@ -1537,18 +1641,26 @@ msgstr "" #: lxc/action.go:32 lxc/action.go:53 lxc/action.go:75 lxc/action.go:98 #: lxc/alias.go:23 lxc/alias.go:60 lxc/alias.go:110 lxc/alias.go:159 -#: lxc/alias.go:214 lxc/cluster.go:29 lxc/cluster.go:122 lxc/cluster.go:206 -#: lxc/cluster.go:255 lxc/cluster.go:306 lxc/cluster.go:367 lxc/cluster.go:439 -#: lxc/cluster.go:471 lxc/cluster.go:521 lxc/cluster.go:604 lxc/cluster.go:689 -#: lxc/cluster.go:804 lxc/cluster.go:880 lxc/cluster.go:982 lxc/cluster.go:1061 -#: lxc/cluster.go:1168 lxc/cluster.go:1190 lxc/cluster_group.go:30 -#: lxc/cluster_group.go:84 lxc/cluster_group.go:157 lxc/cluster_group.go:214 -#: lxc/cluster_group.go:266 lxc/cluster_group.go:382 lxc/cluster_group.go:456 -#: lxc/cluster_group.go:529 lxc/cluster_group.go:577 lxc/cluster_group.go:631 -#: lxc/cluster_role.go:23 lxc/cluster_role.go:50 lxc/cluster_role.go:106 -#: lxc/config.go:32 lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 -#: lxc/config.go:734 lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 -#: lxc/config.go:988 lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 +#: lxc/alias.go:214 lxc/auth.go:30 lxc/auth.go:59 lxc/auth.go:98 +#: lxc/auth.go:152 lxc/auth.go:201 lxc/auth.go:332 lxc/auth.go:392 +#: lxc/auth.go:441 lxc/auth.go:493 lxc/auth.go:516 lxc/auth.go:575 +#: lxc/auth.go:731 lxc/auth.go:759 lxc/auth.go:826 lxc/auth.go:889 +#: lxc/auth.go:1017 lxc/auth.go:1040 lxc/auth.go:1098 lxc/auth.go:1167 +#: lxc/auth.go:1189 lxc/auth.go:1365 lxc/auth.go:1403 lxc/auth.go:1455 +#: lxc/auth.go:1504 lxc/auth.go:1623 lxc/auth.go:1683 lxc/auth.go:1732 +#: lxc/auth.go:1783 lxc/auth.go:1806 lxc/auth.go:1859 lxc/cluster.go:29 +#: lxc/cluster.go:122 lxc/cluster.go:206 lxc/cluster.go:255 lxc/cluster.go:306 +#: lxc/cluster.go:367 lxc/cluster.go:439 lxc/cluster.go:471 lxc/cluster.go:521 +#: lxc/cluster.go:604 lxc/cluster.go:689 lxc/cluster.go:804 lxc/cluster.go:880 +#: lxc/cluster.go:982 lxc/cluster.go:1061 lxc/cluster.go:1168 +#: lxc/cluster.go:1190 lxc/cluster_group.go:30 lxc/cluster_group.go:84 +#: lxc/cluster_group.go:157 lxc/cluster_group.go:214 lxc/cluster_group.go:266 +#: lxc/cluster_group.go:382 lxc/cluster_group.go:456 lxc/cluster_group.go:529 +#: lxc/cluster_group.go:577 lxc/cluster_group.go:631 lxc/cluster_role.go:23 +#: lxc/cluster_role.go:50 lxc/cluster_role.go:106 lxc/config.go:32 +#: lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 lxc/config.go:734 +#: lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 lxc/config.go:988 +#: lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 #: lxc/config_device.go:24 lxc/config_device.go:78 lxc/config_device.go:208 #: lxc/config_device.go:285 lxc/config_device.go:356 lxc/config_device.go:450 #: lxc/config_device.go:548 lxc/config_device.go:555 lxc/config_device.go:668 @@ -1566,7 +1678,7 @@ msgstr "" #: lxc/image.go:651 lxc/image.go:884 lxc/image.go:1019 lxc/image.go:1338 #: lxc/image.go:1418 lxc/image.go:1477 lxc/image.go:1529 lxc/image.go:1585 #: lxc/image_alias.go:24 lxc/image_alias.go:60 lxc/image_alias.go:107 -#: lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:28 +#: lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:29 #: lxc/info.go:33 lxc/init.go:42 lxc/launch.go:24 lxc/list.go:49 lxc/main.go:82 #: lxc/manpage.go:22 lxc/monitor.go:33 lxc/move.go:37 lxc/network.go:32 #: lxc/network.go:135 lxc/network.go:220 lxc/network.go:293 lxc/network.go:372 @@ -1797,6 +1909,10 @@ msgstr "" msgid "Edit a cluster group" msgstr "" +#: lxc/auth.go:888 lxc/auth.go:889 +msgid "Edit an identity as YAML" +msgstr "" + #: lxc/cluster.go:688 lxc/cluster.go:689 msgid "Edit cluster member configurations as YAML" msgstr "" @@ -1805,6 +1921,14 @@ msgstr "" msgid "Edit files in instances" msgstr "" +#: lxc/auth.go:200 lxc/auth.go:201 +msgid "Edit groups as YAML" +msgstr "" + +#: lxc/auth.go:1503 lxc/auth.go:1504 +msgid "Edit identity provider groups as YAML" +msgstr "" + #: lxc/image.go:362 lxc/image.go:363 msgid "Edit image properties" msgstr "" @@ -2272,15 +2396,16 @@ msgid "" "Are you really sure you want to force removing %s? (yes/no): " msgstr "" -#: lxc/alias.go:112 lxc/cluster.go:124 lxc/cluster.go:881 -#: lxc/cluster_group.go:384 lxc/config_template.go:242 lxc/config_trust.go:352 -#: lxc/config_trust.go:434 lxc/image.go:1045 lxc/image_alias.go:157 -#: lxc/list.go:133 lxc/network.go:916 lxc/network.go:1007 lxc/network_acl.go:97 -#: lxc/network_allocations.go:57 lxc/network_forward.go:89 -#: lxc/network_load_balancer.go:93 lxc/network_peer.go:84 -#: lxc/network_zone.go:88 lxc/network_zone.go:692 lxc/operation.go:108 -#: lxc/profile.go:617 lxc/project.go:412 lxc/project.go:804 lxc/remote.go:666 -#: lxc/storage.go:588 lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 +#: lxc/alias.go:112 lxc/auth.go:336 lxc/auth.go:763 lxc/auth.go:1627 +#: lxc/cluster.go:124 lxc/cluster.go:881 lxc/cluster_group.go:384 +#: lxc/config_template.go:242 lxc/config_trust.go:352 lxc/config_trust.go:434 +#: lxc/image.go:1045 lxc/image_alias.go:157 lxc/list.go:133 lxc/network.go:916 +#: lxc/network.go:1007 lxc/network_acl.go:97 lxc/network_allocations.go:57 +#: lxc/network_forward.go:89 lxc/network_load_balancer.go:93 +#: lxc/network_peer.go:84 lxc/network_zone.go:88 lxc/network_zone.go:692 +#: lxc/operation.go:108 lxc/profile.go:617 lxc/project.go:412 +#: lxc/project.go:804 lxc/remote.go:666 lxc/storage.go:588 +#: lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 #: lxc/storage_volume.go:1422 lxc/warning.go:93 msgid "Format (csv|json|table|yaml|compact)" msgstr "" @@ -2329,6 +2454,10 @@ msgstr "" msgid "GPUs:" msgstr "" +#: lxc/auth.go:811 lxc/auth.go:1667 +msgid "GROUPS" +msgstr "" + #: lxc/manpage.go:21 lxc/manpage.go:22 msgid "Generate manpages for all commands" msgstr "" @@ -2474,6 +2603,21 @@ msgstr "" msgid "Given target %q does not match source volume location %q" msgstr "" +#: lxc/auth.go:136 +#, c-format +msgid "Group %s created" +msgstr "" + +#: lxc/auth.go:186 lxc/auth.go:1489 +#, c-format +msgid "Group %s deleted" +msgstr "" + +#: lxc/auth.go:426 lxc/auth.go:1717 +#, c-format +msgid "Group %s renamed to %s" +msgstr "" + #: lxc/exec.go:60 msgid "Group ID to run the command as (default 0)" msgstr "" @@ -2528,6 +2672,10 @@ msgstr "" msgid "ID: %s" msgstr "" +#: lxc/auth.go:810 +msgid "IDENTIFIER" +msgstr "" + #: lxc/project.go:499 msgid "IMAGES" msgstr "" @@ -2556,6 +2704,11 @@ msgstr "" msgid "ISSUE DATE" msgstr "" +#: lxc/auth.go:1439 +#, c-format +msgid "Identity provider group %s created" +msgstr "" + #: lxc/rebuild.go:32 msgid "If an instance is running, stop it and then rebuild it" msgstr "" @@ -2568,7 +2721,7 @@ msgstr "" msgid "If the snapshot name already exists, delete and create a new one" msgstr "" -#: lxc/main.go:395 +#: lxc/main.go:398 msgid "" "If this is your first time running LXD on this machine, you should also run: " "lxd init" @@ -2632,7 +2785,7 @@ msgstr "" msgid "Import backups of custom volumes including their snapshots." msgstr "" -#: lxc/import.go:28 +#: lxc/import.go:29 msgid "Import backups of instances including their snapshots." msgstr "" @@ -2651,7 +2804,7 @@ msgstr "" msgid "Import images into the image store" msgstr "" -#: lxc/import.go:27 +#: lxc/import.go:28 msgid "Import instance backups" msgstr "" @@ -2664,7 +2817,7 @@ msgstr "" msgid "Importing custom volume: %s" msgstr "" -#: lxc/import.go:94 +#: lxc/import.go:96 #, c-format msgid "Importing instance: %s" msgstr "" @@ -2677,6 +2830,10 @@ msgstr "" msgid "Input data" msgstr "" +#: lxc/auth.go:1166 lxc/auth.go:1167 +msgid "Inspect permissions" +msgstr "" + #: lxc/info.go:684 msgid "Instance Only" msgstr "" @@ -2793,7 +2950,7 @@ msgstr "" msgid "Invalid new snapshot name, parent volume must be the same as source" msgstr "" -#: lxc/main.go:495 +#: lxc/main.go:498 msgid "Invalid number of arguments" msgstr "" @@ -2963,6 +3120,18 @@ msgstr "" msgid "List background operations" msgstr "" +#: lxc/auth.go:331 lxc/auth.go:332 +msgid "List groups" +msgstr "" + +#: lxc/auth.go:758 lxc/auth.go:759 +msgid "List identities" +msgstr "" + +#: lxc/auth.go:1622 lxc/auth.go:1623 +msgid "List identity provider groups" +msgstr "" + #: lxc/image_alias.go:151 msgid "List image aliases" msgstr "" @@ -3112,6 +3281,10 @@ msgstr "" msgid "List operations from all projects" msgstr "" +#: lxc/auth.go:1188 lxc/auth.go:1189 +msgid "List permissions" +msgstr "" + #: lxc/profile.go:612 lxc/profile.go:613 msgid "List profiles" msgstr "" @@ -3303,6 +3476,22 @@ msgstr "" msgid "Manage files in instances" msgstr "" +#: lxc/auth.go:58 lxc/auth.go:59 lxc/auth.go:1364 lxc/auth.go:1365 +msgid "Manage groups" +msgstr "" + +#: lxc/auth.go:1016 lxc/auth.go:1017 +msgid "Manage groups for the identity" +msgstr "" + +#: lxc/auth.go:730 lxc/auth.go:731 +msgid "Manage identities" +msgstr "" + +#: lxc/auth.go:1782 lxc/auth.go:1783 +msgid "Manage identity provider group mappings" +msgstr "" + #: lxc/image_alias.go:23 lxc/image_alias.go:24 msgid "Manage image aliases" msgstr "" @@ -3390,6 +3579,10 @@ msgstr "" msgid "Manage network zones" msgstr "" +#: lxc/auth.go:492 lxc/auth.go:493 +msgid "Manage permissions" +msgstr "" + #: lxc/profile.go:28 lxc/profile.go:29 msgid "Manage profiles" msgstr "" @@ -3438,6 +3631,10 @@ msgstr "" msgid "Manage trusted clients" msgstr "" +#: lxc/auth.go:29 lxc/auth.go:30 +msgid "Manage user authorization" +msgstr "" + #: lxc/warning.go:28 lxc/warning.go:29 msgid "Manage warnings" msgstr "" @@ -3531,6 +3728,23 @@ msgstr "" msgid "Missing cluster member name" msgstr "" +#: lxc/auth.go:122 lxc/auth.go:176 lxc/auth.go:254 lxc/auth.go:416 +#: lxc/auth.go:465 lxc/auth.go:540 lxc/auth.go:599 lxc/auth.go:1756 +msgid "Missing group name" +msgstr "" + +#: lxc/auth.go:856 lxc/auth.go:936 lxc/auth.go:1064 lxc/auth.go:1122 +msgid "Missing identity argument" +msgstr "" + +#: lxc/auth.go:1426 lxc/auth.go:1479 lxc/auth.go:1545 lxc/auth.go:1707 +msgid "Missing identity provider group name" +msgstr "" + +#: lxc/auth.go:1830 lxc/auth.go:1883 +msgid "Missing identity provider group name argument" +msgstr "" + #: lxc/config_metadata.go:103 lxc/config_metadata.go:204 #: lxc/config_template.go:91 lxc/config_template.go:134 #: lxc/config_template.go:176 lxc/config_template.go:265 @@ -3749,13 +3963,13 @@ msgstr "" msgid "Must supply instance name for: " msgstr "" -#: lxc/cluster.go:183 lxc/cluster.go:964 lxc/cluster_group.go:437 -#: lxc/config_trust.go:409 lxc/config_trust.go:514 lxc/list.go:565 -#: lxc/network.go:980 lxc/network_acl.go:147 lxc/network_peer.go:139 -#: lxc/network_zone.go:138 lxc/network_zone.go:741 lxc/profile.go:657 -#: lxc/project.go:498 lxc/remote.go:724 lxc/storage.go:638 -#: lxc/storage_bucket.go:506 lxc/storage_bucket.go:826 -#: lxc/storage_volume.go:1510 +#: lxc/auth.go:375 lxc/auth.go:809 lxc/auth.go:1666 lxc/cluster.go:183 +#: lxc/cluster.go:964 lxc/cluster_group.go:437 lxc/config_trust.go:409 +#: lxc/config_trust.go:514 lxc/list.go:565 lxc/network.go:980 +#: lxc/network_acl.go:147 lxc/network_peer.go:139 lxc/network_zone.go:138 +#: lxc/network_zone.go:741 lxc/profile.go:657 lxc/project.go:498 +#: lxc/remote.go:724 lxc/storage.go:638 lxc/storage_bucket.go:506 +#: lxc/storage_bucket.go:826 lxc/storage_volume.go:1510 msgid "NAME" msgstr "" @@ -3938,7 +4152,7 @@ msgstr "" msgid "New aliases to add to the image" msgstr "" -#: lxc/copy.go:54 lxc/init.go:51 lxc/move.go:59 +#: lxc/copy.go:54 lxc/import.go:37 lxc/init.go:51 lxc/move.go:59 msgid "New key/value to apply to a specific device" msgstr "" @@ -4097,7 +4311,7 @@ msgstr "" msgid "Partitions:" msgstr "" -#: lxc/main.go:358 +#: lxc/main.go:361 #, c-format msgid "Password for %s: " msgstr "" @@ -4139,10 +4353,11 @@ msgstr "" msgid "Press ctrl+c to finish" msgstr "" -#: lxc/cluster.go:771 lxc/cluster_group.go:340 lxc/config.go:273 -#: lxc/config.go:348 lxc/config.go:1278 lxc/config_metadata.go:148 -#: lxc/config_template.go:206 lxc/config_trust.go:315 lxc/image.go:457 -#: lxc/network.go:687 lxc/network_acl.go:621 lxc/network_forward.go:636 +#: lxc/auth.go:301 lxc/auth.go:988 lxc/auth.go:1592 lxc/cluster.go:771 +#: lxc/cluster_group.go:340 lxc/config.go:273 lxc/config.go:348 +#: lxc/config.go:1278 lxc/config_metadata.go:148 lxc/config_template.go:206 +#: lxc/config_trust.go:315 lxc/image.go:457 lxc/network.go:687 +#: lxc/network_acl.go:621 lxc/network_forward.go:636 #: lxc/network_load_balancer.go:638 lxc/network_peer.go:611 #: lxc/network_zone.go:552 lxc/network_zone.go:1144 lxc/profile.go:519 #: lxc/project.go:316 lxc/storage.go:311 lxc/storage_bucket.go:344 @@ -4489,6 +4704,10 @@ msgstr "" msgid "Remove a cluster member from a cluster group" msgstr "" +#: lxc/auth.go:1097 lxc/auth.go:1098 +msgid "Remove a group from an identity" +msgstr "" + #: lxc/cluster.go:520 lxc/cluster.go:521 msgid "Remove a member from the cluster" msgstr "" @@ -4521,6 +4740,10 @@ msgstr "" msgid "Remove entries from a network zone record" msgstr "" +#: lxc/auth.go:1858 lxc/auth.go:1859 +msgid "Remove identities from groups" +msgstr "" + #: lxc/config_device.go:449 lxc/config_device.go:450 msgid "Remove instance devices" msgstr "" @@ -4529,6 +4752,10 @@ msgstr "" msgid "Remove member from group" msgstr "" +#: lxc/auth.go:574 lxc/auth.go:575 +msgid "Remove permissions from groups" +msgstr "" + #: lxc/network_forward.go:809 lxc/network_forward.go:810 msgid "Remove ports from a forward" msgstr "" @@ -4570,6 +4797,14 @@ msgstr "" msgid "Rename aliases" msgstr "" +#: lxc/auth.go:391 lxc/auth.go:392 +msgid "Rename groups" +msgstr "" + +#: lxc/auth.go:1682 lxc/auth.go:1683 +msgid "Rename identity provider groups" +msgstr "" + #: lxc/rename.go:20 lxc/rename.go:21 msgid "Rename instances and snapshots" msgstr "" @@ -5075,6 +5310,10 @@ msgstr "" msgid "Show all information messages" msgstr "" +#: lxc/auth.go:1731 lxc/auth.go:1732 +msgid "Show an identity provider group" +msgstr "" + #: lxc/cluster_group.go:576 lxc/cluster_group.go:577 msgid "Show cluster group configurations" msgstr "" @@ -5099,6 +5338,23 @@ msgstr "" msgid "Show full device configuration" msgstr "" +#: lxc/auth.go:440 lxc/auth.go:441 +msgid "Show group configurations" +msgstr "" + +#: lxc/auth.go:826 +msgid "" +"Show identity configurations\n" +"\n" +"The argument must be a concatenation of the authentication method and either " +"the \n" +"name or identifier of the identity, delimited by a forward slash. This " +"command\n" +"will fail if an identity name is used that is not unique within the " +"authentication\n" +"method. Use the identifier instead if this occurs.\n" +msgstr "" + #: lxc/image.go:1417 lxc/image.go:1418 msgid "Show image properties" msgstr "" @@ -5119,7 +5375,7 @@ msgstr "" msgid "Show instance or server information" msgstr "" -#: lxc/main.go:268 lxc/main.go:269 +#: lxc/main.go:271 lxc/main.go:272 msgid "Show less common commands" msgstr "" @@ -5350,7 +5606,7 @@ msgstr "" msgid "Storage pool %s pending on member %s" msgstr "" -#: lxc/copy.go:60 lxc/import.go:35 lxc/init.go:54 lxc/move.go:65 +#: lxc/copy.go:60 lxc/import.go:36 lxc/init.go:54 lxc/move.go:65 msgid "Storage pool name" msgstr "" @@ -5415,9 +5671,9 @@ msgstr "" msgid "TOKEN" msgstr "" -#: lxc/config_trust.go:408 lxc/image.go:1062 lxc/image_alias.go:236 -#: lxc/list.go:571 lxc/network.go:981 lxc/network.go:1055 -#: lxc/network_allocations.go:26 lxc/operation.go:171 +#: lxc/auth.go:808 lxc/config_trust.go:408 lxc/image.go:1062 +#: lxc/image_alias.go:236 lxc/list.go:571 lxc/network.go:981 +#: lxc/network.go:1055 lxc/network_allocations.go:26 lxc/operation.go:171 #: lxc/storage_volume.go:1509 lxc/warning.go:215 msgid "TYPE" msgstr "" @@ -5617,7 +5873,7 @@ msgstr "" msgid "This LXD server is not available on the network" msgstr "" -#: lxc/main.go:290 +#: lxc/main.go:293 msgid "" "This client hasn't been configured to use a remote LXD server yet.\n" "As your platform can't run native Linux instances, you must connect to a " @@ -5653,7 +5909,7 @@ msgstr "" msgid "To detach from the console, press: +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6055,6 +6311,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6122,15 +6382,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6154,6 +6415,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6211,6 +6476,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6219,15 +6492,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6758,6 +7054,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6851,7 +7168,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/ko.po b/po/ko.po index 28e220f6b040..b3f2d202e500 100644 --- a/po/ko.po +++ b/po/ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:09+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Korean +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/lxd.pot b/po/lxd.pot index d8627369e05f..f06a5ca5588d 100644 --- a/po/lxd.pot +++ b/po/lxd.pot @@ -7,7 +7,7 @@ msgid "" msgstr "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" - "POT-Creation-Date: 2024-02-20 10:14+0100\n" + "POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -122,6 +122,63 @@ msgid "### This is a YAML representation of the configuration.\n" "### Note that the name is shown but cannot be changed" msgstr "" +#: lxc/auth.go:213 +msgid "### This is a YAML representation of the group.\n" + "### Any line starting with a '# will be ignored.\n" + "###\n" + "### A group has the following format:\n" + "### name: my-first-group\n" + "### description: My first group.\n" + "### permissions:\n" + "### - entity_type: project\n" + "### url: /1.0/projects/default\n" + "### entitlement: can_view\n" + "### identities:\n" + "### - authentication_method: oidc\n" + "### type: OIDC client\n" + "### identifier: jane.doe@example.com\n" + "### name: Jane Doe\n" + "### metadata:\n" + "### subject: auth0|123456789\n" + "### identity_provider_groups:\n" + "### - sales\n" + "### - operations\n" + "###\n" + "### Note that all group information is shown but only the description and permissions can be modified" +msgstr "" + +#: lxc/auth.go:901 +msgid "### This is a YAML representation of the group.\n" + "### Any line starting with a '# will be ignored.\n" + "###\n" + "### An identity has the following format:\n" + "### authentication_method: oidc\n" + "### type: OIDC client\n" + "### identifier: jane.doe@example.com\n" + "### name: Jane Doe\n" + "### metadata:\n" + "### subject: auth0|123456789\n" + "### projects:\n" + "### - default\n" + "### groups:\n" + "### - my-first-group\n" + "###\n" + "### Note that all identity information is shown but only the projects and groups can be modified" +msgstr "" + +#: lxc/auth.go:1516 +msgid "### This is a YAML representation of the identity provider group.\n" + "### Any line starting with a '# will be ignored.\n" + "###\n" + "### An identity provider group has the following format:\n" + "### name: operations\n" + "### groups:\n" + "### - foo\n" + "### - bar\n" + "###\n" + "### Note that the name is shown but cannot be modified" +msgstr "" + #: lxc/image.go:378 msgid "### This is a YAML representation of the image properties.\n" "### Any line starting with a '# will be ignored.\n" @@ -469,6 +526,10 @@ msgstr "" msgid "AUTH TYPE" msgstr "" +#: lxc/auth.go:807 +msgid "AUTHENTICATION METHOD" +msgstr "" + #: lxc/remote.go:99 msgid "Accept certificate" msgstr "" @@ -503,6 +564,14 @@ msgstr "" msgid "Add a cluster member to a cluster group" msgstr "" +#: lxc/auth.go:1039 lxc/auth.go:1040 +msgid "Add a group to an identity" +msgstr "" + +#: lxc/auth.go:1805 lxc/auth.go:1806 +msgid "Add a group to an identity provider group" +msgstr "" + #: lxc/network_zone.go:1239 msgid "Add a network zone record entry" msgstr "" @@ -561,6 +630,10 @@ msgid "Add new trusted client\n" "restricted to one or more projects.\n" msgstr "" +#: lxc/auth.go:515 lxc/auth.go:516 +msgid "Add permissions to groups" +msgstr "" + #: lxc/network_forward.go:744 lxc/network_forward.go:745 msgid "Add ports to a forward" msgstr "" @@ -1187,6 +1260,16 @@ msgstr "" msgid "Could not find certificate key file path: %s" msgstr "" +#: lxc/auth.go:300 lxc/auth.go:1591 +#, c-format +msgid "Could not parse group: %s" +msgstr "" + +#: lxc/auth.go:987 +#, c-format +msgid "Could not parse identity: %s" +msgstr "" + #: lxc/cluster.go:1113 #, c-format msgid "Could not read certificate file: %s with error: %v" @@ -1235,6 +1318,14 @@ msgstr "" msgid "Create any directories necessary" msgstr "" +#: lxc/auth.go:97 lxc/auth.go:98 +msgid "Create groups" +msgstr "" + +#: lxc/auth.go:1402 lxc/auth.go:1403 +msgid "Create identity provider groups" +msgstr "" + #: lxc/snapshot.go:27 msgid "Create instance snapshots" msgstr "" @@ -1333,7 +1424,7 @@ msgstr "" msgid "DEFAULT TARGET ADDRESS" msgstr "" -#: lxc/cluster.go:188 lxc/cluster_group.go:438 lxc/image.go:1058 lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 lxc/network_acl.go:148 lxc/network_forward.go:145 lxc/network_load_balancer.go:148 lxc/network_peer.go:140 lxc/network_zone.go:139 lxc/network_zone.go:742 lxc/operation.go:172 lxc/profile.go:658 lxc/project.go:505 lxc/storage.go:646 lxc/storage_bucket.go:507 lxc/storage_bucket.go:827 lxc/storage_volume.go:1511 +#: lxc/auth.go:376 lxc/cluster.go:188 lxc/cluster_group.go:438 lxc/image.go:1058 lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 lxc/network_acl.go:148 lxc/network_forward.go:145 lxc/network_load_balancer.go:148 lxc/network_peer.go:140 lxc/network_zone.go:139 lxc/network_zone.go:742 lxc/operation.go:172 lxc/profile.go:658 lxc/project.go:505 lxc/storage.go:646 lxc/storage_bucket.go:507 lxc/storage_bucket.go:827 lxc/storage_volume.go:1511 msgid "DESCRIPTION" msgstr "" @@ -1373,6 +1464,14 @@ msgstr "" msgid "Delete files in instances" msgstr "" +#: lxc/auth.go:151 lxc/auth.go:152 +msgid "Delete groups" +msgstr "" + +#: lxc/auth.go:1454 lxc/auth.go:1455 +msgid "Delete identity provider groups" +msgstr "" + #: lxc/image_alias.go:106 lxc/image_alias.go:107 msgid "Delete image aliases" msgstr "" @@ -1445,7 +1544,7 @@ msgstr "" msgid "Delete warning" msgstr "" -#: lxc/action.go:32 lxc/action.go:53 lxc/action.go:75 lxc/action.go:98 lxc/alias.go:23 lxc/alias.go:60 lxc/alias.go:110 lxc/alias.go:159 lxc/alias.go:214 lxc/cluster.go:29 lxc/cluster.go:122 lxc/cluster.go:206 lxc/cluster.go:255 lxc/cluster.go:306 lxc/cluster.go:367 lxc/cluster.go:439 lxc/cluster.go:471 lxc/cluster.go:521 lxc/cluster.go:604 lxc/cluster.go:689 lxc/cluster.go:804 lxc/cluster.go:880 lxc/cluster.go:982 lxc/cluster.go:1061 lxc/cluster.go:1168 lxc/cluster.go:1190 lxc/cluster_group.go:30 lxc/cluster_group.go:84 lxc/cluster_group.go:157 lxc/cluster_group.go:214 lxc/cluster_group.go:266 lxc/cluster_group.go:382 lxc/cluster_group.go:456 lxc/cluster_group.go:529 lxc/cluster_group.go:577 lxc/cluster_group.go:631 lxc/cluster_role.go:23 lxc/cluster_role.go:50 lxc/cluster_role.go:106 lxc/config.go:32 lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 lxc/config.go:734 lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 lxc/config.go:988 lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 lxc/config_device.go:24 lxc/config_device.go:78 lxc/config_device.go:208 lxc/config_device.go:285 lxc/config_device.go:356 lxc/config_device.go:450 lxc/config_device.go:548 lxc/config_device.go:555 lxc/config_device.go:668 lxc/config_device.go:741 lxc/config_metadata.go:27 lxc/config_metadata.go:55 lxc/config_metadata.go:180 lxc/config_template.go:27 lxc/config_template.go:67 lxc/config_template.go:110 lxc/config_template.go:152 lxc/config_template.go:240 lxc/config_template.go:300 lxc/config_trust.go:34 lxc/config_trust.go:87 lxc/config_trust.go:236 lxc/config_trust.go:350 lxc/config_trust.go:432 lxc/config_trust.go:534 lxc/config_trust.go:580 lxc/config_trust.go:651 lxc/console.go:36 lxc/copy.go:41 lxc/delete.go:31 lxc/exec.go:41 lxc/export.go:32 lxc/file.go:78 lxc/file.go:118 lxc/file.go:167 lxc/file.go:237 lxc/file.go:459 lxc/file.go:967 lxc/image.go:37 lxc/image.go:145 lxc/image.go:312 lxc/image.go:363 lxc/image.go:490 lxc/image.go:651 lxc/image.go:884 lxc/image.go:1019 lxc/image.go:1338 lxc/image.go:1418 lxc/image.go:1477 lxc/image.go:1529 lxc/image.go:1585 lxc/image_alias.go:24 lxc/image_alias.go:60 lxc/image_alias.go:107 lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:28 lxc/info.go:33 lxc/init.go:42 lxc/launch.go:24 lxc/list.go:49 lxc/main.go:82 lxc/manpage.go:22 lxc/monitor.go:33 lxc/move.go:37 lxc/network.go:32 lxc/network.go:135 lxc/network.go:220 lxc/network.go:293 lxc/network.go:372 lxc/network.go:422 lxc/network.go:507 lxc/network.go:592 lxc/network.go:720 lxc/network.go:789 lxc/network.go:912 lxc/network.go:1005 lxc/network.go:1076 lxc/network.go:1128 lxc/network.go:1216 lxc/network.go:1280 lxc/network_acl.go:29 lxc/network_acl.go:94 lxc/network_acl.go:165 lxc/network_acl.go:218 lxc/network_acl.go:266 lxc/network_acl.go:327 lxc/network_acl.go:412 lxc/network_acl.go:492 lxc/network_acl.go:522 lxc/network_acl.go:653 lxc/network_acl.go:702 lxc/network_acl.go:751 lxc/network_acl.go:766 lxc/network_acl.go:887 lxc/network_allocations.go:51 lxc/network_forward.go:29 lxc/network_forward.go:86 lxc/network_forward.go:167 lxc/network_forward.go:231 lxc/network_forward.go:329 lxc/network_forward.go:398 lxc/network_forward.go:496 lxc/network_forward.go:526 lxc/network_forward.go:668 lxc/network_forward.go:730 lxc/network_forward.go:745 lxc/network_forward.go:810 lxc/network_load_balancer.go:29 lxc/network_load_balancer.go:90 lxc/network_load_balancer.go:169 lxc/network_load_balancer.go:233 lxc/network_load_balancer.go:331 lxc/network_load_balancer.go:399 lxc/network_load_balancer.go:497 lxc/network_load_balancer.go:527 lxc/network_load_balancer.go:670 lxc/network_load_balancer.go:731 lxc/network_load_balancer.go:746 lxc/network_load_balancer.go:810 lxc/network_load_balancer.go:896 lxc/network_load_balancer.go:911 lxc/network_load_balancer.go:972 lxc/network_peer.go:28 lxc/network_peer.go:81 lxc/network_peer.go:158 lxc/network_peer.go:215 lxc/network_peer.go:331 lxc/network_peer.go:399 lxc/network_peer.go:488 lxc/network_peer.go:518 lxc/network_peer.go:643 lxc/network_zone.go:28 lxc/network_zone.go:85 lxc/network_zone.go:156 lxc/network_zone.go:211 lxc/network_zone.go:271 lxc/network_zone.go:354 lxc/network_zone.go:434 lxc/network_zone.go:465 lxc/network_zone.go:584 lxc/network_zone.go:632 lxc/network_zone.go:689 lxc/network_zone.go:759 lxc/network_zone.go:811 lxc/network_zone.go:870 lxc/network_zone.go:952 lxc/network_zone.go:1028 lxc/network_zone.go:1058 lxc/network_zone.go:1176 lxc/network_zone.go:1225 lxc/network_zone.go:1240 lxc/network_zone.go:1286 lxc/operation.go:24 lxc/operation.go:56 lxc/operation.go:106 lxc/operation.go:193 lxc/profile.go:29 lxc/profile.go:104 lxc/profile.go:167 lxc/profile.go:250 lxc/profile.go:320 lxc/profile.go:374 lxc/profile.go:424 lxc/profile.go:552 lxc/profile.go:613 lxc/profile.go:674 lxc/profile.go:750 lxc/profile.go:802 lxc/profile.go:878 lxc/profile.go:934 lxc/project.go:29 lxc/project.go:93 lxc/project.go:158 lxc/project.go:221 lxc/project.go:349 lxc/project.go:410 lxc/project.go:523 lxc/project.go:580 lxc/project.go:659 lxc/project.go:690 lxc/project.go:743 lxc/project.go:802 lxc/publish.go:33 lxc/query.go:34 lxc/rebuild.go:27 lxc/remote.go:34 lxc/remote.go:89 lxc/remote.go:624 lxc/remote.go:662 lxc/remote.go:748 lxc/remote.go:821 lxc/remote.go:877 lxc/remote.go:917 lxc/rename.go:21 lxc/restore.go:24 lxc/snapshot.go:28 lxc/storage.go:33 lxc/storage.go:96 lxc/storage.go:170 lxc/storage.go:220 lxc/storage.go:344 lxc/storage.go:414 lxc/storage.go:586 lxc/storage.go:665 lxc/storage.go:761 lxc/storage.go:847 lxc/storage_bucket.go:29 lxc/storage_bucket.go:83 lxc/storage_bucket.go:183 lxc/storage_bucket.go:244 lxc/storage_bucket.go:377 lxc/storage_bucket.go:453 lxc/storage_bucket.go:530 lxc/storage_bucket.go:624 lxc/storage_bucket.go:693 lxc/storage_bucket.go:727 lxc/storage_bucket.go:768 lxc/storage_bucket.go:847 lxc/storage_bucket.go:925 lxc/storage_bucket.go:989 lxc/storage_bucket.go:1124 lxc/storage_volume.go:43 lxc/storage_volume.go:165 lxc/storage_volume.go:240 lxc/storage_volume.go:331 lxc/storage_volume.go:534 lxc/storage_volume.go:613 lxc/storage_volume.go:688 lxc/storage_volume.go:770 lxc/storage_volume.go:851 lxc/storage_volume.go:1060 lxc/storage_volume.go:1175 lxc/storage_volume.go:1322 lxc/storage_volume.go:1406 lxc/storage_volume.go:1613 lxc/storage_volume.go:1694 lxc/storage_volume.go:1809 lxc/storage_volume.go:1953 lxc/storage_volume.go:2062 lxc/storage_volume.go:2108 lxc/storage_volume.go:2205 lxc/storage_volume.go:2272 lxc/storage_volume.go:2426 lxc/version.go:22 lxc/warning.go:29 lxc/warning.go:71 lxc/warning.go:262 lxc/warning.go:303 lxc/warning.go:357 +#: lxc/action.go:32 lxc/action.go:53 lxc/action.go:75 lxc/action.go:98 lxc/alias.go:23 lxc/alias.go:60 lxc/alias.go:110 lxc/alias.go:159 lxc/alias.go:214 lxc/auth.go:30 lxc/auth.go:59 lxc/auth.go:98 lxc/auth.go:152 lxc/auth.go:201 lxc/auth.go:332 lxc/auth.go:392 lxc/auth.go:441 lxc/auth.go:493 lxc/auth.go:516 lxc/auth.go:575 lxc/auth.go:731 lxc/auth.go:759 lxc/auth.go:826 lxc/auth.go:889 lxc/auth.go:1017 lxc/auth.go:1040 lxc/auth.go:1098 lxc/auth.go:1167 lxc/auth.go:1189 lxc/auth.go:1365 lxc/auth.go:1403 lxc/auth.go:1455 lxc/auth.go:1504 lxc/auth.go:1623 lxc/auth.go:1683 lxc/auth.go:1732 lxc/auth.go:1783 lxc/auth.go:1806 lxc/auth.go:1859 lxc/cluster.go:29 lxc/cluster.go:122 lxc/cluster.go:206 lxc/cluster.go:255 lxc/cluster.go:306 lxc/cluster.go:367 lxc/cluster.go:439 lxc/cluster.go:471 lxc/cluster.go:521 lxc/cluster.go:604 lxc/cluster.go:689 lxc/cluster.go:804 lxc/cluster.go:880 lxc/cluster.go:982 lxc/cluster.go:1061 lxc/cluster.go:1168 lxc/cluster.go:1190 lxc/cluster_group.go:30 lxc/cluster_group.go:84 lxc/cluster_group.go:157 lxc/cluster_group.go:214 lxc/cluster_group.go:266 lxc/cluster_group.go:382 lxc/cluster_group.go:456 lxc/cluster_group.go:529 lxc/cluster_group.go:577 lxc/cluster_group.go:631 lxc/cluster_role.go:23 lxc/cluster_role.go:50 lxc/cluster_role.go:106 lxc/config.go:32 lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 lxc/config.go:734 lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 lxc/config.go:988 lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 lxc/config_device.go:24 lxc/config_device.go:78 lxc/config_device.go:208 lxc/config_device.go:285 lxc/config_device.go:356 lxc/config_device.go:450 lxc/config_device.go:548 lxc/config_device.go:555 lxc/config_device.go:668 lxc/config_device.go:741 lxc/config_metadata.go:27 lxc/config_metadata.go:55 lxc/config_metadata.go:180 lxc/config_template.go:27 lxc/config_template.go:67 lxc/config_template.go:110 lxc/config_template.go:152 lxc/config_template.go:240 lxc/config_template.go:300 lxc/config_trust.go:34 lxc/config_trust.go:87 lxc/config_trust.go:236 lxc/config_trust.go:350 lxc/config_trust.go:432 lxc/config_trust.go:534 lxc/config_trust.go:580 lxc/config_trust.go:651 lxc/console.go:36 lxc/copy.go:41 lxc/delete.go:31 lxc/exec.go:41 lxc/export.go:32 lxc/file.go:78 lxc/file.go:118 lxc/file.go:167 lxc/file.go:237 lxc/file.go:459 lxc/file.go:967 lxc/image.go:37 lxc/image.go:145 lxc/image.go:312 lxc/image.go:363 lxc/image.go:490 lxc/image.go:651 lxc/image.go:884 lxc/image.go:1019 lxc/image.go:1338 lxc/image.go:1418 lxc/image.go:1477 lxc/image.go:1529 lxc/image.go:1585 lxc/image_alias.go:24 lxc/image_alias.go:60 lxc/image_alias.go:107 lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:29 lxc/info.go:33 lxc/init.go:42 lxc/launch.go:24 lxc/list.go:49 lxc/main.go:82 lxc/manpage.go:22 lxc/monitor.go:33 lxc/move.go:37 lxc/network.go:32 lxc/network.go:135 lxc/network.go:220 lxc/network.go:293 lxc/network.go:372 lxc/network.go:422 lxc/network.go:507 lxc/network.go:592 lxc/network.go:720 lxc/network.go:789 lxc/network.go:912 lxc/network.go:1005 lxc/network.go:1076 lxc/network.go:1128 lxc/network.go:1216 lxc/network.go:1280 lxc/network_acl.go:29 lxc/network_acl.go:94 lxc/network_acl.go:165 lxc/network_acl.go:218 lxc/network_acl.go:266 lxc/network_acl.go:327 lxc/network_acl.go:412 lxc/network_acl.go:492 lxc/network_acl.go:522 lxc/network_acl.go:653 lxc/network_acl.go:702 lxc/network_acl.go:751 lxc/network_acl.go:766 lxc/network_acl.go:887 lxc/network_allocations.go:51 lxc/network_forward.go:29 lxc/network_forward.go:86 lxc/network_forward.go:167 lxc/network_forward.go:231 lxc/network_forward.go:329 lxc/network_forward.go:398 lxc/network_forward.go:496 lxc/network_forward.go:526 lxc/network_forward.go:668 lxc/network_forward.go:730 lxc/network_forward.go:745 lxc/network_forward.go:810 lxc/network_load_balancer.go:29 lxc/network_load_balancer.go:90 lxc/network_load_balancer.go:169 lxc/network_load_balancer.go:233 lxc/network_load_balancer.go:331 lxc/network_load_balancer.go:399 lxc/network_load_balancer.go:497 lxc/network_load_balancer.go:527 lxc/network_load_balancer.go:670 lxc/network_load_balancer.go:731 lxc/network_load_balancer.go:746 lxc/network_load_balancer.go:810 lxc/network_load_balancer.go:896 lxc/network_load_balancer.go:911 lxc/network_load_balancer.go:972 lxc/network_peer.go:28 lxc/network_peer.go:81 lxc/network_peer.go:158 lxc/network_peer.go:215 lxc/network_peer.go:331 lxc/network_peer.go:399 lxc/network_peer.go:488 lxc/network_peer.go:518 lxc/network_peer.go:643 lxc/network_zone.go:28 lxc/network_zone.go:85 lxc/network_zone.go:156 lxc/network_zone.go:211 lxc/network_zone.go:271 lxc/network_zone.go:354 lxc/network_zone.go:434 lxc/network_zone.go:465 lxc/network_zone.go:584 lxc/network_zone.go:632 lxc/network_zone.go:689 lxc/network_zone.go:759 lxc/network_zone.go:811 lxc/network_zone.go:870 lxc/network_zone.go:952 lxc/network_zone.go:1028 lxc/network_zone.go:1058 lxc/network_zone.go:1176 lxc/network_zone.go:1225 lxc/network_zone.go:1240 lxc/network_zone.go:1286 lxc/operation.go:24 lxc/operation.go:56 lxc/operation.go:106 lxc/operation.go:193 lxc/profile.go:29 lxc/profile.go:104 lxc/profile.go:167 lxc/profile.go:250 lxc/profile.go:320 lxc/profile.go:374 lxc/profile.go:424 lxc/profile.go:552 lxc/profile.go:613 lxc/profile.go:674 lxc/profile.go:750 lxc/profile.go:802 lxc/profile.go:878 lxc/profile.go:934 lxc/project.go:29 lxc/project.go:93 lxc/project.go:158 lxc/project.go:221 lxc/project.go:349 lxc/project.go:410 lxc/project.go:523 lxc/project.go:580 lxc/project.go:659 lxc/project.go:690 lxc/project.go:743 lxc/project.go:802 lxc/publish.go:33 lxc/query.go:34 lxc/rebuild.go:27 lxc/remote.go:34 lxc/remote.go:89 lxc/remote.go:624 lxc/remote.go:662 lxc/remote.go:748 lxc/remote.go:821 lxc/remote.go:877 lxc/remote.go:917 lxc/rename.go:21 lxc/restore.go:24 lxc/snapshot.go:28 lxc/storage.go:33 lxc/storage.go:96 lxc/storage.go:170 lxc/storage.go:220 lxc/storage.go:344 lxc/storage.go:414 lxc/storage.go:586 lxc/storage.go:665 lxc/storage.go:761 lxc/storage.go:847 lxc/storage_bucket.go:29 lxc/storage_bucket.go:83 lxc/storage_bucket.go:183 lxc/storage_bucket.go:244 lxc/storage_bucket.go:377 lxc/storage_bucket.go:453 lxc/storage_bucket.go:530 lxc/storage_bucket.go:624 lxc/storage_bucket.go:693 lxc/storage_bucket.go:727 lxc/storage_bucket.go:768 lxc/storage_bucket.go:847 lxc/storage_bucket.go:925 lxc/storage_bucket.go:989 lxc/storage_bucket.go:1124 lxc/storage_volume.go:43 lxc/storage_volume.go:165 lxc/storage_volume.go:240 lxc/storage_volume.go:331 lxc/storage_volume.go:534 lxc/storage_volume.go:613 lxc/storage_volume.go:688 lxc/storage_volume.go:770 lxc/storage_volume.go:851 lxc/storage_volume.go:1060 lxc/storage_volume.go:1175 lxc/storage_volume.go:1322 lxc/storage_volume.go:1406 lxc/storage_volume.go:1613 lxc/storage_volume.go:1694 lxc/storage_volume.go:1809 lxc/storage_volume.go:1953 lxc/storage_volume.go:2062 lxc/storage_volume.go:2108 lxc/storage_volume.go:2205 lxc/storage_volume.go:2272 lxc/storage_volume.go:2426 lxc/version.go:22 lxc/warning.go:29 lxc/warning.go:71 lxc/warning.go:262 lxc/warning.go:303 lxc/warning.go:357 msgid "Description" msgstr "" @@ -1601,6 +1700,10 @@ msgstr "" msgid "Edit a cluster group" msgstr "" +#: lxc/auth.go:888 lxc/auth.go:889 +msgid "Edit an identity as YAML" +msgstr "" + #: lxc/cluster.go:688 lxc/cluster.go:689 msgid "Edit cluster member configurations as YAML" msgstr "" @@ -1609,6 +1712,14 @@ msgstr "" msgid "Edit files in instances" msgstr "" +#: lxc/auth.go:200 lxc/auth.go:201 +msgid "Edit groups as YAML" +msgstr "" + +#: lxc/auth.go:1503 lxc/auth.go:1504 +msgid "Edit identity provider groups as YAML" +msgstr "" + #: lxc/image.go:362 lxc/image.go:363 msgid "Edit image properties" msgstr "" @@ -2050,7 +2161,7 @@ msgid "Forcefully removing a server from the cluster should only be done as a "Are you really sure you want to force removing %s? (yes/no): " msgstr "" -#: lxc/alias.go:112 lxc/cluster.go:124 lxc/cluster.go:881 lxc/cluster_group.go:384 lxc/config_template.go:242 lxc/config_trust.go:352 lxc/config_trust.go:434 lxc/image.go:1045 lxc/image_alias.go:157 lxc/list.go:133 lxc/network.go:916 lxc/network.go:1007 lxc/network_acl.go:97 lxc/network_allocations.go:57 lxc/network_forward.go:89 lxc/network_load_balancer.go:93 lxc/network_peer.go:84 lxc/network_zone.go:88 lxc/network_zone.go:692 lxc/operation.go:108 lxc/profile.go:617 lxc/project.go:412 lxc/project.go:804 lxc/remote.go:666 lxc/storage.go:588 lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 lxc/storage_volume.go:1422 lxc/warning.go:93 +#: lxc/alias.go:112 lxc/auth.go:336 lxc/auth.go:763 lxc/auth.go:1627 lxc/cluster.go:124 lxc/cluster.go:881 lxc/cluster_group.go:384 lxc/config_template.go:242 lxc/config_trust.go:352 lxc/config_trust.go:434 lxc/image.go:1045 lxc/image_alias.go:157 lxc/list.go:133 lxc/network.go:916 lxc/network.go:1007 lxc/network_acl.go:97 lxc/network_allocations.go:57 lxc/network_forward.go:89 lxc/network_load_balancer.go:93 lxc/network_peer.go:84 lxc/network_zone.go:88 lxc/network_zone.go:692 lxc/operation.go:108 lxc/profile.go:617 lxc/project.go:412 lxc/project.go:804 lxc/remote.go:666 lxc/storage.go:588 lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 lxc/storage_volume.go:1422 lxc/warning.go:93 msgid "Format (csv|json|table|yaml|compact)" msgstr "" @@ -2098,6 +2209,10 @@ msgstr "" msgid "GPUs:" msgstr "" +#: lxc/auth.go:811 lxc/auth.go:1667 +msgid "GROUPS" +msgstr "" + #: lxc/manpage.go:21 lxc/manpage.go:22 msgid "Generate manpages for all commands" msgstr "" @@ -2243,6 +2358,21 @@ msgstr "" msgid "Given target %q does not match source volume location %q" msgstr "" +#: lxc/auth.go:136 +#, c-format +msgid "Group %s created" +msgstr "" + +#: lxc/auth.go:186 lxc/auth.go:1489 +#, c-format +msgid "Group %s deleted" +msgstr "" + +#: lxc/auth.go:426 lxc/auth.go:1717 +#, c-format +msgid "Group %s renamed to %s" +msgstr "" + #: lxc/exec.go:60 msgid "Group ID to run the command as (default 0)" msgstr "" @@ -2297,6 +2427,10 @@ msgstr "" msgid "ID: %s" msgstr "" +#: lxc/auth.go:810 +msgid "IDENTIFIER" +msgstr "" + #: lxc/project.go:499 msgid "IMAGES" msgstr "" @@ -2325,6 +2459,11 @@ msgstr "" msgid "ISSUE DATE" msgstr "" +#: lxc/auth.go:1439 +#, c-format +msgid "Identity provider group %s created" +msgstr "" + #: lxc/rebuild.go:32 msgid "If an instance is running, stop it and then rebuild it" msgstr "" @@ -2337,7 +2476,7 @@ msgstr "" msgid "If the snapshot name already exists, delete and create a new one" msgstr "" -#: lxc/main.go:395 +#: lxc/main.go:398 msgid "If this is your first time running LXD on this machine, you should also run: lxd init" msgstr "" @@ -2399,7 +2538,7 @@ msgstr "" msgid "Import backups of custom volumes including their snapshots." msgstr "" -#: lxc/import.go:28 +#: lxc/import.go:29 msgid "Import backups of instances including their snapshots." msgstr "" @@ -2417,7 +2556,7 @@ msgstr "" msgid "Import images into the image store" msgstr "" -#: lxc/import.go:27 +#: lxc/import.go:28 msgid "Import instance backups" msgstr "" @@ -2430,7 +2569,7 @@ msgstr "" msgid "Importing custom volume: %s" msgstr "" -#: lxc/import.go:94 +#: lxc/import.go:96 #, c-format msgid "Importing instance: %s" msgstr "" @@ -2443,6 +2582,10 @@ msgstr "" msgid "Input data" msgstr "" +#: lxc/auth.go:1166 lxc/auth.go:1167 +msgid "Inspect permissions" +msgstr "" + #: lxc/info.go:684 msgid "Instance Only" msgstr "" @@ -2558,7 +2701,7 @@ msgstr "" msgid "Invalid new snapshot name, parent volume must be the same as source" msgstr "" -#: lxc/main.go:495 +#: lxc/main.go:498 msgid "Invalid number of arguments" msgstr "" @@ -2723,6 +2866,18 @@ msgstr "" msgid "List background operations" msgstr "" +#: lxc/auth.go:331 lxc/auth.go:332 +msgid "List groups" +msgstr "" + +#: lxc/auth.go:758 lxc/auth.go:759 +msgid "List identities" +msgstr "" + +#: lxc/auth.go:1622 lxc/auth.go:1623 +msgid "List identity provider groups" +msgstr "" + #: lxc/image_alias.go:151 msgid "List image aliases" msgstr "" @@ -2862,6 +3017,10 @@ msgstr "" msgid "List operations from all projects" msgstr "" +#: lxc/auth.go:1188 lxc/auth.go:1189 +msgid "List permissions" +msgstr "" + #: lxc/profile.go:612 lxc/profile.go:613 msgid "List profiles" msgstr "" @@ -3051,6 +3210,22 @@ msgstr "" msgid "Manage files in instances" msgstr "" +#: lxc/auth.go:58 lxc/auth.go:59 lxc/auth.go:1364 lxc/auth.go:1365 +msgid "Manage groups" +msgstr "" + +#: lxc/auth.go:1016 lxc/auth.go:1017 +msgid "Manage groups for the identity" +msgstr "" + +#: lxc/auth.go:730 lxc/auth.go:731 +msgid "Manage identities" +msgstr "" + +#: lxc/auth.go:1782 lxc/auth.go:1783 +msgid "Manage identity provider group mappings" +msgstr "" + #: lxc/image_alias.go:23 lxc/image_alias.go:24 msgid "Manage image aliases" msgstr "" @@ -3137,6 +3312,10 @@ msgstr "" msgid "Manage network zones" msgstr "" +#: lxc/auth.go:492 lxc/auth.go:493 +msgid "Manage permissions" +msgstr "" + #: lxc/profile.go:28 lxc/profile.go:29 msgid "Manage profiles" msgstr "" @@ -3183,6 +3362,10 @@ msgstr "" msgid "Manage trusted clients" msgstr "" +#: lxc/auth.go:29 lxc/auth.go:30 +msgid "Manage user authorization" +msgstr "" + #: lxc/warning.go:28 lxc/warning.go:29 msgid "Manage warnings" msgstr "" @@ -3267,6 +3450,22 @@ msgstr "" msgid "Missing cluster member name" msgstr "" +#: lxc/auth.go:122 lxc/auth.go:176 lxc/auth.go:254 lxc/auth.go:416 lxc/auth.go:465 lxc/auth.go:540 lxc/auth.go:599 lxc/auth.go:1756 +msgid "Missing group name" +msgstr "" + +#: lxc/auth.go:856 lxc/auth.go:936 lxc/auth.go:1064 lxc/auth.go:1122 +msgid "Missing identity argument" +msgstr "" + +#: lxc/auth.go:1426 lxc/auth.go:1479 lxc/auth.go:1545 lxc/auth.go:1707 +msgid "Missing identity provider group name" +msgstr "" + +#: lxc/auth.go:1830 lxc/auth.go:1883 +msgid "Missing identity provider group name argument" +msgstr "" + #: lxc/config_metadata.go:103 lxc/config_metadata.go:204 lxc/config_template.go:91 lxc/config_template.go:134 lxc/config_template.go:176 lxc/config_template.go:265 lxc/config_template.go:324 lxc/profile.go:128 lxc/profile.go:201 lxc/profile.go:698 lxc/rebuild.go:59 msgid "Missing instance name" msgstr "" @@ -3424,7 +3623,7 @@ msgstr "" msgid "Must supply instance name for: " msgstr "" -#: lxc/cluster.go:183 lxc/cluster.go:964 lxc/cluster_group.go:437 lxc/config_trust.go:409 lxc/config_trust.go:514 lxc/list.go:565 lxc/network.go:980 lxc/network_acl.go:147 lxc/network_peer.go:139 lxc/network_zone.go:138 lxc/network_zone.go:741 lxc/profile.go:657 lxc/project.go:498 lxc/remote.go:724 lxc/storage.go:638 lxc/storage_bucket.go:506 lxc/storage_bucket.go:826 lxc/storage_volume.go:1510 +#: lxc/auth.go:375 lxc/auth.go:809 lxc/auth.go:1666 lxc/cluster.go:183 lxc/cluster.go:964 lxc/cluster_group.go:437 lxc/config_trust.go:409 lxc/config_trust.go:514 lxc/list.go:565 lxc/network.go:980 lxc/network_acl.go:147 lxc/network_peer.go:139 lxc/network_zone.go:138 lxc/network_zone.go:741 lxc/profile.go:657 lxc/project.go:498 lxc/remote.go:724 lxc/storage.go:638 lxc/storage_bucket.go:506 lxc/storage_bucket.go:826 lxc/storage_volume.go:1510 msgid "NAME" msgstr "" @@ -3603,7 +3802,7 @@ msgstr "" msgid "New aliases to add to the image" msgstr "" -#: lxc/copy.go:54 lxc/init.go:51 lxc/move.go:59 +#: lxc/copy.go:54 lxc/import.go:37 lxc/init.go:51 lxc/move.go:59 msgid "New key/value to apply to a specific device" msgstr "" @@ -3762,7 +3961,7 @@ msgstr "" msgid "Partitions:" msgstr "" -#: lxc/main.go:358 +#: lxc/main.go:361 #, c-format msgid "Password for %s: " msgstr "" @@ -3804,7 +4003,7 @@ msgstr "" msgid "Press ctrl+c to finish" msgstr "" -#: lxc/cluster.go:771 lxc/cluster_group.go:340 lxc/config.go:273 lxc/config.go:348 lxc/config.go:1278 lxc/config_metadata.go:148 lxc/config_template.go:206 lxc/config_trust.go:315 lxc/image.go:457 lxc/network.go:687 lxc/network_acl.go:621 lxc/network_forward.go:636 lxc/network_load_balancer.go:638 lxc/network_peer.go:611 lxc/network_zone.go:552 lxc/network_zone.go:1144 lxc/profile.go:519 lxc/project.go:316 lxc/storage.go:311 lxc/storage_bucket.go:344 lxc/storage_bucket.go:1093 lxc/storage_volume.go:994 lxc/storage_volume.go:1026 +#: lxc/auth.go:301 lxc/auth.go:988 lxc/auth.go:1592 lxc/cluster.go:771 lxc/cluster_group.go:340 lxc/config.go:273 lxc/config.go:348 lxc/config.go:1278 lxc/config_metadata.go:148 lxc/config_template.go:206 lxc/config_trust.go:315 lxc/image.go:457 lxc/network.go:687 lxc/network_acl.go:621 lxc/network_forward.go:636 lxc/network_load_balancer.go:638 lxc/network_peer.go:611 lxc/network_zone.go:552 lxc/network_zone.go:1144 lxc/profile.go:519 lxc/project.go:316 lxc/storage.go:311 lxc/storage_bucket.go:344 lxc/storage_bucket.go:1093 lxc/storage_volume.go:994 lxc/storage_volume.go:1026 msgid "Press enter to open the editor again or ctrl+c to abort change" msgstr "" @@ -4129,6 +4328,10 @@ msgstr "" msgid "Remove a cluster member from a cluster group" msgstr "" +#: lxc/auth.go:1097 lxc/auth.go:1098 +msgid "Remove a group from an identity" +msgstr "" + #: lxc/cluster.go:520 lxc/cluster.go:521 msgid "Remove a member from the cluster" msgstr "" @@ -4161,6 +4364,10 @@ msgstr "" msgid "Remove entries from a network zone record" msgstr "" +#: lxc/auth.go:1858 lxc/auth.go:1859 +msgid "Remove identities from groups" +msgstr "" + #: lxc/config_device.go:449 lxc/config_device.go:450 msgid "Remove instance devices" msgstr "" @@ -4169,6 +4376,10 @@ msgstr "" msgid "Remove member from group" msgstr "" +#: lxc/auth.go:574 lxc/auth.go:575 +msgid "Remove permissions from groups" +msgstr "" + #: lxc/network_forward.go:809 lxc/network_forward.go:810 msgid "Remove ports from a forward" msgstr "" @@ -4209,6 +4420,14 @@ msgstr "" msgid "Rename aliases" msgstr "" +#: lxc/auth.go:391 lxc/auth.go:392 +msgid "Rename groups" +msgstr "" + +#: lxc/auth.go:1682 lxc/auth.go:1683 +msgid "Rename identity provider groups" +msgstr "" + #: lxc/rename.go:20 lxc/rename.go:21 msgid "Rename instances and snapshots" msgstr "" @@ -4683,6 +4902,10 @@ msgstr "" msgid "Show all information messages" msgstr "" +#: lxc/auth.go:1731 lxc/auth.go:1732 +msgid "Show an identity provider group" +msgstr "" + #: lxc/cluster_group.go:576 lxc/cluster_group.go:577 msgid "Show cluster group configurations" msgstr "" @@ -4707,6 +4930,19 @@ msgstr "" msgid "Show full device configuration" msgstr "" +#: lxc/auth.go:440 lxc/auth.go:441 +msgid "Show group configurations" +msgstr "" + +#: lxc/auth.go:826 +msgid "Show identity configurations\n" + "\n" + "The argument must be a concatenation of the authentication method and either the \n" + "name or identifier of the identity, delimited by a forward slash. This command\n" + "will fail if an identity name is used that is not unique within the authentication\n" + "method. Use the identifier instead if this occurs.\n" +msgstr "" + #: lxc/image.go:1417 lxc/image.go:1418 msgid "Show image properties" msgstr "" @@ -4727,7 +4963,7 @@ msgstr "" msgid "Show instance or server information" msgstr "" -#: lxc/main.go:268 lxc/main.go:269 +#: lxc/main.go:271 lxc/main.go:272 msgid "Show less common commands" msgstr "" @@ -4958,7 +5194,7 @@ msgstr "" msgid "Storage pool %s pending on member %s" msgstr "" -#: lxc/copy.go:60 lxc/import.go:35 lxc/init.go:54 lxc/move.go:65 +#: lxc/copy.go:60 lxc/import.go:36 lxc/init.go:54 lxc/move.go:65 msgid "Storage pool name" msgstr "" @@ -5023,7 +5259,7 @@ msgstr "" msgid "TOKEN" msgstr "" -#: lxc/config_trust.go:408 lxc/image.go:1062 lxc/image_alias.go:236 lxc/list.go:571 lxc/network.go:981 lxc/network.go:1055 lxc/network_allocations.go:26 lxc/operation.go:171 lxc/storage_volume.go:1509 lxc/warning.go:215 +#: lxc/auth.go:808 lxc/config_trust.go:408 lxc/image.go:1062 lxc/image_alias.go:236 lxc/list.go:571 lxc/network.go:981 lxc/network.go:1055 lxc/network_allocations.go:26 lxc/operation.go:171 lxc/storage_volume.go:1509 lxc/warning.go:215 msgid "TYPE" msgstr "" @@ -5218,7 +5454,7 @@ msgstr "" msgid "This LXD server is not available on the network" msgstr "" -#: lxc/main.go:290 +#: lxc/main.go:293 msgid "This client hasn't been configured to use a remote LXD server yet.\n" "As your platform can't run native Linux instances, you must connect to a remote LXD server.\n" "\n" @@ -5250,7 +5486,7 @@ msgstr "" msgid "To detach from the console, press: +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" msgstr "" @@ -5640,6 +5876,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -5701,11 +5941,11 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -5729,6 +5969,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 lxc/network_acl.go:699 msgid "[:]" msgstr "" @@ -5785,6 +6029,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -5793,14 +6045,34 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "[:] [] [=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6301,6 +6573,21 @@ msgid "lxc alias rename list my-list\n" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "lxc auth group edit < group.yaml\n" + " Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "lxc auth identity edit / < identity.yaml\n" + " Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "lxc auth identity-provider-group edit < identity-provider-group.yaml\n" + " Update an identity provider group using the content of identity-provider-group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "lxc cluster edit < member.yaml\n" " Update a cluster member using the content of member.yaml" @@ -6376,7 +6663,7 @@ msgid "lxc image edit \n" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." msgstr "" diff --git a/po/mr.po b/po/mr.po index ed82a0f7646d..e96147e35337 100644 --- a/po/mr.po +++ b/po/mr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:10+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Marathi +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/nb_NO.po b/po/nb_NO.po index ec31e8c3a9ea..b1a67ce2b38e 100644 --- a/po/nb_NO.po +++ b/po/nb_NO.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:09+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Norwegian Bokmål +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/nl.po b/po/nl.po index 75d4f763776b..088d11430a99 100644 --- a/po/nl.po +++ b/po/nl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:06+0000\n" "Last-Translator: Heimen Stoffels \n" "Language-Team: Dutch +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6275,6 +6538,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6342,15 +6609,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6374,6 +6642,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6431,6 +6703,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6439,15 +6719,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6978,6 +7281,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -7071,7 +7395,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/pa.po b/po/pa.po index a4dc4e2f005c..a077f2faa31d 100644 --- a/po/pa.po +++ b/po/pa.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:08+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Punjabi +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/pl.po b/po/pl.po index 9810c36c6b0c..a0ae1d1a2984 100644 --- a/po/pl.po +++ b/po/pl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:08+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Polish +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6309,6 +6576,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6376,15 +6647,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6408,6 +6680,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6465,6 +6741,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6473,15 +6757,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -7012,6 +7319,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -7105,7 +7433,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/pt.po b/po/pt.po index 4caf02834bfc..322c0243df87 100644 --- a/po/pt.po +++ b/po/pt.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -130,6 +130,68 @@ msgid "" "### Note that the name is shown but cannot be changed" msgstr "" +#: lxc/auth.go:213 +msgid "" +"### This is a YAML representation of the group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### A group has the following format:\n" +"### name: my-first-group\n" +"### description: My first group.\n" +"### permissions:\n" +"### - entity_type: project\n" +"### url: /1.0/projects/default\n" +"### entitlement: can_view\n" +"### identities:\n" +"### - authentication_method: oidc\n" +"### type: OIDC client\n" +"### identifier: jane.doe@example.com\n" +"### name: Jane Doe\n" +"### metadata:\n" +"### subject: auth0|123456789\n" +"### identity_provider_groups:\n" +"### - sales\n" +"### - operations\n" +"###\n" +"### Note that all group information is shown but only the description and " +"permissions can be modified" +msgstr "" + +#: lxc/auth.go:901 +msgid "" +"### This is a YAML representation of the group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### An identity has the following format:\n" +"### authentication_method: oidc\n" +"### type: OIDC client\n" +"### identifier: jane.doe@example.com\n" +"### name: Jane Doe\n" +"### metadata:\n" +"### subject: auth0|123456789\n" +"### projects:\n" +"### - default\n" +"### groups:\n" +"### - my-first-group\n" +"###\n" +"### Note that all identity information is shown but only the projects and " +"groups can be modified" +msgstr "" + +#: lxc/auth.go:1516 +msgid "" +"### This is a YAML representation of the identity provider group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### An identity provider group has the following format:\n" +"### name: operations\n" +"### groups:\n" +"### - foo\n" +"### - bar\n" +"###\n" +"### Note that the name is shown but cannot be modified" +msgstr "" + #: lxc/image.go:378 msgid "" "### This is a YAML representation of the image properties.\n" @@ -495,6 +557,10 @@ msgstr "" msgid "AUTH TYPE" msgstr "" +#: lxc/auth.go:807 +msgid "AUTHENTICATION METHOD" +msgstr "" + #: lxc/remote.go:99 msgid "Accept certificate" msgstr "" @@ -529,6 +595,14 @@ msgstr "" msgid "Add a cluster member to a cluster group" msgstr "" +#: lxc/auth.go:1039 lxc/auth.go:1040 +msgid "Add a group to an identity" +msgstr "" + +#: lxc/auth.go:1805 lxc/auth.go:1806 +msgid "Add a group to an identity provider group" +msgstr "" + #: lxc/network_zone.go:1239 msgid "Add a network zone record entry" msgstr "" @@ -594,6 +668,10 @@ msgid "" "restricted to one or more projects.\n" msgstr "" +#: lxc/auth.go:515 lxc/auth.go:516 +msgid "Add permissions to groups" +msgstr "" + #: lxc/network_forward.go:744 lxc/network_forward.go:745 msgid "Add ports to a forward" msgstr "" @@ -1269,6 +1347,16 @@ msgstr "" msgid "Could not find certificate key file path: %s" msgstr "" +#: lxc/auth.go:300 lxc/auth.go:1591 +#, c-format +msgid "Could not parse group: %s" +msgstr "" + +#: lxc/auth.go:987 +#, c-format +msgid "Could not parse identity: %s" +msgstr "" + #: lxc/cluster.go:1113 #, c-format msgid "Could not read certificate file: %s with error: %v" @@ -1317,6 +1405,14 @@ msgstr "" msgid "Create any directories necessary" msgstr "" +#: lxc/auth.go:97 lxc/auth.go:98 +msgid "Create groups" +msgstr "" + +#: lxc/auth.go:1402 lxc/auth.go:1403 +msgid "Create identity provider groups" +msgstr "" + #: lxc/snapshot.go:27 msgid "Create instance snapshots" msgstr "" @@ -1416,8 +1512,8 @@ msgstr "" msgid "DEFAULT TARGET ADDRESS" msgstr "" -#: lxc/cluster.go:188 lxc/cluster_group.go:438 lxc/image.go:1058 -#: lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 +#: lxc/auth.go:376 lxc/cluster.go:188 lxc/cluster_group.go:438 +#: lxc/image.go:1058 lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 #: lxc/network_acl.go:148 lxc/network_forward.go:145 #: lxc/network_load_balancer.go:148 lxc/network_peer.go:140 #: lxc/network_zone.go:139 lxc/network_zone.go:742 lxc/operation.go:172 @@ -1463,6 +1559,14 @@ msgstr "" msgid "Delete files in instances" msgstr "" +#: lxc/auth.go:151 lxc/auth.go:152 +msgid "Delete groups" +msgstr "" + +#: lxc/auth.go:1454 lxc/auth.go:1455 +msgid "Delete identity provider groups" +msgstr "" + #: lxc/image_alias.go:106 lxc/image_alias.go:107 msgid "Delete image aliases" msgstr "" @@ -1537,18 +1641,26 @@ msgstr "" #: lxc/action.go:32 lxc/action.go:53 lxc/action.go:75 lxc/action.go:98 #: lxc/alias.go:23 lxc/alias.go:60 lxc/alias.go:110 lxc/alias.go:159 -#: lxc/alias.go:214 lxc/cluster.go:29 lxc/cluster.go:122 lxc/cluster.go:206 -#: lxc/cluster.go:255 lxc/cluster.go:306 lxc/cluster.go:367 lxc/cluster.go:439 -#: lxc/cluster.go:471 lxc/cluster.go:521 lxc/cluster.go:604 lxc/cluster.go:689 -#: lxc/cluster.go:804 lxc/cluster.go:880 lxc/cluster.go:982 lxc/cluster.go:1061 -#: lxc/cluster.go:1168 lxc/cluster.go:1190 lxc/cluster_group.go:30 -#: lxc/cluster_group.go:84 lxc/cluster_group.go:157 lxc/cluster_group.go:214 -#: lxc/cluster_group.go:266 lxc/cluster_group.go:382 lxc/cluster_group.go:456 -#: lxc/cluster_group.go:529 lxc/cluster_group.go:577 lxc/cluster_group.go:631 -#: lxc/cluster_role.go:23 lxc/cluster_role.go:50 lxc/cluster_role.go:106 -#: lxc/config.go:32 lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 -#: lxc/config.go:734 lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 -#: lxc/config.go:988 lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 +#: lxc/alias.go:214 lxc/auth.go:30 lxc/auth.go:59 lxc/auth.go:98 +#: lxc/auth.go:152 lxc/auth.go:201 lxc/auth.go:332 lxc/auth.go:392 +#: lxc/auth.go:441 lxc/auth.go:493 lxc/auth.go:516 lxc/auth.go:575 +#: lxc/auth.go:731 lxc/auth.go:759 lxc/auth.go:826 lxc/auth.go:889 +#: lxc/auth.go:1017 lxc/auth.go:1040 lxc/auth.go:1098 lxc/auth.go:1167 +#: lxc/auth.go:1189 lxc/auth.go:1365 lxc/auth.go:1403 lxc/auth.go:1455 +#: lxc/auth.go:1504 lxc/auth.go:1623 lxc/auth.go:1683 lxc/auth.go:1732 +#: lxc/auth.go:1783 lxc/auth.go:1806 lxc/auth.go:1859 lxc/cluster.go:29 +#: lxc/cluster.go:122 lxc/cluster.go:206 lxc/cluster.go:255 lxc/cluster.go:306 +#: lxc/cluster.go:367 lxc/cluster.go:439 lxc/cluster.go:471 lxc/cluster.go:521 +#: lxc/cluster.go:604 lxc/cluster.go:689 lxc/cluster.go:804 lxc/cluster.go:880 +#: lxc/cluster.go:982 lxc/cluster.go:1061 lxc/cluster.go:1168 +#: lxc/cluster.go:1190 lxc/cluster_group.go:30 lxc/cluster_group.go:84 +#: lxc/cluster_group.go:157 lxc/cluster_group.go:214 lxc/cluster_group.go:266 +#: lxc/cluster_group.go:382 lxc/cluster_group.go:456 lxc/cluster_group.go:529 +#: lxc/cluster_group.go:577 lxc/cluster_group.go:631 lxc/cluster_role.go:23 +#: lxc/cluster_role.go:50 lxc/cluster_role.go:106 lxc/config.go:32 +#: lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 lxc/config.go:734 +#: lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 lxc/config.go:988 +#: lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 #: lxc/config_device.go:24 lxc/config_device.go:78 lxc/config_device.go:208 #: lxc/config_device.go:285 lxc/config_device.go:356 lxc/config_device.go:450 #: lxc/config_device.go:548 lxc/config_device.go:555 lxc/config_device.go:668 @@ -1566,7 +1678,7 @@ msgstr "" #: lxc/image.go:651 lxc/image.go:884 lxc/image.go:1019 lxc/image.go:1338 #: lxc/image.go:1418 lxc/image.go:1477 lxc/image.go:1529 lxc/image.go:1585 #: lxc/image_alias.go:24 lxc/image_alias.go:60 lxc/image_alias.go:107 -#: lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:28 +#: lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:29 #: lxc/info.go:33 lxc/init.go:42 lxc/launch.go:24 lxc/list.go:49 lxc/main.go:82 #: lxc/manpage.go:22 lxc/monitor.go:33 lxc/move.go:37 lxc/network.go:32 #: lxc/network.go:135 lxc/network.go:220 lxc/network.go:293 lxc/network.go:372 @@ -1797,6 +1909,10 @@ msgstr "" msgid "Edit a cluster group" msgstr "" +#: lxc/auth.go:888 lxc/auth.go:889 +msgid "Edit an identity as YAML" +msgstr "" + #: lxc/cluster.go:688 lxc/cluster.go:689 msgid "Edit cluster member configurations as YAML" msgstr "" @@ -1805,6 +1921,14 @@ msgstr "" msgid "Edit files in instances" msgstr "" +#: lxc/auth.go:200 lxc/auth.go:201 +msgid "Edit groups as YAML" +msgstr "" + +#: lxc/auth.go:1503 lxc/auth.go:1504 +msgid "Edit identity provider groups as YAML" +msgstr "" + #: lxc/image.go:362 lxc/image.go:363 msgid "Edit image properties" msgstr "" @@ -2272,15 +2396,16 @@ msgid "" "Are you really sure you want to force removing %s? (yes/no): " msgstr "" -#: lxc/alias.go:112 lxc/cluster.go:124 lxc/cluster.go:881 -#: lxc/cluster_group.go:384 lxc/config_template.go:242 lxc/config_trust.go:352 -#: lxc/config_trust.go:434 lxc/image.go:1045 lxc/image_alias.go:157 -#: lxc/list.go:133 lxc/network.go:916 lxc/network.go:1007 lxc/network_acl.go:97 -#: lxc/network_allocations.go:57 lxc/network_forward.go:89 -#: lxc/network_load_balancer.go:93 lxc/network_peer.go:84 -#: lxc/network_zone.go:88 lxc/network_zone.go:692 lxc/operation.go:108 -#: lxc/profile.go:617 lxc/project.go:412 lxc/project.go:804 lxc/remote.go:666 -#: lxc/storage.go:588 lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 +#: lxc/alias.go:112 lxc/auth.go:336 lxc/auth.go:763 lxc/auth.go:1627 +#: lxc/cluster.go:124 lxc/cluster.go:881 lxc/cluster_group.go:384 +#: lxc/config_template.go:242 lxc/config_trust.go:352 lxc/config_trust.go:434 +#: lxc/image.go:1045 lxc/image_alias.go:157 lxc/list.go:133 lxc/network.go:916 +#: lxc/network.go:1007 lxc/network_acl.go:97 lxc/network_allocations.go:57 +#: lxc/network_forward.go:89 lxc/network_load_balancer.go:93 +#: lxc/network_peer.go:84 lxc/network_zone.go:88 lxc/network_zone.go:692 +#: lxc/operation.go:108 lxc/profile.go:617 lxc/project.go:412 +#: lxc/project.go:804 lxc/remote.go:666 lxc/storage.go:588 +#: lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 #: lxc/storage_volume.go:1422 lxc/warning.go:93 msgid "Format (csv|json|table|yaml|compact)" msgstr "" @@ -2329,6 +2454,10 @@ msgstr "" msgid "GPUs:" msgstr "" +#: lxc/auth.go:811 lxc/auth.go:1667 +msgid "GROUPS" +msgstr "" + #: lxc/manpage.go:21 lxc/manpage.go:22 msgid "Generate manpages for all commands" msgstr "" @@ -2474,6 +2603,21 @@ msgstr "" msgid "Given target %q does not match source volume location %q" msgstr "" +#: lxc/auth.go:136 +#, c-format +msgid "Group %s created" +msgstr "" + +#: lxc/auth.go:186 lxc/auth.go:1489 +#, c-format +msgid "Group %s deleted" +msgstr "" + +#: lxc/auth.go:426 lxc/auth.go:1717 +#, c-format +msgid "Group %s renamed to %s" +msgstr "" + #: lxc/exec.go:60 msgid "Group ID to run the command as (default 0)" msgstr "" @@ -2528,6 +2672,10 @@ msgstr "" msgid "ID: %s" msgstr "" +#: lxc/auth.go:810 +msgid "IDENTIFIER" +msgstr "" + #: lxc/project.go:499 msgid "IMAGES" msgstr "" @@ -2556,6 +2704,11 @@ msgstr "" msgid "ISSUE DATE" msgstr "" +#: lxc/auth.go:1439 +#, c-format +msgid "Identity provider group %s created" +msgstr "" + #: lxc/rebuild.go:32 msgid "If an instance is running, stop it and then rebuild it" msgstr "" @@ -2568,7 +2721,7 @@ msgstr "" msgid "If the snapshot name already exists, delete and create a new one" msgstr "" -#: lxc/main.go:395 +#: lxc/main.go:398 msgid "" "If this is your first time running LXD on this machine, you should also run: " "lxd init" @@ -2632,7 +2785,7 @@ msgstr "" msgid "Import backups of custom volumes including their snapshots." msgstr "" -#: lxc/import.go:28 +#: lxc/import.go:29 msgid "Import backups of instances including their snapshots." msgstr "" @@ -2651,7 +2804,7 @@ msgstr "" msgid "Import images into the image store" msgstr "" -#: lxc/import.go:27 +#: lxc/import.go:28 msgid "Import instance backups" msgstr "" @@ -2664,7 +2817,7 @@ msgstr "" msgid "Importing custom volume: %s" msgstr "" -#: lxc/import.go:94 +#: lxc/import.go:96 #, c-format msgid "Importing instance: %s" msgstr "" @@ -2677,6 +2830,10 @@ msgstr "" msgid "Input data" msgstr "" +#: lxc/auth.go:1166 lxc/auth.go:1167 +msgid "Inspect permissions" +msgstr "" + #: lxc/info.go:684 msgid "Instance Only" msgstr "" @@ -2793,7 +2950,7 @@ msgstr "" msgid "Invalid new snapshot name, parent volume must be the same as source" msgstr "" -#: lxc/main.go:495 +#: lxc/main.go:498 msgid "Invalid number of arguments" msgstr "" @@ -2963,6 +3120,18 @@ msgstr "" msgid "List background operations" msgstr "" +#: lxc/auth.go:331 lxc/auth.go:332 +msgid "List groups" +msgstr "" + +#: lxc/auth.go:758 lxc/auth.go:759 +msgid "List identities" +msgstr "" + +#: lxc/auth.go:1622 lxc/auth.go:1623 +msgid "List identity provider groups" +msgstr "" + #: lxc/image_alias.go:151 msgid "List image aliases" msgstr "" @@ -3112,6 +3281,10 @@ msgstr "" msgid "List operations from all projects" msgstr "" +#: lxc/auth.go:1188 lxc/auth.go:1189 +msgid "List permissions" +msgstr "" + #: lxc/profile.go:612 lxc/profile.go:613 msgid "List profiles" msgstr "" @@ -3303,6 +3476,22 @@ msgstr "" msgid "Manage files in instances" msgstr "" +#: lxc/auth.go:58 lxc/auth.go:59 lxc/auth.go:1364 lxc/auth.go:1365 +msgid "Manage groups" +msgstr "" + +#: lxc/auth.go:1016 lxc/auth.go:1017 +msgid "Manage groups for the identity" +msgstr "" + +#: lxc/auth.go:730 lxc/auth.go:731 +msgid "Manage identities" +msgstr "" + +#: lxc/auth.go:1782 lxc/auth.go:1783 +msgid "Manage identity provider group mappings" +msgstr "" + #: lxc/image_alias.go:23 lxc/image_alias.go:24 msgid "Manage image aliases" msgstr "" @@ -3390,6 +3579,10 @@ msgstr "" msgid "Manage network zones" msgstr "" +#: lxc/auth.go:492 lxc/auth.go:493 +msgid "Manage permissions" +msgstr "" + #: lxc/profile.go:28 lxc/profile.go:29 msgid "Manage profiles" msgstr "" @@ -3438,6 +3631,10 @@ msgstr "" msgid "Manage trusted clients" msgstr "" +#: lxc/auth.go:29 lxc/auth.go:30 +msgid "Manage user authorization" +msgstr "" + #: lxc/warning.go:28 lxc/warning.go:29 msgid "Manage warnings" msgstr "" @@ -3531,6 +3728,23 @@ msgstr "" msgid "Missing cluster member name" msgstr "" +#: lxc/auth.go:122 lxc/auth.go:176 lxc/auth.go:254 lxc/auth.go:416 +#: lxc/auth.go:465 lxc/auth.go:540 lxc/auth.go:599 lxc/auth.go:1756 +msgid "Missing group name" +msgstr "" + +#: lxc/auth.go:856 lxc/auth.go:936 lxc/auth.go:1064 lxc/auth.go:1122 +msgid "Missing identity argument" +msgstr "" + +#: lxc/auth.go:1426 lxc/auth.go:1479 lxc/auth.go:1545 lxc/auth.go:1707 +msgid "Missing identity provider group name" +msgstr "" + +#: lxc/auth.go:1830 lxc/auth.go:1883 +msgid "Missing identity provider group name argument" +msgstr "" + #: lxc/config_metadata.go:103 lxc/config_metadata.go:204 #: lxc/config_template.go:91 lxc/config_template.go:134 #: lxc/config_template.go:176 lxc/config_template.go:265 @@ -3749,13 +3963,13 @@ msgstr "" msgid "Must supply instance name for: " msgstr "" -#: lxc/cluster.go:183 lxc/cluster.go:964 lxc/cluster_group.go:437 -#: lxc/config_trust.go:409 lxc/config_trust.go:514 lxc/list.go:565 -#: lxc/network.go:980 lxc/network_acl.go:147 lxc/network_peer.go:139 -#: lxc/network_zone.go:138 lxc/network_zone.go:741 lxc/profile.go:657 -#: lxc/project.go:498 lxc/remote.go:724 lxc/storage.go:638 -#: lxc/storage_bucket.go:506 lxc/storage_bucket.go:826 -#: lxc/storage_volume.go:1510 +#: lxc/auth.go:375 lxc/auth.go:809 lxc/auth.go:1666 lxc/cluster.go:183 +#: lxc/cluster.go:964 lxc/cluster_group.go:437 lxc/config_trust.go:409 +#: lxc/config_trust.go:514 lxc/list.go:565 lxc/network.go:980 +#: lxc/network_acl.go:147 lxc/network_peer.go:139 lxc/network_zone.go:138 +#: lxc/network_zone.go:741 lxc/profile.go:657 lxc/project.go:498 +#: lxc/remote.go:724 lxc/storage.go:638 lxc/storage_bucket.go:506 +#: lxc/storage_bucket.go:826 lxc/storage_volume.go:1510 msgid "NAME" msgstr "" @@ -3938,7 +4152,7 @@ msgstr "" msgid "New aliases to add to the image" msgstr "" -#: lxc/copy.go:54 lxc/init.go:51 lxc/move.go:59 +#: lxc/copy.go:54 lxc/import.go:37 lxc/init.go:51 lxc/move.go:59 msgid "New key/value to apply to a specific device" msgstr "" @@ -4097,7 +4311,7 @@ msgstr "" msgid "Partitions:" msgstr "" -#: lxc/main.go:358 +#: lxc/main.go:361 #, c-format msgid "Password for %s: " msgstr "" @@ -4139,10 +4353,11 @@ msgstr "" msgid "Press ctrl+c to finish" msgstr "" -#: lxc/cluster.go:771 lxc/cluster_group.go:340 lxc/config.go:273 -#: lxc/config.go:348 lxc/config.go:1278 lxc/config_metadata.go:148 -#: lxc/config_template.go:206 lxc/config_trust.go:315 lxc/image.go:457 -#: lxc/network.go:687 lxc/network_acl.go:621 lxc/network_forward.go:636 +#: lxc/auth.go:301 lxc/auth.go:988 lxc/auth.go:1592 lxc/cluster.go:771 +#: lxc/cluster_group.go:340 lxc/config.go:273 lxc/config.go:348 +#: lxc/config.go:1278 lxc/config_metadata.go:148 lxc/config_template.go:206 +#: lxc/config_trust.go:315 lxc/image.go:457 lxc/network.go:687 +#: lxc/network_acl.go:621 lxc/network_forward.go:636 #: lxc/network_load_balancer.go:638 lxc/network_peer.go:611 #: lxc/network_zone.go:552 lxc/network_zone.go:1144 lxc/profile.go:519 #: lxc/project.go:316 lxc/storage.go:311 lxc/storage_bucket.go:344 @@ -4489,6 +4704,10 @@ msgstr "" msgid "Remove a cluster member from a cluster group" msgstr "" +#: lxc/auth.go:1097 lxc/auth.go:1098 +msgid "Remove a group from an identity" +msgstr "" + #: lxc/cluster.go:520 lxc/cluster.go:521 msgid "Remove a member from the cluster" msgstr "" @@ -4521,6 +4740,10 @@ msgstr "" msgid "Remove entries from a network zone record" msgstr "" +#: lxc/auth.go:1858 lxc/auth.go:1859 +msgid "Remove identities from groups" +msgstr "" + #: lxc/config_device.go:449 lxc/config_device.go:450 msgid "Remove instance devices" msgstr "" @@ -4529,6 +4752,10 @@ msgstr "" msgid "Remove member from group" msgstr "" +#: lxc/auth.go:574 lxc/auth.go:575 +msgid "Remove permissions from groups" +msgstr "" + #: lxc/network_forward.go:809 lxc/network_forward.go:810 msgid "Remove ports from a forward" msgstr "" @@ -4570,6 +4797,14 @@ msgstr "" msgid "Rename aliases" msgstr "" +#: lxc/auth.go:391 lxc/auth.go:392 +msgid "Rename groups" +msgstr "" + +#: lxc/auth.go:1682 lxc/auth.go:1683 +msgid "Rename identity provider groups" +msgstr "" + #: lxc/rename.go:20 lxc/rename.go:21 msgid "Rename instances and snapshots" msgstr "" @@ -5075,6 +5310,10 @@ msgstr "" msgid "Show all information messages" msgstr "" +#: lxc/auth.go:1731 lxc/auth.go:1732 +msgid "Show an identity provider group" +msgstr "" + #: lxc/cluster_group.go:576 lxc/cluster_group.go:577 msgid "Show cluster group configurations" msgstr "" @@ -5099,6 +5338,23 @@ msgstr "" msgid "Show full device configuration" msgstr "" +#: lxc/auth.go:440 lxc/auth.go:441 +msgid "Show group configurations" +msgstr "" + +#: lxc/auth.go:826 +msgid "" +"Show identity configurations\n" +"\n" +"The argument must be a concatenation of the authentication method and either " +"the \n" +"name or identifier of the identity, delimited by a forward slash. This " +"command\n" +"will fail if an identity name is used that is not unique within the " +"authentication\n" +"method. Use the identifier instead if this occurs.\n" +msgstr "" + #: lxc/image.go:1417 lxc/image.go:1418 msgid "Show image properties" msgstr "" @@ -5119,7 +5375,7 @@ msgstr "" msgid "Show instance or server information" msgstr "" -#: lxc/main.go:268 lxc/main.go:269 +#: lxc/main.go:271 lxc/main.go:272 msgid "Show less common commands" msgstr "" @@ -5350,7 +5606,7 @@ msgstr "" msgid "Storage pool %s pending on member %s" msgstr "" -#: lxc/copy.go:60 lxc/import.go:35 lxc/init.go:54 lxc/move.go:65 +#: lxc/copy.go:60 lxc/import.go:36 lxc/init.go:54 lxc/move.go:65 msgid "Storage pool name" msgstr "" @@ -5415,9 +5671,9 @@ msgstr "" msgid "TOKEN" msgstr "" -#: lxc/config_trust.go:408 lxc/image.go:1062 lxc/image_alias.go:236 -#: lxc/list.go:571 lxc/network.go:981 lxc/network.go:1055 -#: lxc/network_allocations.go:26 lxc/operation.go:171 +#: lxc/auth.go:808 lxc/config_trust.go:408 lxc/image.go:1062 +#: lxc/image_alias.go:236 lxc/list.go:571 lxc/network.go:981 +#: lxc/network.go:1055 lxc/network_allocations.go:26 lxc/operation.go:171 #: lxc/storage_volume.go:1509 lxc/warning.go:215 msgid "TYPE" msgstr "" @@ -5617,7 +5873,7 @@ msgstr "" msgid "This LXD server is not available on the network" msgstr "" -#: lxc/main.go:290 +#: lxc/main.go:293 msgid "" "This client hasn't been configured to use a remote LXD server yet.\n" "As your platform can't run native Linux instances, you must connect to a " @@ -5653,7 +5909,7 @@ msgstr "" msgid "To detach from the console, press: +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6055,6 +6311,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6122,15 +6382,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6154,6 +6415,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6211,6 +6476,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6219,15 +6492,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6758,6 +7054,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6851,7 +7168,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/pt_BR.po b/po/pt_BR.po index a0f5d6a775cf..9fffd08579ec 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:08+0000\n" "Last-Translator: Renato dos Santos \n" "Language-Team: Portuguese (Brazil) +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6566,6 +6847,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "Em cache: %s" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6634,15 +6919,16 @@ msgstr "" msgid "[] [] []" msgstr "Criar perfis" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 #, fuzzy msgid "[:] []" msgstr "Criar perfis" @@ -6669,6 +6955,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 #, fuzzy @@ -6736,6 +7026,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6745,17 +7043,45 @@ msgstr "" msgid "[:]" msgstr "Criar perfis" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 #, fuzzy msgid "[:]" msgstr "Criar perfis" +#: lxc/auth.go:514 lxc/auth.go:572 +#, fuzzy +msgid "" +"[:] [] " +"[=...]" +msgstr "Criar perfis" + #: lxc/cluster_group.go:526 #, fuzzy msgid "[:] " msgstr "Criar perfis" +#: lxc/auth.go:389 +#, fuzzy +msgid "[:] " +msgstr "Criar perfis" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +#, fuzzy +msgid "[:]" +msgstr "Criar perfis" + +#: lxc/auth.go:1804 +#, fuzzy +msgid "[:] " +msgstr "Editar templates de arquivo do container" + +#: lxc/auth.go:1680 +#, fuzzy +msgid "[:] " +msgstr "Criar perfis" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -7337,6 +7663,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -7430,7 +7777,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." @@ -7710,10 +8057,6 @@ msgstr "sim" #~ msgid "[:]/ " #~ msgstr "Editar templates de arquivo do container" -#, fuzzy -#~ msgid "Creates a new cluster groups" -#~ msgstr "Criar novas redes" - #, fuzzy #~ msgid "Manage network zone record entriess" #~ msgstr "Criar novas redes" diff --git a/po/ru.po b/po/ru.po index 9c5ef2a5ec90..3a939064655c 100644 --- a/po/ru.po +++ b/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:06+0000\n" "Last-Translator: Александр Киль \n" "Language-Team: Russian +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6548,6 +6828,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6623,11 +6907,12 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 #, fuzzy msgid "[:]" msgstr "" @@ -6635,7 +6920,7 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" -#: lxc/import.go:26 +#: lxc/import.go:27 #, fuzzy msgid "[:] []" msgstr "" @@ -6683,6 +6968,10 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 #, fuzzy @@ -6796,6 +7085,14 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 #, fuzzy msgid "[:]" @@ -6812,8 +7109,9 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 #, fuzzy msgid "[:]" msgstr "" @@ -6821,6 +7119,16 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" +#: lxc/auth.go:514 lxc/auth.go:572 +#, fuzzy +msgid "" +"[:] [] " +"[=...]" +msgstr "" +"Изменение состояния одного или нескольких контейнеров %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + #: lxc/cluster_group.go:526 #, fuzzy msgid "[:] " @@ -6829,6 +7137,38 @@ msgstr "" "\n" "lxc %s [:] [[:]...]%s" +#: lxc/auth.go:389 +#, fuzzy +msgid "[:] " +msgstr "" +"Изменение состояния одного или нескольких контейнеров %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +#, fuzzy +msgid "[:]" +msgstr "" +"Изменение состояния одного или нескольких контейнеров %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + +#: lxc/auth.go:1804 +#, fuzzy +msgid "[:] " +msgstr "" +"Изменение состояния одного или нескольких контейнеров %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + +#: lxc/auth.go:1680 +#, fuzzy +msgid "[:] " +msgstr "" +"Изменение состояния одного или нескольких контейнеров %s.\n" +"\n" +"lxc %s [:] [[:]...]%s" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 #, fuzzy msgid "[:]" @@ -7811,6 +8151,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -7904,7 +8265,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." @@ -8228,14 +8589,6 @@ msgstr "да" #~ "\n" #~ "lxc %s [:] [[:]...]%s" -#, fuzzy -#~ msgid "Creates a new cluster groups" -#~ msgstr "Копирование образа: %s" - -#, fuzzy -#~ msgid "Rename a cluster groups" -#~ msgstr "Копирование образа: %s" - #, fuzzy #~ msgid "Manage network zone record entriess" #~ msgstr "Копирование образа: %s" diff --git a/po/si.po b/po/si.po index 0b6cd0bb5d97..29168a9997be 100644 --- a/po/si.po +++ b/po/si.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:11+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Sinhala +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/sl.po b/po/sl.po index e8cf3154ac41..6573dbeb6235 100644 --- a/po/sl.po +++ b/po/sl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:10+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Slovenian +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6059,6 +6315,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6126,15 +6386,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6158,6 +6419,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6215,6 +6480,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6223,15 +6496,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6762,6 +7058,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6855,7 +7172,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/sr.po b/po/sr.po index 3ef6cdcf0a8a..738c7610a60a 100644 --- a/po/sr.po +++ b/po/sr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:07+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Serbian +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6059,6 +6315,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6126,15 +6386,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6158,6 +6419,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6215,6 +6480,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6223,15 +6496,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6762,6 +7058,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6855,7 +7172,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/sv.po b/po/sv.po index 7bc1b0345b2e..a0a2083a0c92 100644 --- a/po/sv.po +++ b/po/sv.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:07+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Swedish +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/te.po b/po/te.po index 9cbf8ee59a27..dead58f454a5 100644 --- a/po/te.po +++ b/po/te.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:09+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Telugu +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/th.po b/po/th.po index 4e46924f2553..0c207d9fb7ee 100644 --- a/po/th.po +++ b/po/th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -130,6 +130,68 @@ msgid "" "### Note that the name is shown but cannot be changed" msgstr "" +#: lxc/auth.go:213 +msgid "" +"### This is a YAML representation of the group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### A group has the following format:\n" +"### name: my-first-group\n" +"### description: My first group.\n" +"### permissions:\n" +"### - entity_type: project\n" +"### url: /1.0/projects/default\n" +"### entitlement: can_view\n" +"### identities:\n" +"### - authentication_method: oidc\n" +"### type: OIDC client\n" +"### identifier: jane.doe@example.com\n" +"### name: Jane Doe\n" +"### metadata:\n" +"### subject: auth0|123456789\n" +"### identity_provider_groups:\n" +"### - sales\n" +"### - operations\n" +"###\n" +"### Note that all group information is shown but only the description and " +"permissions can be modified" +msgstr "" + +#: lxc/auth.go:901 +msgid "" +"### This is a YAML representation of the group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### An identity has the following format:\n" +"### authentication_method: oidc\n" +"### type: OIDC client\n" +"### identifier: jane.doe@example.com\n" +"### name: Jane Doe\n" +"### metadata:\n" +"### subject: auth0|123456789\n" +"### projects:\n" +"### - default\n" +"### groups:\n" +"### - my-first-group\n" +"###\n" +"### Note that all identity information is shown but only the projects and " +"groups can be modified" +msgstr "" + +#: lxc/auth.go:1516 +msgid "" +"### This is a YAML representation of the identity provider group.\n" +"### Any line starting with a '# will be ignored.\n" +"###\n" +"### An identity provider group has the following format:\n" +"### name: operations\n" +"### groups:\n" +"### - foo\n" +"### - bar\n" +"###\n" +"### Note that the name is shown but cannot be modified" +msgstr "" + #: lxc/image.go:378 msgid "" "### This is a YAML representation of the image properties.\n" @@ -495,6 +557,10 @@ msgstr "" msgid "AUTH TYPE" msgstr "" +#: lxc/auth.go:807 +msgid "AUTHENTICATION METHOD" +msgstr "" + #: lxc/remote.go:99 msgid "Accept certificate" msgstr "" @@ -529,6 +595,14 @@ msgstr "" msgid "Add a cluster member to a cluster group" msgstr "" +#: lxc/auth.go:1039 lxc/auth.go:1040 +msgid "Add a group to an identity" +msgstr "" + +#: lxc/auth.go:1805 lxc/auth.go:1806 +msgid "Add a group to an identity provider group" +msgstr "" + #: lxc/network_zone.go:1239 msgid "Add a network zone record entry" msgstr "" @@ -594,6 +668,10 @@ msgid "" "restricted to one or more projects.\n" msgstr "" +#: lxc/auth.go:515 lxc/auth.go:516 +msgid "Add permissions to groups" +msgstr "" + #: lxc/network_forward.go:744 lxc/network_forward.go:745 msgid "Add ports to a forward" msgstr "" @@ -1269,6 +1347,16 @@ msgstr "" msgid "Could not find certificate key file path: %s" msgstr "" +#: lxc/auth.go:300 lxc/auth.go:1591 +#, c-format +msgid "Could not parse group: %s" +msgstr "" + +#: lxc/auth.go:987 +#, c-format +msgid "Could not parse identity: %s" +msgstr "" + #: lxc/cluster.go:1113 #, c-format msgid "Could not read certificate file: %s with error: %v" @@ -1317,6 +1405,14 @@ msgstr "" msgid "Create any directories necessary" msgstr "" +#: lxc/auth.go:97 lxc/auth.go:98 +msgid "Create groups" +msgstr "" + +#: lxc/auth.go:1402 lxc/auth.go:1403 +msgid "Create identity provider groups" +msgstr "" + #: lxc/snapshot.go:27 msgid "Create instance snapshots" msgstr "" @@ -1416,8 +1512,8 @@ msgstr "" msgid "DEFAULT TARGET ADDRESS" msgstr "" -#: lxc/cluster.go:188 lxc/cluster_group.go:438 lxc/image.go:1058 -#: lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 +#: lxc/auth.go:376 lxc/cluster.go:188 lxc/cluster_group.go:438 +#: lxc/image.go:1058 lxc/image_alias.go:237 lxc/list.go:557 lxc/network.go:985 #: lxc/network_acl.go:148 lxc/network_forward.go:145 #: lxc/network_load_balancer.go:148 lxc/network_peer.go:140 #: lxc/network_zone.go:139 lxc/network_zone.go:742 lxc/operation.go:172 @@ -1463,6 +1559,14 @@ msgstr "" msgid "Delete files in instances" msgstr "" +#: lxc/auth.go:151 lxc/auth.go:152 +msgid "Delete groups" +msgstr "" + +#: lxc/auth.go:1454 lxc/auth.go:1455 +msgid "Delete identity provider groups" +msgstr "" + #: lxc/image_alias.go:106 lxc/image_alias.go:107 msgid "Delete image aliases" msgstr "" @@ -1537,18 +1641,26 @@ msgstr "" #: lxc/action.go:32 lxc/action.go:53 lxc/action.go:75 lxc/action.go:98 #: lxc/alias.go:23 lxc/alias.go:60 lxc/alias.go:110 lxc/alias.go:159 -#: lxc/alias.go:214 lxc/cluster.go:29 lxc/cluster.go:122 lxc/cluster.go:206 -#: lxc/cluster.go:255 lxc/cluster.go:306 lxc/cluster.go:367 lxc/cluster.go:439 -#: lxc/cluster.go:471 lxc/cluster.go:521 lxc/cluster.go:604 lxc/cluster.go:689 -#: lxc/cluster.go:804 lxc/cluster.go:880 lxc/cluster.go:982 lxc/cluster.go:1061 -#: lxc/cluster.go:1168 lxc/cluster.go:1190 lxc/cluster_group.go:30 -#: lxc/cluster_group.go:84 lxc/cluster_group.go:157 lxc/cluster_group.go:214 -#: lxc/cluster_group.go:266 lxc/cluster_group.go:382 lxc/cluster_group.go:456 -#: lxc/cluster_group.go:529 lxc/cluster_group.go:577 lxc/cluster_group.go:631 -#: lxc/cluster_role.go:23 lxc/cluster_role.go:50 lxc/cluster_role.go:106 -#: lxc/config.go:32 lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 -#: lxc/config.go:734 lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 -#: lxc/config.go:988 lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 +#: lxc/alias.go:214 lxc/auth.go:30 lxc/auth.go:59 lxc/auth.go:98 +#: lxc/auth.go:152 lxc/auth.go:201 lxc/auth.go:332 lxc/auth.go:392 +#: lxc/auth.go:441 lxc/auth.go:493 lxc/auth.go:516 lxc/auth.go:575 +#: lxc/auth.go:731 lxc/auth.go:759 lxc/auth.go:826 lxc/auth.go:889 +#: lxc/auth.go:1017 lxc/auth.go:1040 lxc/auth.go:1098 lxc/auth.go:1167 +#: lxc/auth.go:1189 lxc/auth.go:1365 lxc/auth.go:1403 lxc/auth.go:1455 +#: lxc/auth.go:1504 lxc/auth.go:1623 lxc/auth.go:1683 lxc/auth.go:1732 +#: lxc/auth.go:1783 lxc/auth.go:1806 lxc/auth.go:1859 lxc/cluster.go:29 +#: lxc/cluster.go:122 lxc/cluster.go:206 lxc/cluster.go:255 lxc/cluster.go:306 +#: lxc/cluster.go:367 lxc/cluster.go:439 lxc/cluster.go:471 lxc/cluster.go:521 +#: lxc/cluster.go:604 lxc/cluster.go:689 lxc/cluster.go:804 lxc/cluster.go:880 +#: lxc/cluster.go:982 lxc/cluster.go:1061 lxc/cluster.go:1168 +#: lxc/cluster.go:1190 lxc/cluster_group.go:30 lxc/cluster_group.go:84 +#: lxc/cluster_group.go:157 lxc/cluster_group.go:214 lxc/cluster_group.go:266 +#: lxc/cluster_group.go:382 lxc/cluster_group.go:456 lxc/cluster_group.go:529 +#: lxc/cluster_group.go:577 lxc/cluster_group.go:631 lxc/cluster_role.go:23 +#: lxc/cluster_role.go:50 lxc/cluster_role.go:106 lxc/config.go:32 +#: lxc/config.go:99 lxc/config.go:384 lxc/config.go:517 lxc/config.go:734 +#: lxc/config.go:858 lxc/config.go:893 lxc/config.go:933 lxc/config.go:988 +#: lxc/config.go:1079 lxc/config.go:1110 lxc/config.go:1164 #: lxc/config_device.go:24 lxc/config_device.go:78 lxc/config_device.go:208 #: lxc/config_device.go:285 lxc/config_device.go:356 lxc/config_device.go:450 #: lxc/config_device.go:548 lxc/config_device.go:555 lxc/config_device.go:668 @@ -1566,7 +1678,7 @@ msgstr "" #: lxc/image.go:651 lxc/image.go:884 lxc/image.go:1019 lxc/image.go:1338 #: lxc/image.go:1418 lxc/image.go:1477 lxc/image.go:1529 lxc/image.go:1585 #: lxc/image_alias.go:24 lxc/image_alias.go:60 lxc/image_alias.go:107 -#: lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:28 +#: lxc/image_alias.go:152 lxc/image_alias.go:255 lxc/import.go:29 #: lxc/info.go:33 lxc/init.go:42 lxc/launch.go:24 lxc/list.go:49 lxc/main.go:82 #: lxc/manpage.go:22 lxc/monitor.go:33 lxc/move.go:37 lxc/network.go:32 #: lxc/network.go:135 lxc/network.go:220 lxc/network.go:293 lxc/network.go:372 @@ -1797,6 +1909,10 @@ msgstr "" msgid "Edit a cluster group" msgstr "" +#: lxc/auth.go:888 lxc/auth.go:889 +msgid "Edit an identity as YAML" +msgstr "" + #: lxc/cluster.go:688 lxc/cluster.go:689 msgid "Edit cluster member configurations as YAML" msgstr "" @@ -1805,6 +1921,14 @@ msgstr "" msgid "Edit files in instances" msgstr "" +#: lxc/auth.go:200 lxc/auth.go:201 +msgid "Edit groups as YAML" +msgstr "" + +#: lxc/auth.go:1503 lxc/auth.go:1504 +msgid "Edit identity provider groups as YAML" +msgstr "" + #: lxc/image.go:362 lxc/image.go:363 msgid "Edit image properties" msgstr "" @@ -2272,15 +2396,16 @@ msgid "" "Are you really sure you want to force removing %s? (yes/no): " msgstr "" -#: lxc/alias.go:112 lxc/cluster.go:124 lxc/cluster.go:881 -#: lxc/cluster_group.go:384 lxc/config_template.go:242 lxc/config_trust.go:352 -#: lxc/config_trust.go:434 lxc/image.go:1045 lxc/image_alias.go:157 -#: lxc/list.go:133 lxc/network.go:916 lxc/network.go:1007 lxc/network_acl.go:97 -#: lxc/network_allocations.go:57 lxc/network_forward.go:89 -#: lxc/network_load_balancer.go:93 lxc/network_peer.go:84 -#: lxc/network_zone.go:88 lxc/network_zone.go:692 lxc/operation.go:108 -#: lxc/profile.go:617 lxc/project.go:412 lxc/project.go:804 lxc/remote.go:666 -#: lxc/storage.go:588 lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 +#: lxc/alias.go:112 lxc/auth.go:336 lxc/auth.go:763 lxc/auth.go:1627 +#: lxc/cluster.go:124 lxc/cluster.go:881 lxc/cluster_group.go:384 +#: lxc/config_template.go:242 lxc/config_trust.go:352 lxc/config_trust.go:434 +#: lxc/image.go:1045 lxc/image_alias.go:157 lxc/list.go:133 lxc/network.go:916 +#: lxc/network.go:1007 lxc/network_acl.go:97 lxc/network_allocations.go:57 +#: lxc/network_forward.go:89 lxc/network_load_balancer.go:93 +#: lxc/network_peer.go:84 lxc/network_zone.go:88 lxc/network_zone.go:692 +#: lxc/operation.go:108 lxc/profile.go:617 lxc/project.go:412 +#: lxc/project.go:804 lxc/remote.go:666 lxc/storage.go:588 +#: lxc/storage_bucket.go:454 lxc/storage_bucket.go:769 #: lxc/storage_volume.go:1422 lxc/warning.go:93 msgid "Format (csv|json|table|yaml|compact)" msgstr "" @@ -2329,6 +2454,10 @@ msgstr "" msgid "GPUs:" msgstr "" +#: lxc/auth.go:811 lxc/auth.go:1667 +msgid "GROUPS" +msgstr "" + #: lxc/manpage.go:21 lxc/manpage.go:22 msgid "Generate manpages for all commands" msgstr "" @@ -2474,6 +2603,21 @@ msgstr "" msgid "Given target %q does not match source volume location %q" msgstr "" +#: lxc/auth.go:136 +#, c-format +msgid "Group %s created" +msgstr "" + +#: lxc/auth.go:186 lxc/auth.go:1489 +#, c-format +msgid "Group %s deleted" +msgstr "" + +#: lxc/auth.go:426 lxc/auth.go:1717 +#, c-format +msgid "Group %s renamed to %s" +msgstr "" + #: lxc/exec.go:60 msgid "Group ID to run the command as (default 0)" msgstr "" @@ -2528,6 +2672,10 @@ msgstr "" msgid "ID: %s" msgstr "" +#: lxc/auth.go:810 +msgid "IDENTIFIER" +msgstr "" + #: lxc/project.go:499 msgid "IMAGES" msgstr "" @@ -2556,6 +2704,11 @@ msgstr "" msgid "ISSUE DATE" msgstr "" +#: lxc/auth.go:1439 +#, c-format +msgid "Identity provider group %s created" +msgstr "" + #: lxc/rebuild.go:32 msgid "If an instance is running, stop it and then rebuild it" msgstr "" @@ -2568,7 +2721,7 @@ msgstr "" msgid "If the snapshot name already exists, delete and create a new one" msgstr "" -#: lxc/main.go:395 +#: lxc/main.go:398 msgid "" "If this is your first time running LXD on this machine, you should also run: " "lxd init" @@ -2632,7 +2785,7 @@ msgstr "" msgid "Import backups of custom volumes including their snapshots." msgstr "" -#: lxc/import.go:28 +#: lxc/import.go:29 msgid "Import backups of instances including their snapshots." msgstr "" @@ -2651,7 +2804,7 @@ msgstr "" msgid "Import images into the image store" msgstr "" -#: lxc/import.go:27 +#: lxc/import.go:28 msgid "Import instance backups" msgstr "" @@ -2664,7 +2817,7 @@ msgstr "" msgid "Importing custom volume: %s" msgstr "" -#: lxc/import.go:94 +#: lxc/import.go:96 #, c-format msgid "Importing instance: %s" msgstr "" @@ -2677,6 +2830,10 @@ msgstr "" msgid "Input data" msgstr "" +#: lxc/auth.go:1166 lxc/auth.go:1167 +msgid "Inspect permissions" +msgstr "" + #: lxc/info.go:684 msgid "Instance Only" msgstr "" @@ -2793,7 +2950,7 @@ msgstr "" msgid "Invalid new snapshot name, parent volume must be the same as source" msgstr "" -#: lxc/main.go:495 +#: lxc/main.go:498 msgid "Invalid number of arguments" msgstr "" @@ -2963,6 +3120,18 @@ msgstr "" msgid "List background operations" msgstr "" +#: lxc/auth.go:331 lxc/auth.go:332 +msgid "List groups" +msgstr "" + +#: lxc/auth.go:758 lxc/auth.go:759 +msgid "List identities" +msgstr "" + +#: lxc/auth.go:1622 lxc/auth.go:1623 +msgid "List identity provider groups" +msgstr "" + #: lxc/image_alias.go:151 msgid "List image aliases" msgstr "" @@ -3112,6 +3281,10 @@ msgstr "" msgid "List operations from all projects" msgstr "" +#: lxc/auth.go:1188 lxc/auth.go:1189 +msgid "List permissions" +msgstr "" + #: lxc/profile.go:612 lxc/profile.go:613 msgid "List profiles" msgstr "" @@ -3303,6 +3476,22 @@ msgstr "" msgid "Manage files in instances" msgstr "" +#: lxc/auth.go:58 lxc/auth.go:59 lxc/auth.go:1364 lxc/auth.go:1365 +msgid "Manage groups" +msgstr "" + +#: lxc/auth.go:1016 lxc/auth.go:1017 +msgid "Manage groups for the identity" +msgstr "" + +#: lxc/auth.go:730 lxc/auth.go:731 +msgid "Manage identities" +msgstr "" + +#: lxc/auth.go:1782 lxc/auth.go:1783 +msgid "Manage identity provider group mappings" +msgstr "" + #: lxc/image_alias.go:23 lxc/image_alias.go:24 msgid "Manage image aliases" msgstr "" @@ -3390,6 +3579,10 @@ msgstr "" msgid "Manage network zones" msgstr "" +#: lxc/auth.go:492 lxc/auth.go:493 +msgid "Manage permissions" +msgstr "" + #: lxc/profile.go:28 lxc/profile.go:29 msgid "Manage profiles" msgstr "" @@ -3438,6 +3631,10 @@ msgstr "" msgid "Manage trusted clients" msgstr "" +#: lxc/auth.go:29 lxc/auth.go:30 +msgid "Manage user authorization" +msgstr "" + #: lxc/warning.go:28 lxc/warning.go:29 msgid "Manage warnings" msgstr "" @@ -3531,6 +3728,23 @@ msgstr "" msgid "Missing cluster member name" msgstr "" +#: lxc/auth.go:122 lxc/auth.go:176 lxc/auth.go:254 lxc/auth.go:416 +#: lxc/auth.go:465 lxc/auth.go:540 lxc/auth.go:599 lxc/auth.go:1756 +msgid "Missing group name" +msgstr "" + +#: lxc/auth.go:856 lxc/auth.go:936 lxc/auth.go:1064 lxc/auth.go:1122 +msgid "Missing identity argument" +msgstr "" + +#: lxc/auth.go:1426 lxc/auth.go:1479 lxc/auth.go:1545 lxc/auth.go:1707 +msgid "Missing identity provider group name" +msgstr "" + +#: lxc/auth.go:1830 lxc/auth.go:1883 +msgid "Missing identity provider group name argument" +msgstr "" + #: lxc/config_metadata.go:103 lxc/config_metadata.go:204 #: lxc/config_template.go:91 lxc/config_template.go:134 #: lxc/config_template.go:176 lxc/config_template.go:265 @@ -3749,13 +3963,13 @@ msgstr "" msgid "Must supply instance name for: " msgstr "" -#: lxc/cluster.go:183 lxc/cluster.go:964 lxc/cluster_group.go:437 -#: lxc/config_trust.go:409 lxc/config_trust.go:514 lxc/list.go:565 -#: lxc/network.go:980 lxc/network_acl.go:147 lxc/network_peer.go:139 -#: lxc/network_zone.go:138 lxc/network_zone.go:741 lxc/profile.go:657 -#: lxc/project.go:498 lxc/remote.go:724 lxc/storage.go:638 -#: lxc/storage_bucket.go:506 lxc/storage_bucket.go:826 -#: lxc/storage_volume.go:1510 +#: lxc/auth.go:375 lxc/auth.go:809 lxc/auth.go:1666 lxc/cluster.go:183 +#: lxc/cluster.go:964 lxc/cluster_group.go:437 lxc/config_trust.go:409 +#: lxc/config_trust.go:514 lxc/list.go:565 lxc/network.go:980 +#: lxc/network_acl.go:147 lxc/network_peer.go:139 lxc/network_zone.go:138 +#: lxc/network_zone.go:741 lxc/profile.go:657 lxc/project.go:498 +#: lxc/remote.go:724 lxc/storage.go:638 lxc/storage_bucket.go:506 +#: lxc/storage_bucket.go:826 lxc/storage_volume.go:1510 msgid "NAME" msgstr "" @@ -3938,7 +4152,7 @@ msgstr "" msgid "New aliases to add to the image" msgstr "" -#: lxc/copy.go:54 lxc/init.go:51 lxc/move.go:59 +#: lxc/copy.go:54 lxc/import.go:37 lxc/init.go:51 lxc/move.go:59 msgid "New key/value to apply to a specific device" msgstr "" @@ -4097,7 +4311,7 @@ msgstr "" msgid "Partitions:" msgstr "" -#: lxc/main.go:358 +#: lxc/main.go:361 #, c-format msgid "Password for %s: " msgstr "" @@ -4139,10 +4353,11 @@ msgstr "" msgid "Press ctrl+c to finish" msgstr "" -#: lxc/cluster.go:771 lxc/cluster_group.go:340 lxc/config.go:273 -#: lxc/config.go:348 lxc/config.go:1278 lxc/config_metadata.go:148 -#: lxc/config_template.go:206 lxc/config_trust.go:315 lxc/image.go:457 -#: lxc/network.go:687 lxc/network_acl.go:621 lxc/network_forward.go:636 +#: lxc/auth.go:301 lxc/auth.go:988 lxc/auth.go:1592 lxc/cluster.go:771 +#: lxc/cluster_group.go:340 lxc/config.go:273 lxc/config.go:348 +#: lxc/config.go:1278 lxc/config_metadata.go:148 lxc/config_template.go:206 +#: lxc/config_trust.go:315 lxc/image.go:457 lxc/network.go:687 +#: lxc/network_acl.go:621 lxc/network_forward.go:636 #: lxc/network_load_balancer.go:638 lxc/network_peer.go:611 #: lxc/network_zone.go:552 lxc/network_zone.go:1144 lxc/profile.go:519 #: lxc/project.go:316 lxc/storage.go:311 lxc/storage_bucket.go:344 @@ -4489,6 +4704,10 @@ msgstr "" msgid "Remove a cluster member from a cluster group" msgstr "" +#: lxc/auth.go:1097 lxc/auth.go:1098 +msgid "Remove a group from an identity" +msgstr "" + #: lxc/cluster.go:520 lxc/cluster.go:521 msgid "Remove a member from the cluster" msgstr "" @@ -4521,6 +4740,10 @@ msgstr "" msgid "Remove entries from a network zone record" msgstr "" +#: lxc/auth.go:1858 lxc/auth.go:1859 +msgid "Remove identities from groups" +msgstr "" + #: lxc/config_device.go:449 lxc/config_device.go:450 msgid "Remove instance devices" msgstr "" @@ -4529,6 +4752,10 @@ msgstr "" msgid "Remove member from group" msgstr "" +#: lxc/auth.go:574 lxc/auth.go:575 +msgid "Remove permissions from groups" +msgstr "" + #: lxc/network_forward.go:809 lxc/network_forward.go:810 msgid "Remove ports from a forward" msgstr "" @@ -4570,6 +4797,14 @@ msgstr "" msgid "Rename aliases" msgstr "" +#: lxc/auth.go:391 lxc/auth.go:392 +msgid "Rename groups" +msgstr "" + +#: lxc/auth.go:1682 lxc/auth.go:1683 +msgid "Rename identity provider groups" +msgstr "" + #: lxc/rename.go:20 lxc/rename.go:21 msgid "Rename instances and snapshots" msgstr "" @@ -5075,6 +5310,10 @@ msgstr "" msgid "Show all information messages" msgstr "" +#: lxc/auth.go:1731 lxc/auth.go:1732 +msgid "Show an identity provider group" +msgstr "" + #: lxc/cluster_group.go:576 lxc/cluster_group.go:577 msgid "Show cluster group configurations" msgstr "" @@ -5099,6 +5338,23 @@ msgstr "" msgid "Show full device configuration" msgstr "" +#: lxc/auth.go:440 lxc/auth.go:441 +msgid "Show group configurations" +msgstr "" + +#: lxc/auth.go:826 +msgid "" +"Show identity configurations\n" +"\n" +"The argument must be a concatenation of the authentication method and either " +"the \n" +"name or identifier of the identity, delimited by a forward slash. This " +"command\n" +"will fail if an identity name is used that is not unique within the " +"authentication\n" +"method. Use the identifier instead if this occurs.\n" +msgstr "" + #: lxc/image.go:1417 lxc/image.go:1418 msgid "Show image properties" msgstr "" @@ -5119,7 +5375,7 @@ msgstr "" msgid "Show instance or server information" msgstr "" -#: lxc/main.go:268 lxc/main.go:269 +#: lxc/main.go:271 lxc/main.go:272 msgid "Show less common commands" msgstr "" @@ -5350,7 +5606,7 @@ msgstr "" msgid "Storage pool %s pending on member %s" msgstr "" -#: lxc/copy.go:60 lxc/import.go:35 lxc/init.go:54 lxc/move.go:65 +#: lxc/copy.go:60 lxc/import.go:36 lxc/init.go:54 lxc/move.go:65 msgid "Storage pool name" msgstr "" @@ -5415,9 +5671,9 @@ msgstr "" msgid "TOKEN" msgstr "" -#: lxc/config_trust.go:408 lxc/image.go:1062 lxc/image_alias.go:236 -#: lxc/list.go:571 lxc/network.go:981 lxc/network.go:1055 -#: lxc/network_allocations.go:26 lxc/operation.go:171 +#: lxc/auth.go:808 lxc/config_trust.go:408 lxc/image.go:1062 +#: lxc/image_alias.go:236 lxc/list.go:571 lxc/network.go:981 +#: lxc/network.go:1055 lxc/network_allocations.go:26 lxc/operation.go:171 #: lxc/storage_volume.go:1509 lxc/warning.go:215 msgid "TYPE" msgstr "" @@ -5617,7 +5873,7 @@ msgstr "" msgid "This LXD server is not available on the network" msgstr "" -#: lxc/main.go:290 +#: lxc/main.go:293 msgid "" "This client hasn't been configured to use a remote LXD server yet.\n" "As your platform can't run native Linux instances, you must connect to a " @@ -5653,7 +5909,7 @@ msgstr "" msgid "To detach from the console, press: +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6055,6 +6311,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6122,15 +6382,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6154,6 +6415,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6211,6 +6476,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6219,15 +6492,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6758,6 +7054,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6851,7 +7168,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/tr.po b/po/tr.po index 7b07b3d866a7..e02e57dc8daa 100644 --- a/po/tr.po +++ b/po/tr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:07+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Turkish +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/tzm.po b/po/tzm.po index 785157dcfbdf..dc8590225f7b 100644 --- a/po/tzm.po +++ b/po/tzm.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:10+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Tamazight (Central Atlas) +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/ug.po b/po/ug.po index 3cdb94192a8a..a8eeed562140 100644 --- a/po/ug.po +++ b/po/ug.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:10+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Uyghur +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/uk.po b/po/uk.po index f535cf053151..86d313333914 100644 --- a/po/uk.po +++ b/po/uk.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:09+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Ukrainian +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6059,6 +6315,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6126,15 +6386,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6158,6 +6419,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6215,6 +6480,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6223,15 +6496,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6762,6 +7058,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6855,7 +7172,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/zh_Hans.po b/po/zh_Hans.po index bb89907f2b9f..5f7b99ef569f 100644 --- a/po/zh_Hans.po +++ b/po/zh_Hans.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:07+0000\n" "Last-Translator: 0x0916 \n" "Language-Team: Chinese (Simplified) +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6208,6 +6475,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6275,15 +6546,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6307,6 +6579,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6364,6 +6640,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6372,15 +6656,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6911,6 +7218,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -7004,7 +7332,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." diff --git a/po/zh_Hant.po b/po/zh_Hant.po index f524f39ff81d..d60e714d764a 100644 --- a/po/zh_Hant.po +++ b/po/zh_Hant.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: lxd\n" "Report-Msgid-Bugs-To: lxd@lists.canonical.com\n" -"POT-Creation-Date: 2024-02-20 10:11+0100\n" +"POT-Creation-Date: 2024-02-20 21:09+0000\n" "PO-Revision-Date: 2022-03-10 15:11+0000\n" "Last-Translator: Anonymous \n" "Language-Team: Chinese (Traditional) +a q" msgstr "" -#: lxc/main.go:400 +#: lxc/main.go:403 msgid "" "To start your first container, try: lxc launch ubuntu:22.04\n" "Or for a virtual machine: lxc launch ubuntu:22.04 --vm" @@ -6058,6 +6314,10 @@ msgstr "" msgid "Verb: %s (%s)" msgstr "" +#: lxc/auth.go:825 +msgid "View an identity" +msgstr "" + #: lxc/storage_volume.go:1374 msgid "Volume Only" msgstr "" @@ -6125,15 +6385,16 @@ msgstr "" msgid "[] [] []" msgstr "" -#: lxc/cluster.go:119 lxc/cluster.go:878 lxc/cluster_group.go:379 -#: lxc/config_trust.go:347 lxc/config_trust.go:430 lxc/monitor.go:31 -#: lxc/network.go:909 lxc/network_acl.go:91 lxc/network_zone.go:82 -#: lxc/operation.go:103 lxc/profile.go:610 lxc/project.go:407 -#: lxc/storage.go:583 lxc/version.go:20 lxc/warning.go:68 +#: lxc/auth.go:329 lxc/auth.go:756 lxc/auth.go:1620 lxc/cluster.go:119 +#: lxc/cluster.go:878 lxc/cluster_group.go:379 lxc/config_trust.go:347 +#: lxc/config_trust.go:430 lxc/monitor.go:31 lxc/network.go:909 +#: lxc/network_acl.go:91 lxc/network_zone.go:82 lxc/operation.go:103 +#: lxc/profile.go:610 lxc/project.go:407 lxc/storage.go:583 lxc/version.go:20 +#: lxc/warning.go:68 msgid "[:]" msgstr "" -#: lxc/import.go:26 +#: lxc/import.go:27 msgid "[:] []" msgstr "" @@ -6157,6 +6418,10 @@ msgstr "" msgid "[:] [...]" msgstr "" +#: lxc/auth.go:1187 +msgid "[:] [project=] [entity_type=]" +msgstr "" + #: lxc/network_acl.go:163 lxc/network_acl.go:216 lxc/network_acl.go:520 #: lxc/network_acl.go:699 msgid "[:]" @@ -6214,6 +6479,14 @@ msgstr "" msgid "[:] " msgstr "" +#: lxc/auth.go:824 +msgid "[:]/" +msgstr "" + +#: lxc/auth.go:1038 lxc/auth.go:1096 lxc/auth.go:1857 +msgid "[:]/ " +msgstr "" + #: lxc/cluster.go:687 msgid "[:]" msgstr "" @@ -6222,15 +6495,38 @@ msgstr "" msgid "[:]" msgstr "" -#: lxc/cluster_group.go:155 lxc/cluster_group.go:211 lxc/cluster_group.go:264 -#: lxc/cluster_group.go:575 +#: lxc/auth.go:96 lxc/auth.go:149 lxc/auth.go:199 lxc/auth.go:439 +#: lxc/auth.go:887 lxc/auth.go:1401 lxc/cluster_group.go:155 +#: lxc/cluster_group.go:211 lxc/cluster_group.go:264 lxc/cluster_group.go:575 msgid "[:]" msgstr "" +#: lxc/auth.go:514 lxc/auth.go:572 +msgid "" +"[:] [] " +"[=...]" +msgstr "" + #: lxc/cluster_group.go:526 msgid "[:] " msgstr "" +#: lxc/auth.go:389 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1452 lxc/auth.go:1502 lxc/auth.go:1730 +msgid "[:]" +msgstr "" + +#: lxc/auth.go:1804 +msgid "[:] " +msgstr "" + +#: lxc/auth.go:1680 +msgid "[:] " +msgstr "" + #: lxc/image.go:361 lxc/image.go:882 lxc/image.go:1416 msgid "[:]" msgstr "" @@ -6761,6 +7057,27 @@ msgid "" " Rename existing alias \"list\" to \"my-list\"." msgstr "" +#: lxc/auth.go:203 +msgid "" +"lxc auth group edit < group.yaml\n" +" Update a group using the content of group.yaml" +msgstr "" + +#: lxc/auth.go:891 +msgid "" +"lxc auth identity edit / < " +"identity.yaml\n" +" Update an identity using the content of identity.yaml" +msgstr "" + +#: lxc/auth.go:1506 +msgid "" +"lxc auth identity-provider-group edit < identity-" +"provider-group.yaml\n" +" Update an identity provider group using the content of identity-provider-" +"group.yaml" +msgstr "" + #: lxc/cluster.go:691 msgid "" "lxc cluster edit < member.yaml\n" @@ -6854,7 +7171,7 @@ msgid "" " Load the image properties from a YAML file" msgstr "" -#: lxc/import.go:30 +#: lxc/import.go:31 msgid "" "lxc import backup0.tar.gz\n" " Create a new instance using backup0.tar.gz as the source." From c6af7ec4eef43230026c72651662a63aa2e72713 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 19 Feb 2024 18:15:28 +0000 Subject: [PATCH 34/34] test/suites: Adds authorization test. Signed-off-by: Mark Laing --- test/main.sh | 1 + test/suites/auth.sh | 119 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 test/suites/auth.sh diff --git a/test/main.sh b/test/main.sh index 973ef11acb33..ce3278b35d9b 100755 --- a/test/main.sh +++ b/test/main.sh @@ -207,6 +207,7 @@ if [ "${1:-"all"}" != "cluster" ]; then run_test test_sql "lxd sql" run_test test_tls_restrictions "TLS restrictions" run_test test_oidc "OpenID Connect" + run_test test_authorization "Authorization" run_test test_certificate_edit "Certificate edit" run_test test_basic_usage "basic usage" run_test test_server_info "server info" diff --git a/test/suites/auth.sh b/test/suites/auth.sh new file mode 100644 index 000000000000..6d425b13bbf7 --- /dev/null +++ b/test/suites/auth.sh @@ -0,0 +1,119 @@ +test_authorization() { + ensure_import_testimage + ensure_has_localhost_remote "${LXD_ADDR}" + tls_user_fingerprint="$(lxc config trust list --format json | jq -r '.[0].fingerprint')" + + ### GROUP MANAGEMENT ### + lxc auth group create test-group + + # Invalid entity types + ! lxc auth group permission add test-group not_an_entity_type admin || false + ! lxc auth group permission add test-group not_an_entity_type not_an_entity_name admin || false + + # Entity types with no entitlements + ! lxc auth group permission add test-group container fake_name can_view || false # No entitlements defined for containers (use instance). + ! lxc auth group permission add test-group certificate "${tls_user_fingerprint}" can_view || false # No entitlements defined for certificates (use identity). + ! lxc auth group permission add test-group instance_backup fake_name can_view || false # No entitlements defined for instance backups (use can_manage_backups on parent instance). + ! lxc auth group permission add test-group instance_snapshot fake_name can_view || false # No entitlements defined for instance snapshots (use can_manage_snapshots on parent instance). + ! lxc auth group permission add test-group node fake_name can_view || false # No entitlements defined for cluster members (use server entitlements). + ! lxc auth group permission add test-group operation fake_name can_view || false # No entitlements defined for operations (use operation secrets). + ! lxc auth group permission add test-group storage_volume_backup fake_name can_view || false # No entitlements defined for storage volume backups (use can_manage_backups on parent volume). + ! lxc auth group permission add test-group storage_volume_snapshot fake_name can_view || false # No entitlements defined for storage volume snapshots (use can_manage_snapshots on parent volume). + ! lxc auth group permission add test-group warning fake_name can_view || false # No entitlements defined for warnings (may contain sensitive data, use server level entitlements). + ! lxc auth group permission add test-group cluster_group fake_name can_view || false # No entitlements defined for cluster groups (use server entitlements). + + # Server permissions + lxc auth group permission add test-group server admin # Valid + lxc auth group permission remove test-group server admin # Valid + ! lxc auth group permission remove test-group server admin || false # Permission already removed + ! lxc auth group permission add test-group server not_a_server_entitlement || false # Invalid entitlement + + # Identity permissions. + ! lxc auth group permission add test-group identity "${tls_user_fingerprint}" can_view || false # Missing authentication method + lxc auth group permission add test-group identity "tls/${tls_user_fingerprint}" can_view # Valid + lxc auth group permission remove test-group identity "tls/${tls_user_fingerprint}" can_view + ! lxc auth group permission remove test-group identity "tls/${tls_user_fingerprint}" can_view || false # Already removed + + # Project permissions. + ! lxc auth group permission add test-group project not-found operator # Not found + lxc auth group permission add test-group project default operator # Valid + lxc auth group permission remove test-group project default operator # Valid + ! lxc auth group permission remove test-group project default operator || false # Already removed + ! lxc auth group permission add test-group project default not_a_project_entitlement || false # Invalid entitlement + + # Instance permissions. + ! lxc auth group permission add test-group instance c1 can_exec project=default || false # Not found + lxc init testimage c1 + ! lxc auth group permission add test-group instance c1 can_exec || false # No project + lxc auth group permission add test-group instance c1 can_exec project=default # Valid + lxc auth group permission remove test-group instance c1 can_exec project=default # Valid + ! lxc auth group permission remove test-group instance c1 can_exec project=default || false # Already removed + ! lxc auth group permission add test-group instance c1 not_an_instance_entitlement project=default || false # Invalid entitlement + lxc rm c1 + + # Network permissions + ! lxc auth group permission add test-group network n1 can_view project=default || false # Not found + lxc network create n1 + ! lxc auth group permission add test-group network n1 can_view || false # No project + lxc auth group permission add test-group network n1 can_view project=default # Valid + lxc auth group permission remove test-group network n1 can_view project=default # Valid + ! lxc auth group permission remove test-group network n1 can_view project=default || false # Already removed + ! lxc auth group permission add test-group network n1 not_a_network_entitlement project=default || false # Invalid entitlement + lxc network rm n1 + + ### IDENTITY MANAGEMENT ### + lxc config trust show "${tls_user_fingerprint}" + ! lxc auth identity group add "tls/${tls_user_fingerprint}" test-group || false # TLS identities cannot be added to groups (yet). + + spawn_oidc + lxc config set "oidc.issuer=http://127.0.0.1:$(cat "${TEST_DIR}/oidc.port")/" + lxc config set "oidc.client.id=device" + + set_oidc test-user test-user@example.com + BROWSER=curl lxc remote add --accept-certificate oidc "${LXD_ADDR}" --auth-type oidc + + ! lxc auth identity group add oidc/test-user@example.com not-found || false # Group not found + lxc auth identity group add oidc/test-user@example.com test-group # Valid + + # Check user has been added to the group. + lxc auth identity list --format csv | grep -Fq 'oidc,OIDC client," ",test-user@example.com,test-group' + + ### IDENTITY PROVIDER GROUP MANAGEMENT ### + lxc auth identity-provider-group create test-idp-group + ! lxc auth identity-provider-group group add test-idp-group not-found || false # Group not found + lxc auth identity-provider-group group add test-idp-group test-group + lxc auth identity-provider-group group remove test-idp-group test-group + ! lxc auth identity-provider-group group remove test-idp-group test-group || false # Group not mapped + + ### PERMISSION INSPECTION ### + list_output="$(lxc auth permission list --format csv)" + + # grep for some easily grepable things. + echo "${list_output}" | grep -Fq 'identity,/1.0/auth/identities/oidc/test-user@example.com,"can_delete,can_edit,can_view"' + echo "${list_output}" | grep -Fq 'group,/1.0/auth/groups/test-group,"can_delete,can_edit,can_view"' + echo "${list_output}" | grep -Fq 'identity_provider_group,/1.0/auth/identity-provider-groups/test-idp-group,"can_delete,can_edit,can_view"' + echo "${list_output}" | grep -Fq 'image_alias,/1.0/images/aliases/testimage?project=default,"can_delete,can_edit,can_view"' + echo "${list_output}" | grep -Fq 'profile,/1.0/profiles/default?project=default,"can_delete,can_edit,can_view"' + echo "${list_output}" | grep -Fq 'project,/1.0/projects/default,"can_create_image_aliases,can_create_images,can_create_instances,..."' + + list_output="$(lxc auth permission list entity_type=server --format csv --max-entitlements 0)" + echo "${list_output}" | grep -Fq 'server,/1.0,"admin,can_create_groups,can_create_identities,can_create_projects,can_create_storage_pools,can_delete_groups,can_delete_identities,can_delete_projects,can_delete_storage_pools,can_edit,can_edit_groups,can_edit_identities,can_edit_projects,can_edit_storage_pools,can_override_cluster_target_restriction,can_view,can_view_configuration,can_view_groups,can_view_identities,can_view_metrics,can_view_permissions,can_view_privileged_events,can_view_projects,can_view_resources,can_view_warnings,permission_manager,project_manager,storage_pool_manager,viewer"' + + list_output="$(lxc auth permission list entity_type=project --format csv --max-entitlements 0)" + echo "${list_output}" | grep -Fq 'project,/1.0/projects/default,"can_create_image_aliases,can_create_images,can_create_instances,can_create_network_acls,can_create_network_zones,can_create_networks,can_create_profiles,can_create_storage_buckets,can_create_storage_volumes,can_delete,can_delete_image_aliases,can_delete_images,can_delete_instances,can_delete_network_acls,can_delete_network_zones,can_delete_networks,can_delete_profiles,can_delete_storage_buckets,can_delete_storage_volumes,can_edit,can_edit_image_aliases,can_edit_images,can_edit_instances,can_edit_network_acls,can_edit_network_zones,can_edit_networks,can_edit_profiles,can_edit_storage_buckets,can_edit_storage_volumes,can_operate_instances,can_view,can_view_events,can_view_image_aliases,can_view_images,can_view_instances,can_view_network_acls,can_view_network_zones,can_view_networks,can_view_operations,can_view_profiles,can_view_storage_buckets,can_view_storage_volumes,image_alias_manager,image_manager,instance_manager,network_acl_manager,network_manager,network_zone_manager,operator,profile_manager,storage_bucket_manager,storage_volume_manager,viewer"' + + # Test max entitlements flag doesn't apply to entitlements that are assigned. + lxc auth group permission add test-group server viewer + lxc auth group permission add test-group server project_manager + list_output="$(lxc auth permission list entity_type=server --format csv)" + echo "${list_output}" | grep -Fq 'server,/1.0,"project_manager:(test-group),viewer:(test-group),admin,can_create_groups,can_create_identities,..."' + + # Cleanup + lxc auth group delete test-group + lxc auth identity-provider-group delete test-idp-group + lxc remote remove oidc + kill_oidc + rm "${TEST_DIR}/oidc.user" + lxc config unset oidc.issuer + lxc config unset oidc.client.id +}