Skip to content

Commit

Permalink
Merge pull request #13703 from markylaing/auth-server-admin-handling
Browse files Browse the repository at this point in the history
Auth: Use util for determining if the caller is a server administrator
  • Loading branch information
tomponline authored Jul 5, 2024
2 parents 66da824 + 365078f commit 50fdcee
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 185 deletions.
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 {
// 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

0 comments on commit 50fdcee

Please sign in to comment.