diff --git a/lxd/auth/drivers/openfga.go b/lxd/auth/drivers/openfga.go index 1a3eb3aed890..a85bea3a3732 100644 --- a/lxd/auth/drivers/openfga.go +++ b/lxd/auth/drivers/openfga.go @@ -476,6 +476,77 @@ func (e *embeddedOpenFGA) GetPermissionChecker(ctx context.Context, entitlement }, nil } +// GetEntitlements gets entitlements to an entity using the embedded OpenFGA server. +func (e *embeddedOpenFGA) GetEntitlements(ctx context.Context, entityType entity.Type, entityURL *api.URL, queriedEntitlementCandidates []auth.Entitlement) ([]string, error) { + id, err := auth.GetIdentityFromCtx(ctx, e.identityCache) + if err != nil { + return nil, fmt.Errorf("Failed to get caller identity: %w", err) + } + + if !identity.IsFineGrainedIdentityType(id.IdentityType) { + return e.tlsAuthorizer.GetEntitlements(ctx, entityType, entityURL, queriedEntitlementCandidates) + } + + entitlements := make([]string, 0) + for _, entitlement := range queriedEntitlementCandidates { + err = e.CheckPermission(ctx, entityURL, entitlement) + if err != nil { + if err == api.NewGenericStatusError(http.StatusNotFound) || err == api.NewGenericStatusError(http.StatusForbidden) { + continue + } + + return nil, fmt.Errorf("Failed to add entitlements %q for entity URL %q: %w", entitlement, entityURL.String(), err) + } + + entitlements = append(entitlements, string(entitlement)) + } + + return entitlements, nil +} + +// GetEntitlementsToEntities adds entitlements to multiple entities using the embedded OpenFGA server. +func (e *embeddedOpenFGA) GetEntitlementsToEntities(ctx context.Context, entityType entity.Type, entityURLs []*api.URL, queriedEntitlementCandidates []auth.Entitlement) (map[string][]string, error) { + if len(entityURLs) == 0 { + return nil, nil + } + + id, err := auth.GetIdentityFromCtx(ctx, e.identityCache) + if err != nil { + return nil, fmt.Errorf("Failed to get caller identity: %w", err) + } + + if !identity.IsFineGrainedIdentityType(id.IdentityType) { + return e.tlsAuthorizer.GetEntitlementsToEntities(ctx, entityType, entityURLs, queriedEntitlementCandidates) + } + + checkersByEntitlement := make(map[auth.Entitlement]auth.PermissionChecker) + for _, entitlement := range queriedEntitlementCandidates { + checker, err := e.GetPermissionChecker(ctx, entitlement, entityType) + if err != nil { + return nil, fmt.Errorf("Failed to get a permission checker for entitlement %q and for entity type %q: %w", entitlement, entityType, err) + } + + checkersByEntitlement[entitlement] = checker + } + + entities := make(map[string][]string) + for entitlement, checker := range checkersByEntitlement { + for _, url := range entityURLs { + if checker(url) { + urlStr := url.String() + _, ok := entities[urlStr] + if !ok { + entities[urlStr] = make([]string, 0) + } + + entities[urlStr] = append(entities[urlStr], string(entitlement)) + } + } + } + + return entities, nil +} + // openfgaLogger implements OpenFGAs logger.Logger interface but delegates to our logger. type openfgaLogger struct { l logger.Logger diff --git a/lxd/auth/drivers/tls.go b/lxd/auth/drivers/tls.go index c5f8c2029953..3d5b48221fc5 100644 --- a/lxd/auth/drivers/tls.go +++ b/lxd/auth/drivers/tls.go @@ -160,6 +160,16 @@ func (t *tls) GetPermissionChecker(ctx context.Context, entitlement auth.Entitle }, nil } +// GetEntitlements is not supported by the TLS authorization driver. +func (t *tls) GetEntitlements(ctx context.Context, entityType entity.Type, entityURL *api.URL, queriedEntitlementCandidates []auth.Entitlement) ([]string, error) { + return nil, api.StatusErrorf(http.StatusNotImplemented, "Getting entitlements to an entity is not supported by the TLS authorization driver") +} + +// GetEntitlementsToEntities is not supported by the TLS authorization driver. +func (t *tls) GetEntitlementsToEntities(ctx context.Context, entityType entity.Type, entityURLs []*api.URL, queriedEntitlementCandidates []auth.Entitlement) (map[string][]string, error) { + return nil, api.StatusErrorf(http.StatusNotImplemented, "Getting entitlements to a list of entities is not supported by the TLS authorization driver") +} + func (t *tls) allowProjectUnspecificEntityType(entitlement auth.Entitlement, entityType entity.Type, id *identity.CacheEntry, projectName string, pathArguments []string) bool { switch entityType { case entity.TypeServer: diff --git a/lxd/auth/types.go b/lxd/auth/types.go index 9b31f62def6b..78c850f708ed 100644 --- a/lxd/auth/types.go +++ b/lxd/auth/types.go @@ -52,6 +52,12 @@ type Authorizer interface { // the entity. The effective project for the entity must be set in the request context as request.CtxEffectiveProjectName // *before* the call to GetPermissionChecker. GetPermissionChecker(ctx context.Context, entitlement Entitlement, entityType entity.Type) (PermissionChecker, error) + + // GetEntitlements gets entitlements for an entity from a list of entitlement candidates. + GetEntitlements(ctx context.Context, entityType entity.Type, entityURL *api.URL, queriedEntitlementCandidates []Entitlement) ([]string, error) + + // GetEntitlementsToEntities returns the entitlements for multiple entity URLs and types given a list of entitlement candidates. + GetEntitlementsToEntities(ctx context.Context, entityType entity.Type, entityURLs []*api.URL, queriedEntitlementCandidates []Entitlement) (map[string][]string, error) } // IsDeniedError returns true if the error is not found or forbidden. This is because the CheckPermission method on