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: adds option to enable verbose logging during sso #24892

Merged
merged 5 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/24892.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
auth: adds `VerboseLogging` option to auth-method config for debugging SSO
```
3 changes: 3 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,9 @@ type ACLAuthMethodConfig struct {
// (value).
ClaimMappings map[string]string
ListClaimMappings map[string]string
// Enables logging of claims and binding-rule evaluations when
// debug level logging is enabled.
VerboseLogging bool
}

// MarshalJSON implements the json.Marshaler interface and allows
Expand Down
13 changes: 12 additions & 1 deletion lib/auth/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
Expand Down Expand Up @@ -53,7 +54,7 @@ func (b *Bindings) None() bool {
}

// Bind collects the ACL roles and policies to be assigned to the created token.
func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, identity *Identity) (*Bindings, error) {
func (b *Binder) Bind(vlog hclog.Logger, authMethod *structs.ACLAuthMethod, identity *Identity) (*Bindings, error) {
var (
bindings Bindings
err error
Expand All @@ -75,6 +76,9 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, identity *Identity) (*B
rule := raw.(*structs.ACLBindingRule)
if doesSelectorMatch(rule.Selector, identity.Claims) {
matchingRules = append(matchingRules, rule)
vlog.Debug("binding-rule selector matches an identity claim, will evaluate bind-name", "selector", rule.Selector)
} else {
vlog.Debug("bind-rule selector did not match any claims", "selector", rule.Selector)
}
}
if len(matchingRules) == 0 {
Expand Down Expand Up @@ -103,6 +107,9 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, identity *Identity) (*B
bindings.Roles = append(bindings.Roles, &structs.ACLTokenRoleLink{
ID: role.ID,
})
vlog.Debug("role found with name matching ACL binding-rule", "name", bindName)
} else {
vlog.Debug("no role found with name matching ACL binding-rule", "name", bindName)
}
case structs.ACLBindingRuleBindTypePolicy:
policy, err := b.store.ACLPolicyByName(nil, bindName)
Expand All @@ -112,8 +119,12 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, identity *Identity) (*B

if policy != nil {
bindings.Policies = append(bindings.Policies, policy.Name)
vlog.Debug("policy found with name matching ACL binding-rule", "name", bindName)
} else {
vlog.Debug("no policy found with name matching ACL binding-rule", "name", bindName)
}
case structs.ACLBindingRuleBindTypeManagement:
vlog.Debug("management ACL binding-rule found", "name", bindName)
bindings.Management = true
bindings.Policies = nil
bindings.Roles = nil
Expand Down
4 changes: 3 additions & 1 deletion lib/auth/binder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/shoenig/test/must"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
Expand Down Expand Up @@ -115,7 +116,8 @@ func TestBinder_Bind(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := testBind.Bind(tt.authMethod, tt.identity)
l := hclog.NewNullLogger()
got, err := testBind.Bind(l, tt.authMethod, tt.identity)
if tt.wantErr {
must.Error(t, err)
} else {
Expand Down
63 changes: 61 additions & 2 deletions nomad/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package nomad

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -52,6 +53,10 @@ const (
// aclLoginRequestExpiryTime is the deadline used when performing HTTP
// requests to external APIs during the validation of bearer tokens.
aclLoginRequestExpiryTime = 60 * time.Second

// verboseLoggingMessage is the message displayed to a user when
// this auth config is enabled
verboseLoggingMessage = "attempting login with verbose logging enabled"
)

// ACL endpoint is used for manipulating ACL tokens and policies
Expand Down Expand Up @@ -2691,6 +2696,14 @@ func (a *ACL) OIDCCompleteAuth(
return structs.NewErrRPCCodedf(http.StatusBadRequest, "auth-method %q not found", args.AuthMethodName)
}

// vlog is a verbose logger used for debugging OIDC in test environments
vlog := hclog.NewNullLogger()
if authMethod.Config.VerboseLogging {
vlog = a.logger
}

vlog.Debug(verboseLoggingMessage)

// If the authentication method generates global ACL tokens, we need to
// forward the request onto the authoritative regional leader.
if authMethod.TokenLocalityIsGlobal() {
Expand Down Expand Up @@ -2761,14 +2774,37 @@ func (a *ACL) OIDCCompleteAuth(
return err
}

// No need to do all this marshaling if VerboseLogging is disabled
if authMethod.Config.VerboseLogging {
idTokenClaimBytes, err := json.MarshalIndent(idTokenClaims, "", " ")
if err != nil {
vlog.Debug("failed to marshal ID token claims")
}

userClaimBytes, err := json.MarshalIndent(userClaims, "", " ")
if err != nil {
vlog.Debug("failed to marshal user claims")
}
vlog.Debug("claims from jwt token and user info endpoint",
"token_claims", string(idTokenClaimBytes),
"user_claims", string(userClaimBytes),
)

internalClaimBytes, err := json.MarshalIndent(oidcInternalClaims.List, "", " ")
if err != nil {
vlog.Debug("failed to marshal OIDC internal claims list")
}
vlog.Debug("claims after mapping to nomad identity attributes", "internal_claims", string(internalClaimBytes))
}

// Create a new binder object based on the current state snapshot to
// provide consistency within the RPC handler.
oidcBinder := auth.NewBinder(stateSnapshot)

// Generate the role and policy bindings that will be assigned to the ACL
// token. Ensure we have at least 1 role or policy, otherwise the RPC will
// fail anyway.
tokenBindings, err := oidcBinder.Bind(authMethod, auth.NewIdentity(authMethod.Config, oidcInternalClaims))
tokenBindings, err := oidcBinder.Bind(vlog, authMethod, auth.NewIdentity(authMethod.Config, oidcInternalClaims))
if err != nil {
return err
}
Expand Down Expand Up @@ -2910,6 +2946,14 @@ func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLLoginRespon
)
}

// vlog is a verbose logger used for debugging in test environments
vlog := hclog.NewNullLogger()
if authMethod.Config.VerboseLogging {
vlog = a.logger
}

vlog.Debug(verboseLoggingMessage)

// Create a new binder object based on the current state snapshot to
// provide consistency within the RPC handler.
jwtBinder := auth.NewBinder(stateSnapshot)
Expand All @@ -2921,7 +2965,22 @@ func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLLoginRespon
return err
}

tokenBindings, err := jwtBinder.Bind(authMethod, auth.NewIdentity(authMethod.Config, jwtClaims))
// No need to do marshaling if VerboseLogging is not enabled
if authMethod.Config.VerboseLogging {
idTokenClaimBytes, err := json.MarshalIndent(claims, "", " ")
if err != nil {
vlog.Debug("failed to marshal token claims")
}
vlog.Debug("jwt token claims", "token_claims", string(idTokenClaimBytes))

internalClaimBytes, err := json.MarshalIndent(jwtClaims.List, "", " ")
if err != nil {
vlog.Debug("failed to marshal claims list")
}
vlog.Debug("claims after mapping to nomad identity attributes", "internal_claims", string(internalClaimBytes))
}

tokenBindings, err := jwtBinder.Bind(vlog, authMethod, auth.NewIdentity(authMethod.Config, jwtClaims))
if err != nil {
return err
}
Expand Down
36 changes: 34 additions & 2 deletions nomad/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
package nomad

import (
"bytes"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/golang-jwt/jwt/v5"
capOIDC "github.com/hashicorp/cap/oidc"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc/v2"
"github.com/hashicorp/nomad/ci"
Expand Down Expand Up @@ -3631,7 +3635,14 @@ func TestACL_OIDCAuthURL(t *testing.T) {
func TestACL_OIDCCompleteAuth(t *testing.T) {
ci.Parallel(t)

testServer, _, testServerCleanupFn := TestACLServer(t, nil)
// setup logging output to test verbose logging
var buf bytes.Buffer
testServer, _, testServerCleanupFn := TestACLServer(t, func(c *Config) {
c.Logger = hclog.NewInterceptLogger(&hclog.LoggerOptions{
Level: hclog.Debug,
Output: io.MultiWriter(&buf, os.Stderr),
})
})
defer testServerCleanupFn()
codec := rpcClient(t, testServer)
testutil.WaitForLeader(t, testServer.RPC)
Expand Down Expand Up @@ -3720,6 +3731,12 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
must.Error(t, err)
must.ErrorContains(t, err, "400")
must.ErrorContains(t, err, "no role or policy bindings matched")
must.False(t, strings.Contains(buf.String(), verboseLoggingMessage))

// Reset buf and enable verbose logging
buf.Reset()
mockedAuthMethod.Config.VerboseLogging = true
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(11, []*structs.ACLAuthMethod{mockedAuthMethod}))

// Upsert an ACL policy and role, so that we can reference this within our
// OIDC claims.
Expand Down Expand Up @@ -3767,6 +3784,7 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
must.Len(t, 1, completeAuthResp4.ACLToken.Roles)
must.Eq(t, mockACLRole.Name, completeAuthResp4.ACLToken.Roles[0].Name)
must.Eq(t, mockACLRole.ID, completeAuthResp4.ACLToken.Roles[0].ID)
must.True(t, strings.Contains(buf.String(), verboseLoggingMessage))

// Create a binding rule which generates management tokens. This should
// override the other rules, giving us a management token when we next
Expand Down Expand Up @@ -3803,7 +3821,14 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
func TestACL_Login(t *testing.T) {
ci.Parallel(t)

testServer, _, testServerCleanupFn := TestACLServer(t, nil)
// setup logging output to test verbose logging
var buf bytes.Buffer
testServer, _, testServerCleanupFn := TestACLServer(t, func(c *Config) {
c.Logger = hclog.NewInterceptLogger(&hclog.LoggerOptions{
Level: hclog.Debug,
Output: io.MultiWriter(&buf, os.Stderr),
})
})
defer testServerCleanupFn()
codec := rpcClient(t, testServer)
testutil.WaitForLeader(t, testServer.RPC)
Expand Down Expand Up @@ -3885,6 +3910,12 @@ func TestACL_Login(t *testing.T) {
must.Error(t, err)
must.ErrorContains(t, err, "400")
must.ErrorContains(t, err, "no role or policy bindings matched")
must.False(t, strings.Contains(buf.String(), verboseLoggingMessage))

// Reset buf and enable verbose logging
buf.Reset()
mockedAuthMethod.Config.VerboseLogging = true
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(11, []*structs.ACLAuthMethod{mockedAuthMethod}))

// Upsert an ACL policy and role, so that we can reference this within our
// JWT claims.
Expand Down Expand Up @@ -3930,6 +3961,7 @@ func TestACL_Login(t *testing.T) {
must.Eq(t, mockACLRole.Name, completeAuthResp4.ACLToken.Roles[0].Name)
must.Eq(t, mockACLRole.ID, completeAuthResp4.ACLToken.Roles[0].ID)
must.Eq(t, mockedAuthMethod.Type+"-"+mockedAuthMethod.Name, completeAuthResp4.ACLToken.Name)
must.True(t, strings.Contains(buf.String(), verboseLoggingMessage))

// Create a binding rule which generates management tokens. This should
// override the other rules, giving us a management token when we next
Expand Down
1 change: 1 addition & 0 deletions nomad/mock/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ func ACLOIDCAuthMethod() *structs.ACLAuthMethod {
SigningAlgs: []string{"RS256"},
ClaimMappings: map[string]string{"foo": "bar"},
ListClaimMappings: map[string]string{"foo": "bar"},
VerboseLogging: false,
},
CreateTime: time.Now().UTC(),
CreateIndex: 10,
Expand Down
5 changes: 5 additions & 0 deletions nomad/structs/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@ func (a *ACLAuthMethod) SetHash() []byte {
_, _ = hash.Write([]byte(a.Config.OIDCClientID))
_, _ = hash.Write([]byte(a.Config.OIDCClientSecret))
_, _ = hash.Write([]byte(strconv.FormatBool(a.Config.OIDCDisableUserInfo)))
_, _ = hash.Write([]byte(strconv.FormatBool(a.Config.VerboseLogging)))
_, _ = hash.Write([]byte(a.Config.ExpirationLeeway.String()))
_, _ = hash.Write([]byte(a.Config.NotBeforeLeeway.String()))
_, _ = hash.Write([]byte(a.Config.ClockSkewLeeway.String()))
Expand Down Expand Up @@ -1043,6 +1044,10 @@ type ACLAuthMethodConfig struct {
// (value).
ClaimMappings map[string]string
ListClaimMappings map[string]string

// Enables logging of claims and binding-rule evaluations when
// debug level logging is enabled.
VerboseLogging bool
}

func (a *ACLAuthMethodConfig) Copy() *ACLAuthMethodConfig {
Expand Down
4 changes: 4 additions & 0 deletions website/content/api-docs/acl/auth-methods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ The table below shows this endpoint's support for
copied to a metadata field (value). Use this if the claim you are capturing is
list-like (such as groups).

- `VerboseLogging` `(bool: false)` - When set to `true`, Nomad will log token claims
and information related to binding-rule and role/policy evaluations. Not recommended
in production since sensitive information may be present.

### Sample payload

```json
Expand Down
Loading