Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
markylaing committed Feb 29, 2024
1 parent 542eae9 commit e97686d
Show file tree
Hide file tree
Showing 12 changed files with 648 additions and 550 deletions.
114 changes: 41 additions & 73 deletions lxd/auth_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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),
})
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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}
Expand All @@ -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
}
29 changes: 29 additions & 0 deletions lxd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {

Check failure on line 2011 in lxd/daemon.go

View workflow job for this annotation

GitHub Actions / Code

early-return: if c { ... } else { ... return } can be simplified to if !c { ... return } ... (revive)
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")
Expand Down
131 changes: 131 additions & 0 deletions lxd/db/cluster/auth_group_permissions.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit e97686d

Please sign in to comment.