Skip to content

Commit

Permalink
auth: adds option to enable verbose logging during sso
Browse files Browse the repository at this point in the history
  • Loading branch information
mismithhisler committed Jan 17, 2025
1 parent 1df94b1 commit 64fcbf5
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 6 deletions.
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
67 changes: 65 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,38 @@ 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 {
return fmt.Errorf("failed to marshal ID token claims")
}

userClaimBytes, err := json.MarshalIndent(userClaims, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal user claims")
}

internalClaimBytes, err := json.MarshalIndent(oidcInternalClaims.List, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal OIDC internal claims list")
}

vlog.Debug("Claims",
"Token Claims", string(idTokenClaimBytes),
"User Claims", string(userClaimBytes),
"Claims after mapping to Nomad identity attributes", 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 +2947,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 +2966,25 @@ 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 {
return fmt.Errorf("failed to marshal token claims")
}

internalClaimBytes, err := json.MarshalIndent(jwtClaims.List, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal claims list")
}

vlog.Debug("Claims",
"Token Claims", string(idTokenClaimBytes),
"Claims after mapping to Nomad identity attributes", 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

0 comments on commit 64fcbf5

Please sign in to comment.