Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth: Use util for determining if the caller is a server administrator #13703

Merged
merged 8 commits into from
Jul 5, 2024
94 changes: 17 additions & 77 deletions lxd/auth/drivers/openfga.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (e *embeddedOpenFGA) CheckPermission(ctx context.Context, entityURL *api.UR
return api.StatusErrorf(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}

isRoot, err := auth.IsRootUserFromCtx(ctx)
isRoot, err := auth.IsServerAdmin(ctx, e.identityCache)
if err != nil {
return fmt.Errorf("Failed to check caller privilege: %w", err)
}
Expand All @@ -133,43 +133,22 @@ func (e *embeddedOpenFGA) CheckPermission(ctx context.Context, entityURL *api.UR
return nil
}

username, err := auth.GetUsernameFromCtx(ctx)
id, err := auth.GetIdentityFromCtx(ctx, e.identityCache)
if err != nil {
return fmt.Errorf("Failed to get caller username: %w", err)
return fmt.Errorf("Failed to get caller identity: %w", err)
}

authenticationMethod, err := auth.GetAuthenticationMethodFromCtx(ctx)
if err != nil {
return fmt.Errorf("Failed to get caller authentication method: %w", err)
}

logCtx["username"] = username
logCtx["protocol"] = authenticationMethod
logCtx["username"] = id.Identifier
logCtx["protocol"] = id.AuthenticationMethod
l := e.logger.AddContext(logCtx)

// If the authentication method was TLS, use the TLS driver instead.
if authenticationMethod == api.AuthenticationMethodTLS || authenticationMethod == auth.AuthenticationMethodPKI {
if id.AuthenticationMethod == api.AuthenticationMethodTLS {
return e.tlsAuthorizer.CheckPermission(ctx, entityURL, entitlement)
}

// Get the identity.
identityCacheEntry, err := e.identityCache.Get(authenticationMethod, username)
if err != nil {
return fmt.Errorf("Failed loading identity for %q: %w", username, err)
}

// If the identity type is not restricted, allow all (TLS authorization compatibility).
isRestricted, err := identity.IsRestrictedIdentityType(identityCacheEntry.IdentityType)
if err != nil {
return fmt.Errorf("Failed to check restricted status for %q: %w", username, err)
}

if !isRestricted {
return nil
}

// Combine the users LXD groups with any mappings that have come from the IDP.
groups := identityCacheEntry.Groups
groups := id.Groups
idpGroups, err := auth.GetIdentityProviderGroupsFromCtx(ctx)
if err != nil {
return fmt.Errorf("Failed to get caller identity provider groups: %w", err)
Expand All @@ -196,7 +175,7 @@ func (e *embeddedOpenFGA) CheckPermission(ctx context.Context, entityURL *api.UR
return fmt.Errorf("Authorization driver failed to parse entity URL %q: %w", entityURL.String(), err)
}

userObject := fmt.Sprintf("%s:%s", entity.TypeIdentity, entity.IdentityURL(authenticationMethod, username).String())
userObject := fmt.Sprintf("%s:%s", entity.TypeIdentity, entity.IdentityURL(id.AuthenticationMethod, id.Identifier).String())
entityObject := fmt.Sprintf("%s:%s", entityType, entityURL.String())

// Construct an OpenFGA check request.
Expand Down Expand Up @@ -228,15 +207,6 @@ func (e *embeddedOpenFGA) CheckPermission(ctx context.Context, entityURL *api.UR
})
}

// For each project, append a contextual tuple to set make the identity an operator of that project (TLS authorization compatibility).
for _, projectName := range identityCacheEntry.Projects {
req.ContextualTuples.TupleKeys = append(req.ContextualTuples.TupleKeys, &openfgav1.TupleKey{
User: userObject,
Relation: string(auth.EntitlementOperator),
Object: fmt.Sprintf("%s:%s", entity.TypeProject, entity.ProjectURL(projectName).String()),
})
}

// Perform the check.
l.Debug("Checking OpenFGA relation")
resp, err := e.server.Check(ctx, req)
Expand Down Expand Up @@ -324,7 +294,7 @@ func (e *embeddedOpenFGA) GetPermissionChecker(ctx context.Context, entitlement
return allowFunc(false), nil
}

isRoot, err := auth.IsRootUserFromCtx(ctx)
isRoot, err := auth.IsServerAdmin(ctx, e.identityCache)
if err != nil {
return nil, fmt.Errorf("Failed to check caller privilege: %w", err)
}
Expand All @@ -334,43 +304,22 @@ func (e *embeddedOpenFGA) GetPermissionChecker(ctx context.Context, entitlement
return allowFunc(true), nil
}

username, err := auth.GetUsernameFromCtx(ctx)
id, err := auth.GetIdentityFromCtx(ctx, e.identityCache)
if err != nil {
return nil, fmt.Errorf("Failed to get caller username: %w", err)
return nil, fmt.Errorf("Failed to get caller identity: %w", err)
}

authenticationMethod, err := auth.GetAuthenticationMethodFromCtx(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to get caller authentication method: %w", err)
}

logCtx["username"] = username
logCtx["protocol"] = authenticationMethod
logCtx["username"] = id.Identifier
logCtx["protocol"] = id.AuthenticationMethod
l := e.logger.AddContext(logCtx)

// If the authentication method was TLS, use the TLS driver instead.
if authenticationMethod == api.AuthenticationMethodTLS || authenticationMethod == auth.AuthenticationMethodPKI {
if id.AuthenticationMethod == api.AuthenticationMethodTLS {
return e.tlsAuthorizer.GetPermissionChecker(ctx, entitlement, entityType)
}

// Get the identity.
identityCacheEntry, err := e.identityCache.Get(authenticationMethod, username)
if err != nil {
return nil, fmt.Errorf("Failed loading identity for %q: %w", username, err)
}

// If the identity type is not restricted, allow all (TLS authorization compatibility).
isRestricted, err := identity.IsRestrictedIdentityType(identityCacheEntry.IdentityType)
if err != nil {
return nil, fmt.Errorf("Failed to check restricted status for %q: %w", username, err)
}

if !isRestricted {
return allowFunc(true), nil
}

// Combine the users LXD groups with any mappings that have come from the IDP.
groups := identityCacheEntry.Groups
groups := id.Groups
idpGroups, err := auth.GetIdentityProviderGroupsFromCtx(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to get caller identity provider groups: %w", err)
Expand All @@ -392,7 +341,7 @@ func (e *embeddedOpenFGA) GetPermissionChecker(ctx context.Context, entitlement
}

// Construct an OpenFGA list objects request.
userObject := fmt.Sprintf("%s:%s", entity.TypeIdentity, entity.IdentityURL(authenticationMethod, username).String())
userObject := fmt.Sprintf("%s:%s", entity.TypeIdentity, entity.IdentityURL(id.AuthenticationMethod, id.Identifier).String())
req := &openfgav1.ListObjectsRequest{
StoreId: dummyDatastoreULID,
Type: entityType.String(),
Expand All @@ -419,15 +368,6 @@ func (e *embeddedOpenFGA) GetPermissionChecker(ctx context.Context, entitlement
})
}

// For each project, append a contextual tuple to set make the identity an operator of that project (TLS authorization compatibility).
for _, projectName := range identityCacheEntry.Projects {
req.ContextualTuples.TupleKeys = append(req.ContextualTuples.TupleKeys, &openfgav1.TupleKey{
User: userObject,
Relation: string(auth.EntitlementOperator),
Object: fmt.Sprintf("%s:%s", entity.TypeProject, entity.ProjectURL(projectName).String()),
})
}

// Perform the request.
l.Debug("Listing related objects for user")
resp, err := e.server.ListObjects(ctx, req)
Expand All @@ -439,7 +379,7 @@ func (e *embeddedOpenFGA) GetPermissionChecker(ctx context.Context, entitlement
err = openFGAInternalError.Internal()
}

return nil, fmt.Errorf("Failed to list OpenFGA objects of type %q with entitlement %q for user %q: %w", entityType.String(), entitlement, username, err)
return nil, fmt.Errorf("Failed to list OpenFGA objects of type %q with entitlement %q for user %q: %w", entityType.String(), entitlement, id.Identifier, err)
}

objects := resp.GetObjects()
Expand Down
66 changes: 8 additions & 58 deletions lxd/auth/drivers/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (t *tls) CheckPermission(ctx context.Context, entityURL *api.URL, entitleme
return api.StatusErrorf(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}

isRoot, err := auth.IsRootUserFromCtx(ctx)
isRoot, err := auth.IsServerAdmin(ctx, t.identities)
if err != nil {
return fmt.Errorf("Failed to check caller privilege: %w", err)
}
Expand All @@ -56,37 +56,12 @@ func (t *tls) CheckPermission(ctx context.Context, entityURL *api.URL, entitleme
return nil
}

authenticationMethod, err := auth.GetAuthenticationMethodFromCtx(ctx)
id, err := auth.GetIdentityFromCtx(ctx, t.identities)
if err != nil {
return fmt.Errorf("Failed to get caller authentication method: %w", err)
return fmt.Errorf("Failed to get caller identity: %w", err)
}

if authenticationMethod != api.AuthenticationMethodTLS && authenticationMethod != auth.AuthenticationMethodPKI {
return api.StatusErrorf(http.StatusInternalServerError, "Authentication protocol %q is not compatible with authorization driver", authenticationMethod)
}

username, err := auth.GetUsernameFromCtx(ctx)
if err != nil {
return fmt.Errorf("Failed to get caller username: %w", err)
}

id, err := t.identities.Get(api.AuthenticationMethodTLS, username)
if err != nil {
if authenticationMethod == auth.AuthenticationMethodPKI && api.StatusErrorCheck(err, http.StatusNotFound) {
return nil
}

return fmt.Errorf("Failed loading certificate for %q: %w", username, err)
}

isRestricted, err := identity.IsRestrictedIdentityType(id.IdentityType)
if err != nil {
return fmt.Errorf("Failed to check restricted status of identity: %w", err)
}

if !isRestricted {
return nil
} else if id.IdentityType == api.IdentityTypeCertificateMetricsUnrestricted && entitlement == auth.EntitlementCanViewMetrics {
if id.IdentityType == api.IdentityTypeCertificateMetricsUnrestricted && entitlement == auth.EntitlementCanViewMetrics {
return nil
}

Expand Down Expand Up @@ -140,7 +115,7 @@ func (t *tls) GetPermissionChecker(ctx context.Context, entitlement auth.Entitle
return allowFunc(false), nil
}

isRoot, err := auth.IsRootUserFromCtx(ctx)
isRoot, err := auth.IsServerAdmin(ctx, t.identities)
if err != nil {
return nil, fmt.Errorf("Failed to check caller privilege: %w", err)
}
Expand All @@ -149,37 +124,12 @@ func (t *tls) GetPermissionChecker(ctx context.Context, entitlement auth.Entitle
return allowFunc(true), nil
}

authenticationMethod, err := auth.GetAuthenticationMethodFromCtx(ctx)
id, err := auth.GetIdentityFromCtx(ctx, t.identities)
if err != nil {
return nil, fmt.Errorf("Failed to get caller authentication method: %w", err)
return nil, fmt.Errorf("Failed to get caller identity: %w", err)
}

if authenticationMethod != api.AuthenticationMethodTLS && authenticationMethod != auth.AuthenticationMethodPKI {
return nil, api.StatusErrorf(http.StatusInternalServerError, "Authentication protocol %q is not compatible with authorization driver", authenticationMethod)
}

username, err := auth.GetUsernameFromCtx(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to get caller username: %w", err)
}

id, err := t.identities.Get(api.AuthenticationMethodTLS, username)
if err != nil {
if authenticationMethod == auth.AuthenticationMethodPKI && api.StatusErrorCheck(err, http.StatusNotFound) {
return allowFunc(true), nil
}

return nil, fmt.Errorf("Failed loading certificate for %q: %w", username, err)
}

isRestricted, err := identity.IsRestrictedIdentityType(id.IdentityType)
if err != nil {
return nil, fmt.Errorf("Failed to check restricted status of identity: %w", err)
}

if !isRestricted {
return allowFunc(true), nil
} else if id.IdentityType == api.IdentityTypeCertificateMetricsUnrestricted && entitlement == auth.EntitlementCanViewMetrics {
if id.IdentityType == api.IdentityTypeCertificateMetricsUnrestricted && entitlement == auth.EntitlementCanViewMetrics {
return allowFunc(true), nil
}

Expand Down
58 changes: 43 additions & 15 deletions lxd/auth/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,14 @@ package auth

import (
"context"
"fmt"
"net/http"

"github.com/canonical/lxd/lxd/identity"
"github.com/canonical/lxd/lxd/request"
"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"
)

// 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
}

// IsTrusted returns true if the value for `request.CtxTrusted` is set and is true.
func IsTrusted(ctx context.Context) bool {
// The zero-value of a bool is false, so even if it isn't present in the context we'll return false.
Expand All @@ -27,9 +18,9 @@ func IsTrusted(ctx context.Context) bool {
return trusted
}

// IsRootUserFromCtx inspects the context and returns true if the request was made from
// the unix socket or initiated by another cluster member.
func IsRootUserFromCtx(ctx context.Context) (bool, error) {
// IsServerAdmin inspects the context and returns true if the request was made over the unix socket, initiated by
// another cluster member, or sent by a client with an unrestricted certificate.
func IsServerAdmin(ctx context.Context, identityCache *identity.Cache) (bool, error) {
method, err := GetAuthenticationMethodFromCtx(ctx)
if err != nil {
return false, err
Expand All @@ -40,7 +31,44 @@ func IsRootUserFromCtx(ctx context.Context) (bool, error) {
return true, nil
}

return false, nil
id, err := GetIdentityFromCtx(ctx, identityCache)
if err != nil {
tomponline marked this conversation as resolved.
Show resolved Hide resolved
// AuthenticationMethodPKI is only set as the value of request.CtxProtocol when `core.trust_ca_certificates` is
// true. This setting grants full access to LXD for all clients with CA-signed certificates.
if method == AuthenticationMethodPKI && api.StatusErrorCheck(err, http.StatusNotFound) {
return true, nil
}

return false, fmt.Errorf("Failed to get caller identity: %w", err)
}

isRestricted, err := identity.IsRestrictedIdentityType(id.IdentityType)
if err != nil {
return false, fmt.Errorf("Failed to check restricted status of identity: %w", err)
}

return !isRestricted, nil
}

// GetIdentityFromCtx returns the identity.CacheEntry for the current authenticated caller.
func GetIdentityFromCtx(ctx context.Context, identityCache *identity.Cache) (*identity.CacheEntry, error) {
authenticationMethod, err := GetAuthenticationMethodFromCtx(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to get caller authentication method: %w", err)
}

// If the caller authenticated via a CA-signed certificate and `core.trust_ca_certificates` is enabled. We still
// want to check for any potential trust store entries corresponding to their certificate fingerprint.
if authenticationMethod == AuthenticationMethodPKI {
authenticationMethod = api.AuthenticationMethodTLS
}

username, err := GetUsernameFromCtx(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to get caller username: %w", err)
}

return identityCache.Get(authenticationMethod, username)
}

// GetUsernameFromCtx inspects the context and returns the username of the initial caller.
Expand Down
Loading
Loading