diff --git a/lxd/auth_groups.go b/lxd/auth_groups.go index 9051b4304308..aacd3f57c3df 100644 --- a/lxd/auth_groups.go +++ b/lxd/auth_groups.go @@ -24,6 +24,7 @@ import ( "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/entity" + "github.com/canonical/lxd/shared/logger" ) var authGroupsCmd = APIEndpoint{ @@ -170,7 +171,7 @@ func getAuthGroups(d *Daemon, r *http.Request) response.Response { } var groups []dbCluster.AuthGroup - groupsPermissions := make(map[int][]dbCluster.Permission) + var authGroupPermissions []dbCluster.AuthGroupPermission groupsIdentities := make(map[int][]dbCluster.Identity) groupsIdentityProviderGroups := make(map[int][]dbCluster.IdentityProviderGroup) entityURLs := make(map[entity.Type]map[int]*api.URL) @@ -204,26 +205,25 @@ func getAuthGroups(d *Daemon, r *http.Request) response.Response { return err } - groupsPermissions, err = dbCluster.GetAllPermissionsByAuthGroupIDs(ctx, tx.Tx()) + authGroupPermissions, err = dbCluster.GetAuthGroupPermissions(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) + var danglingPermissions []dbCluster.AuthGroupPermission + authGroupPermissions, danglingPermissions, entityURLs, err = dbCluster.GetPermissionEntityURLs(ctx, tx.Tx(), authGroupPermissions) if err != nil { return err } + + if len(danglingPermissions) > 0 { + logger.Warn("Auth group permissions table references entities that no longer exist. Attempting clean up...") + err := dbCluster.DeletePermissions(ctx, tx.Tx(), danglingPermissions) + if err != nil { + logger.Error("Failed to delete orphaned permissions", logger.Ctx{"error": err}) + } + } } return nil @@ -233,29 +233,23 @@ func getAuthGroups(d *Daemon, r *http.Request) response.Response { } if recursion == "1" { + authGroupPermissionsByGroupID := make(map[int][]dbCluster.AuthGroupPermission, len(groups)) + for _, permission := range authGroupPermissions { + authGroupPermissionsByGroupID[permission.GroupID] = append(authGroupPermissionsByGroupID[permission.GroupID], permission) + } + 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] + permissions, ok := authGroupPermissionsByGroupID[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(), + EntityReference: entityURLs[entity.Type(permission.EntityType)][permission.EntityID].String(), Entitlement: string(permission.Entitlement), }) } @@ -357,12 +351,7 @@ func createAuthGroup(d *Daemon, r *http.Request) response.Response { return err } - permissionIDs, err := upsertPermissions(ctx, tx.Tx(), group.Permissions) - if err != nil { - return err - } - - err = dbCluster.SetAuthGroupPermissions(ctx, tx.Tx(), int(groupID), permissionIDs) + err = upsertPermissions(ctx, tx.Tx(), int(groupID), group.Permissions) if err != nil { return err } @@ -515,12 +504,7 @@ func updateAuthGroup(d *Daemon, r *http.Request) response.Response { return err } - permissionIDs, err := upsertPermissions(ctx, tx.Tx(), groupPut.Permissions) - if err != nil { - return err - } - - err = dbCluster.SetAuthGroupPermissions(ctx, tx.Tx(), group.ID, permissionIDs) + err = upsertPermissions(ctx, tx.Tx(), group.ID, groupPut.Permissions) if err != nil { return err } @@ -618,12 +602,7 @@ func patchAuthGroup(d *Daemon, r *http.Request) response.Response { } } - permissionIDs, err := upsertPermissions(ctx, tx.Tx(), newPermissions) - if err != nil { - return err - } - - err = dbCluster.SetAuthGroupPermissions(ctx, tx.Tx(), group.ID, permissionIDs) + err = upsertPermissions(ctx, tx.Tx(), group.ID, newPermissions) if err != nil { return err } @@ -825,16 +804,15 @@ func validatePermissions(permissions []api.Permission) error { 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) { +// upsertPermissions converts the given slice of api.Permission into a slice of cluster.AuthGroupPermission by resolving +// the URLs of each permission to an entity ID. Then sets those permissions against the group with the given ID. +func upsertPermissions(ctx context.Context, tx *sql.Tx, groupID int, permissions []api.Permission) 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) + return fmt.Errorf("Failed to parse permission entity reference: %w", err) } apiURL := &api.URL{URL: *u} @@ -844,40 +822,30 @@ func upsertPermissions(ctx context.Context, tx *sql.Tx, permissions []api.Permis err := dbCluster.PopulateEntityReferencesFromURLs(ctx, tx, entityReferences) if err != nil { - return nil, err + return err } - var permissionIDs []int + authGroupPermissions := make([]dbCluster.AuthGroupPermission, 0, len(permissions)) 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) + return api.StatusErrorf(http.StatusBadRequest, "Missing entity ID for permission with URL %q", permission.EntityReference) } - // 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) - } + authGroupPermissions = append(authGroupPermissions, dbCluster.AuthGroupPermission{ + GroupID: groupID, + Entitlement: entitlement, + EntityType: entityType, + EntityID: entityRef.EntityID, + }) + } - permissionIDs = append(permissionIDs, int(lastInsertID)) + err = dbCluster.SetAuthGroupPermissions(ctx, tx, groupID, authGroupPermissions) + if err != nil { + return fmt.Errorf("Failed to set group permissions: %w", err) } - return permissionIDs, nil + return nil } diff --git a/lxd/daemon.go b/lxd/daemon.go index 922e35c878d3..bd44bfae42dd 100644 --- a/lxd/daemon.go +++ b/lxd/daemon.go @@ -1257,6 +1257,19 @@ func (d *Daemon) init() error { } d.gateway.Cluster = d.db.Cluster + isLeader, err := isRaftLeader(d) + if err != nil { + return fmt.Errorf("Failed to check raft leader status: %w", err) + } + + if isLeader { + err := d.db.Cluster.Transaction(d.shutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error { + return dbCluster.PrepareTriggers(ctx, tx.Tx()) + }) + if err != nil { + return fmt.Errorf("Failed to prepare cluster database triggers: %w", err) + } + } // This logic used to belong to patchUpdateFromV10, but has been moved // here because it needs database access. @@ -1991,6 +2004,22 @@ func (d *Daemon) setupSyslogSocket(enable bool) error { return nil } +func isRaftLeader(d *Daemon) (bool, error) { + var isLeader bool + leaderAddress, err := d.gateway.LeaderAddress() + if err != nil { + if errors.Is(err, cluster.ErrNodeIsNotClustered) { + isLeader = true + } else { + return false, err + } + } else if leaderAddress == d.localConfig.ClusterAddress() { + isLeader = true + } + + return isLeader, nil +} + // Create a database connection and perform any updates needed. func initializeDbObject(d *Daemon) error { logger.Info("Initializing local database") diff --git a/lxd/db/cluster/auth_group_permissions.go b/lxd/db/cluster/auth_group_permissions.go new file mode 100644 index 000000000000..276c18129246 --- /dev/null +++ b/lxd/db/cluster/auth_group_permissions.go @@ -0,0 +1,131 @@ +package cluster + +import ( + "context" + "database/sql" + "fmt" + "net/http" + + "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" +) + +// AuthGroupPermission is the database representation of an api.Permission. +type AuthGroupPermission struct { + ID int + GroupID int + Entitlement auth.Entitlement + EntityType EntityType + EntityID int +} + +// GetPermissionEntityURLs accepts a slice of AuthGroupPermission as input. The input AuthGroupPermission slice may include permissions +// that are no longer valid because the entity against which they are defined no longer exists. This methon determines +// which permissions are valid and which are not valid by attempting to retrieve their entity URL. It uses as few +// queries as possible to do this. It returns a slice of valid permissions, a slice of invalid permissions and a map of +// entity.Type, to entity ID, to api.URL. The returned map contains the URL of the entity of each returned valid +// permission. It is used for populating api.Permission. The invalid permissions can be ignored or deleted. +func GetPermissionEntityURLs(ctx context.Context, tx *sql.Tx, permissions []AuthGroupPermission) (validPermissions []AuthGroupPermission, danglingPermissions []AuthGroupPermission, entityURLs map[entity.Type]map[int]*api.URL, err error) { + // To make as few calls as possible, categorize the permissions by entity type. + permissionsByEntityType := map[EntityType][]AuthGroupPermission{} + for _, permission := range permissions { + permissionsByEntityType[permission.EntityType] = append(permissionsByEntityType[permission.EntityType], permission) + } + + type e struct { + t EntityType + id int + } + + done := make(map[e]struct{}) + + // 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 + } + + _, ok := done[e{t: entityType, id: permissions[0].EntityID}] + if ok { + continue + } + + u, err := GetEntityURL(ctx, tx, entity.Type(entityType), permissions[0].EntityID) + if err != nil && !api.StatusErrorCheck(err, http.StatusNotFound) { + return nil, nil, nil, err + } else if err != nil { + continue + } + + entityURLs[entity.Type(entityType)] = make(map[int]*api.URL) + entityURLs[entity.Type(entityType)][permissions[0].EntityID] = u + done[e{t: entityType, id: permissions[0].EntityID}] = struct{}{} + } + + // 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, nil, nil, err + } + + for k, v := range entityURLsAll { + entityURLs[k] = v + } + } + + // Iterate over the input permissions and check which ones are present in the entityURLs map. + // If they are not present, the entity against which they are defined is no longer present in the DB. + for _, permission := range permissions { + entityIDToURL, ok := entityURLs[entity.Type(permission.EntityType)] + if !ok { + danglingPermissions = append(danglingPermissions, permission) + continue + } + + _, ok = entityIDToURL[permission.EntityID] + if !ok { + danglingPermissions = append(danglingPermissions, permission) + continue + } + + validPermissions = append(validPermissions, permission) + } + + return validPermissions, danglingPermissions, entityURLs, nil +} + +// DeletePermissions deletes the given permissions. +func DeletePermissions(ctx context.Context, tx *sql.Tx, danglingPermissions []AuthGroupPermission) error { + if len(danglingPermissions) == 0 { + return nil + } + + args := make([]any, 0, len(danglingPermissions)) + for _, perm := range danglingPermissions { + args = append(args, perm.ID) + } + + q := fmt.Sprintf(`DELETE FROM auth_group_permissions WHERE auth_group_permissions.id IN %s`, query.Params(len(danglingPermissions))) + res, err := tx.ExecContext(ctx, q, args...) + if err != nil { + return fmt.Errorf("Failed to clean up dangling permissions: %w", err) + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("Failed to validate clean up dangling permissions: %w", err) + } + + if len(args) != int(rowsAffected) { + return fmt.Errorf("Failed to delete expected number of dangling permissions on clean up (expected %d, got %d)", len(args), rowsAffected) + } + + return nil +} diff --git a/lxd/db/cluster/auth_groups.go b/lxd/db/cluster/auth_groups.go index 1561cc0c76f9..384db84e3769 100644 --- a/lxd/db/cluster/auth_groups.go +++ b/lxd/db/cluster/auth_groups.go @@ -8,6 +8,7 @@ import ( "github.com/canonical/lxd/lxd/db/query" "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/entity" + "github.com/canonical/lxd/shared/logger" ) // Code generation directives. @@ -55,16 +56,24 @@ func (g *AuthGroup) ToAPI(ctx context.Context, tx *sql.Tx) (*api.AuthGroup, erro }, } - permissions, err := GetPermissionsByAuthGroupID(ctx, tx, g.ID) + permissions, err := GetAuthGroupPermissionsByGroupID(ctx, tx, g.ID) if err != nil { return nil, err } - entityURLs, err := GetPermissionEntityURLs(ctx, tx, permissions) + permissions, danglingPermissions, entityURLs, err := GetPermissionEntityURLs(ctx, tx, permissions) if err != nil { return nil, err } + if len(danglingPermissions) > 0 { + logger.Warn("Auth group permissions table references entities that no longer exist. Attempting clean up...") + err := DeletePermissions(ctx, tx, danglingPermissions) + if err != nil { + logger.Error("Failed to delete orphaned permissions", logger.Ctx{"error": err}) + } + } + apiPermissions := make([]api.Permission, 0, len(permissions)) for _, p := range permissions { entityURLs, ok := entityURLs[entity.Type(p.EntityType)] @@ -229,17 +238,12 @@ JOIN auth_groups_identity_provider_groups ON identity_provider_groups.id = auth_ 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 +// GetAuthGroupPermissionsByGroupID returns the permissions that belong to the group with the given ID. +func GetAuthGroupPermissionsByGroupID(ctx context.Context, tx *sql.Tx, groupID int) ([]AuthGroupPermission, error) { + var result []AuthGroupPermission dest := func(scan func(dest ...any) error) error { - p := Permission{} - err := scan(&p.ID, &p.Entitlement, &p.EntityType, &p.EntityID) + p := AuthGroupPermission{} + err := scan(&p.ID, &p.GroupID, &p.Entitlement, &p.EntityType, &p.EntityID) if err != nil { return err } @@ -248,7 +252,7 @@ WHERE auth_groups_permissions.auth_group_id = ?`, permissionColumns()) return nil } - err := query.Scan(ctx, tx, stmt, dest, groupID) + err := query.Scan(ctx, tx, `SELECT id, auth_group_id, entitlement, entity_type, entity_id FROM auth_groups_permissions WHERE auth_group_id = ?`, dest, groupID) if err != nil { return nil, fmt.Errorf("Failed to get permissions for the group with ID `%d`: %w", groupID, err) } @@ -256,23 +260,19 @@ WHERE auth_groups_permissions.auth_group_id = ?`, permissionColumns()) 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()) +// GetAuthGroupPermissions returns a map of group ID to the permissions that belong to the auth group with that ID. +func GetAuthGroupPermissions(ctx context.Context, tx *sql.Tx) ([]AuthGroupPermission, error) { + stmt := `SELECT id, auth_group_id, entitlement, entity_type, entity_id FROM auth_groups_permissions` - result := make(map[int][]Permission) + var result []AuthGroupPermission dest := func(scan func(dest ...any) error) error { - var groupID int - p := Permission{} - err := scan(&groupID, &p.ID, &p.Entitlement, &p.EntityType, &p.EntityID) + p := AuthGroupPermission{} + err := scan(&p.ID, &p.GroupID, &p.Entitlement, &p.EntityType, &p.EntityID) if err != nil { return err } - result[groupID] = append(result[groupID], p) + result = append(result, p) return nil } @@ -286,18 +286,18 @@ JOIN auth_groups_permissions ON permissions.id = auth_groups_permissions.permiss // 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 { +func SetAuthGroupPermissions(ctx context.Context, tx *sql.Tx, groupID int, authGroupPermissions []AuthGroupPermission) 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 { + if len(authGroupPermissions) == 0 { return nil } - for _, permissionID := range permissionIDs { - _, err := tx.ExecContext(ctx, `INSERT INTO auth_groups_permissions (auth_group_id, permission_id) VALUES (?, ?);`, groupID, permissionID) + for _, permission := range authGroupPermissions { + _, err := tx.ExecContext(ctx, `INSERT INTO auth_groups_permissions (auth_group_id, entity_type, entity_id, entitlement) VALUES (?, ?, ?, ?);`, permission.GroupID, permission.EntityType, permission.EntityID, permission.Entitlement) if err != nil { return fmt.Errorf("Failed to write group permissions: %w", err) } diff --git a/lxd/db/cluster/entities.go b/lxd/db/cluster/entities.go index 6883a9617e18..4a0e138f0874 100644 --- a/lxd/db/cluster/entities.go +++ b/lxd/db/cluster/entities.go @@ -915,9 +915,9 @@ WHERE projects.name = ? // 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 +SELECT ?, storage_volumes_snapshots.id +FROM storage_volumes_snapshots +JOIN storage_volumes ON storage_volumes_snapshots.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 @@ -931,7 +931,7 @@ WHERE projects.name = ? WHEN %d THEN '%s' END = ? AND storage_volumes.name = ? - AND storage_volumes_backups.name = ? + AND storage_volumes_snapshots.name = ? `, StoragePoolVolumeTypeContainer, StoragePoolVolumeTypeNameContainer, StoragePoolVolumeTypeImage, StoragePoolVolumeTypeNameImage, StoragePoolVolumeTypeCustom, StoragePoolVolumeTypeNameCustom, StoragePoolVolumeTypeVM, StoragePoolVolumeTypeNameVM) // warningIDFromURL gets the ID of a warning from its URL. @@ -1138,3 +1138,351 @@ func PopulateEntityReferencesFromURLs(ctx context.Context, tx *sql.Tx, entityURL return nil } + +var entityDeletionTriggers = map[entity.Type]string{ + entity.TypeImage: imageDeletionTrigger, + entity.TypeProfile: profileDeletionTrigger, + entity.TypeProject: projectDeletionTrigger, + entity.TypeInstance: instanceDeletionTrigger, + entity.TypeInstanceBackup: instanceBackupDeletionTrigger, + entity.TypeInstanceSnapshot: instanceSnapshotDeletionTrigger, + entity.TypeNetwork: networkDeletionTrigger, + entity.TypeNetworkACL: networkACLDeletionTrigger, + entity.TypeNode: nodeDeletionTrigger, + entity.TypeOperation: operationDeletionTrigger, + entity.TypeStoragePool: storagePoolDeletionTrigger, + entity.TypeStorageVolume: storageVolumeDeletionTrigger, + entity.TypeStorageVolumeBackup: storageVolumeBackupDeletionTrigger, + entity.TypeStorageVolumeSnapshot: storageVolumeSnapshotDeletionTrigger, + entity.TypeWarning: warningDeletionTrigger, + entity.TypeClusterGroup: clusterGroupDeletionTrigger, + entity.TypeStorageBucket: storageBucketDeletionTrigger, + entity.TypeImageAlias: imageAliasDeletionTrigger, + entity.TypeNetworkZone: networkZoneDeletionTrigger, + entity.TypeAuthGroup: authGroupDeletionTrigger, + entity.TypeIdentityProviderGroup: identityProviderGroupDeletionTrigger, + entity.TypeIdentity: identityDeletionTrigger, +} + +var imageDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_image_delete; +CREATE TRIGGER on_image_delete + AFTER DELETE ON images + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeImage, entityTypeImage) + +// entityTypeProfile int64 = 2 +var profileDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_profile_delete; +CREATE TRIGGER on_profile_delete + AFTER DELETE ON profiles + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeProfile, entityTypeProfile) + +// entityTypeProject int64 = 3 +var projectDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_project_delete; +CREATE TRIGGER on_project_delete + AFTER DELETE ON projects + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeProject, entityTypeProject) + +// entityTypeInstance int64 = 5 +var instanceDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_instance_delete; +CREATE TRIGGER on_instance_delete + AFTER DELETE ON instances + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeInstance, entityTypeInstance) + +// entityTypeInstanceBackup int64 = 6 +var instanceBackupDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_instance_backup_delete; +CREATE TRIGGER on_instance_backup_delete + AFTER DELETE ON instances_backups + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeInstanceBackup, entityTypeInstanceBackup) + +// entityTypeInstanceSnapshot int64 = 7 +var instanceSnapshotDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_instance_snaphot_delete; +CREATE TRIGGER on_instance_snaphot_delete + AFTER DELETE ON instances_snapshots + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeInstanceSnapshot, entityTypeInstanceSnapshot) + +// entityTypeNetwork int64 = 8 +var networkDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_network_delete; +CREATE TRIGGER on_network_delete + AFTER DELETE ON networks + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeNetwork, entityTypeNetwork) + +// entityTypeNetworkACL int64 = 9 +var networkACLDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_network_acl_delete; +CREATE TRIGGER on_network_acl_delete + AFTER DELETE ON networks_acls + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeNetworkACL, entityTypeNetworkACL) + +// entityTypeNode int64 = 10 +var nodeDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_node_delete; +CREATE TRIGGER on_node_delete + AFTER DELETE ON nodes + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeNode, entityTypeNode) + +// entityTypeOperation int64 = 11 +var operationDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_operation_delete; +CREATE TRIGGER on_operation_delete + AFTER DELETE ON operations + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeOperation, entityTypeOperation) + +// entityTypeStoragePool int64 = 12 +var storagePoolDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_storage_pool_delete; +CREATE TRIGGER on_storage_pool_delete + AFTER DELETE ON storage_pools + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeStoragePool, entityTypeStoragePool) + +// entityTypeStorageVolume int64 = 13 +var storageVolumeDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_storage_volume_delete; +CREATE TRIGGER on_storage_volume_delete + AFTER DELETE ON storage_volumes + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeStorageVolume, entityTypeStorageVolume) + +// entityTypeStorageVolumeBackup int64 = 14 +var storageVolumeBackupDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_storage_volume_backup_delete; +CREATE TRIGGER on_storage_volume_backup_delete + AFTER DELETE ON storage_volumes_backups + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeStorageVolumeBackup, entityTypeStorageVolumeBackup) + +// entityTypeStorageVolumeSnapshot int64 = 15 +var storageVolumeSnapshotDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_storage_volume_snapshot_delete; +CREATE TRIGGER on_storage_volume_snapshot_delete + AFTER DELETE ON storage_volumes_snapshots + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeStorageVolumeSnapshot, entityTypeStorageVolumeSnapshot) + +// entityTypeWarning int64 = 16 +var warningDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_warning_delete; +CREATE TRIGGER on_warning_delete + AFTER DELETE ON warnings + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + END +`, entityTypeWarning) + +// entityTypeClusterGroup int64 = 17 +var clusterGroupDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_cluster_group_delete; +CREATE TRIGGER on_cluster_group_delete + AFTER DELETE ON cluster_groups + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeClusterGroup, entityTypeClusterGroup) + +// entityTypeStorageBucket int64 = 18 +var storageBucketDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_storage_bucket_delete; +CREATE TRIGGER on_storage_bucket_delete + AFTER DELETE ON storage_buckets + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeStorageBucket, entityTypeStorageBucket) + +// entityTypeNetworkZone int64 = 19 +var networkZoneDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_network_zone_delete; +CREATE TRIGGER on_network_zone_delete + AFTER DELETE ON networks_zones + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeNetworkZone, entityTypeNetworkZone) + +// entityTypeImageAlias int64 = 20 +var imageAliasDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_image_alias_delete; +CREATE TRIGGER on_image_alias_delete + AFTER DELETE ON images_aliases + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeImageAlias, entityTypeImageAlias) + +// entityTypeAuthGroup int64 = 22 +var authGroupDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_auth_group_delete; +CREATE TRIGGER on_auth_group_delete + AFTER DELETE ON auth_groups + BEGIN + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeAuthGroup) + +// entityTypeIdentityProviderGroup int64 = 23 +var identityProviderGroupDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_identity_provider_group_delete; +CREATE TRIGGER on_identity_provider_group_delete + AFTER DELETE ON identity_provider_groups + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeIdentityProviderGroup, entityTypeIdentityProviderGroup) + +// entityTypeIdentity int64 = 24 +var identityDeletionTrigger = fmt.Sprintf(` +DROP TRIGGER IF EXISTS on_identity_delete; +CREATE TRIGGER on_identity_delete + AFTER DELETE ON identities + BEGIN + DELETE FROM auth_groups_permissions + WHERE entity_type = %d + AND entity_id = OLD.id; + DELETE FROM warnings + WHERE entity_type_code = %d + AND entity_id = OLD.ID; + END +`, entityTypeIdentity, entityTypeIdentity) diff --git a/lxd/db/cluster/permissions.go b/lxd/db/cluster/permissions.go deleted file mode 100644 index b3621f92c111..000000000000 --- a/lxd/db/cluster/permissions.go +++ /dev/null @@ -1,113 +0,0 @@ -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. -// -//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 -} - -// 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 -} diff --git a/lxd/db/cluster/permissions.interface.mapper.go b/lxd/db/cluster/permissions.interface.mapper.go deleted file mode 100644 index a2654146d21b..000000000000 --- a/lxd/db/cluster/permissions.interface.mapper.go +++ /dev/null @@ -1,21 +0,0 @@ -//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 deleted file mode 100644 index 1f290a4d315e..000000000000 --- a/lxd/db/cluster/permissions.mapper.go +++ /dev/null @@ -1,269 +0,0 @@ -//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") - } -} diff --git a/lxd/db/cluster/schema.go b/lxd/db/cluster/schema.go index 77d185960e4b..c0a7b9565f2c 100644 --- a/lxd/db/cluster/schema.go +++ b/lxd/db/cluster/schema.go @@ -23,10 +23,11 @@ CREATE TABLE auth_groups_identity_provider_groups ( CREATE TABLE auth_groups_permissions ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, auth_group_id INTEGER NOT NULL, - permission_id INTEGER NOT NULL, + entity_type INTEGER NOT NULL, + entity_id INTEGER NOT NULL, + entitlement TEXT NOT NULL, FOREIGN KEY (auth_group_id) REFERENCES auth_groups (id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions (id) ON DELETE CASCADE, - UNIQUE (auth_group_id, permission_id) + UNIQUE (auth_group_id, entity_type, entitlement, entity_id) ); CREATE TABLE "cluster_groups" ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, @@ -433,13 +434,6 @@ 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, @@ -668,5 +662,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 (72, strftime("%s")) +INSERT INTO schema (version, updated_at) VALUES (73, strftime("%s")) ` diff --git a/lxd/db/cluster/stmt.go b/lxd/db/cluster/stmt.go index b36d25daac30..711e5e85c878 100644 --- a/lxd/db/cluster/stmt.go +++ b/lxd/db/cluster/stmt.go @@ -3,6 +3,7 @@ package cluster import ( + "context" "database/sql" "fmt" ) @@ -60,3 +61,14 @@ func StmtString(code int) (string, error) { return stmt, nil } + +func PrepareTriggers(ctx context.Context, tx *sql.Tx) error { + for _, triggerStmt := range entityDeletionTriggers { + _, err := tx.ExecContext(ctx, triggerStmt) + if err != nil { + return err + } + } + + return nil +} diff --git a/lxd/db/cluster/update.go b/lxd/db/cluster/update.go index 2e15b3467df6..bea9bcce4fbb 100644 --- a/lxd/db/cluster/update.go +++ b/lxd/db/cluster/update.go @@ -109,6 +109,28 @@ var updates = map[int]schema.Update{ 70: updateFromV69, 71: updateFromV70, 72: updateFromV71, + 73: updateFromV72, +} + +func updateFromV72(ctx context.Context, tx *sql.Tx) error { + _, err := tx.ExecContext(ctx, ` +DROP TABLE permissions; +DROP TABLE auth_groups_permissions; +CREATE TABLE auth_groups_permissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + auth_group_id INTEGER NOT NULL, + entity_type INTEGER NOT NULL, + entity_id INTEGER NOT NULL, + entitlement TEXT NOT NULL, + FOREIGN KEY (auth_group_id) REFERENCES auth_groups (id) ON DELETE CASCADE, + UNIQUE (auth_group_id, entity_type, entitlement, entity_id) +); +`) + if err != nil { + return err + } + + return nil } func updateFromV71(ctx context.Context, tx *sql.Tx) error { diff --git a/lxd/permissions.go b/lxd/permissions.go index b5d871c9c52f..2d62e20d5c55 100644 --- a/lxd/permissions.go +++ b/lxd/permissions.go @@ -3,14 +3,13 @@ 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" + "net/http" ) var permissionsCmd = APIEndpoint{ @@ -114,7 +113,7 @@ var permissionsCmd = APIEndpoint{ // type: array // description: List of permissions // items: -// $ref: "#/definitions/Permission" +// $ref: "#/definitions/AuthGroupPermission" // "403": // $ref: "#/responses/Forbidden" // "500": @@ -135,8 +134,8 @@ func getPermissions(d *Daemon, r *http.Request) response.Response { } var entityURLs map[entity.Type]map[int]*api.URL - var permissions []cluster.Permission - var groupsByPermissionID map[int][]cluster.AuthGroup + var groups []cluster.AuthGroup + var authGroupPermissions []cluster.AuthGroupPermission err := d.State().DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { var err error if projectNameFilter != "" { @@ -148,14 +147,20 @@ func getPermissions(d *Daemon, r *http.Request) response.Response { } if recursion == "1" { - permissions, err = cluster.GetPermissions(ctx, tx.Tx()) + groups, err = cluster.GetAuthGroups(ctx, tx.Tx()) + if err != nil { + return fmt.Errorf("Failed to get groups: %w", err) + } + + authGroupPermissions, err = cluster.GetAuthGroupPermissions(ctx, tx.Tx()) if err != nil { return fmt.Errorf("Failed to get currently assigned permissions: %w", err) } - groupsByPermissionID, err = cluster.GetAllAuthGroupsByPermissionID(ctx, tx.Tx()) + // Call GetPermissionEntityURLs to ensure only valid permissions are returned. + authGroupPermissions, _, _, err = cluster.GetPermissionEntityURLs(ctx, tx.Tx(), authGroupPermissions) if err != nil { - return fmt.Errorf("Failed to get groups by permission mapping: %w", err) + return fmt.Errorf("Failed to get entity URLs for permissions: %w", err) } } @@ -170,28 +175,20 @@ func getPermissions(d *Daemon, r *http.Request) response.Response { 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)) + assignedPermissions := make(map[cluster.AuthGroupPermission][]string, len(authGroupPermissions)) 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) - } + groupNames := make(map[int]string, len(groups)) + for _, group := range groups { + groupNames[group.ID] = group.Name + } - assignedPermissions[perm] = groupNames + for _, perm := range authGroupPermissions { + // A permission is unique via its entity ID, entity type, and entitlement. Set the permission ID and group ID + // to zero so that we can create a map key from the entityURL map below. + groupName := groupNames[perm.GroupID] + perm.ID = 0 + perm.GroupID = 0 + assignedPermissions[perm] = append(assignedPermissions[perm], groupName) } } @@ -212,9 +209,9 @@ func getPermissions(d *Daemon, r *http.Request) response.Response { 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{ + // Get the groups from the assigned permissions map. We don't have the permission ID or group ID + // in scope here. That's why we set it to zero above. + Groups: assignedPermissions[cluster.AuthGroupPermission{ Entitlement: entitlement, EntityType: cluster.EntityType(entityType), EntityID: entityID,