mirror of
https://github.com/hashicorp/vault.git
synced 2026-04-03 12:52:15 +02:00
* Add audit log entries for enterprise JWT token fields * Reduce enterprise token field comment detail - simplify enterprise token comments in sdk/logical/request.go - remove verbose wording about issuer/audience/authorization semantics * Fix TestAudit_JWT_DelegationToken permission denied error The test was failing with 'permission denied' when using a delegation token (JWT with act claim) to access cubbyhole. The root causes were: 1. RAR (Rich Authorization Request) check: The JWT contained 'authorization_details' constraints that only allowed access to 'secret/data/users/alice' and 'secret/data/config/general', but the test was attempting to access 'cubbyhole/test'. The RAR check in PerformRARCheck() was correctly denying this mismatch. 2. Missing entity policies for actor ACL: For delegation tokens, the actor's ACL is built solely from entity identity policies (not token policies like 'default'). Without explicit policies on the actor entity, the delegation ACL intersection check would fail. Fixes: - Removed 'authorization_details' from the test JWT since the test is about verifying audit log entries for delegation tokens, not RAR constraints - Added 'default' policy to both subject and actor entities to ensure both ACLs allow cubbyhole access for the delegation token intersection - Updated test assertions to match the simplified JWT (removed authorization_details verification) * Use require.NoError instead of t.Fatalf for error check * Add explicit checks for auth field presence before type assertion Adds separate checks to verify the 'auth' and 'metadata' fields exist in the map before attempting type assertion, preventing potential panics and improving test clarity. * test: tighten request metadata merge assertions * test: simplify enterprise metadata assertions * test: split enterprise metadata merge coverage * style: apply gofumpt to entry formatter tests * test: add godoc for enterprise token metadata test --------- Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
500 lines
13 KiB
Go
500 lines
13 KiB
Go
// Copyright IBM Corp. 2016, 2025
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
nshelper "github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
"github.com/hashicorp/vault/sdk/helper/salt"
|
|
"github.com/hashicorp/vault/sdk/helper/wrapping"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/mitchellh/copystructure"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCopy_auth(t *testing.T) {
|
|
// Make a non-pointer one so that it can't be modified directly
|
|
expected := logical.Auth{
|
|
LeaseOptions: logical.LeaseOptions{
|
|
TTL: 1 * time.Hour,
|
|
},
|
|
|
|
ClientToken: "foo",
|
|
}
|
|
auth := expected
|
|
|
|
// Copy it
|
|
dup, err := copystructure.Copy(&auth)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Check equality
|
|
auth2 := dup.(*logical.Auth)
|
|
if !reflect.DeepEqual(*auth2, expected) {
|
|
t.Fatalf("bad:\n\n%#v\n\n%#v", *auth2, expected)
|
|
}
|
|
}
|
|
|
|
func TestCopy_request(t *testing.T) {
|
|
// Make a non-pointer one so that it can't be modified directly
|
|
expected := logical.Request{
|
|
Data: map[string]interface{}{
|
|
"foo": "bar",
|
|
},
|
|
WrapInfo: &logical.RequestWrapInfo{
|
|
TTL: 60 * time.Second,
|
|
},
|
|
}
|
|
arg := expected
|
|
|
|
// Copy it
|
|
dup, err := copystructure.Copy(&arg)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Check equality
|
|
arg2 := dup.(*logical.Request)
|
|
if !reflect.DeepEqual(*arg2, expected) {
|
|
t.Fatalf("bad:\n\n%#v\n\n%#v", *arg2, expected)
|
|
}
|
|
}
|
|
|
|
func TestCopy_response(t *testing.T) {
|
|
// Make a non-pointer one so that it can't be modified directly
|
|
expected := logical.Response{
|
|
Data: map[string]interface{}{
|
|
"foo": "bar",
|
|
},
|
|
WrapInfo: &wrapping.ResponseWrapInfo{
|
|
TTL: 60,
|
|
Token: "foo",
|
|
CreationTime: time.Now(),
|
|
WrappedAccessor: "abcd1234",
|
|
},
|
|
}
|
|
arg := expected
|
|
|
|
// Copy it
|
|
dup, err := copystructure.Copy(&arg)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Check equality
|
|
arg2 := dup.(*logical.Response)
|
|
if !reflect.DeepEqual(*arg2, expected) {
|
|
t.Fatalf("bad:\n\n%#v\n\n%#v", *arg2, expected)
|
|
}
|
|
}
|
|
|
|
// testSalter is a structure that implements the Salter interface in a trivial
|
|
// manner.
|
|
type testSalter struct{}
|
|
|
|
// Salt returns a salt.Salt pointer based on dummy data stored in an in-memory
|
|
// storage instance.
|
|
func (*testSalter) Salt(ctx context.Context) (*salt.Salt, error) {
|
|
inmemStorage := &logical.InmemStorage{}
|
|
err := inmemStorage.Put(context.Background(), &logical.StorageEntry{
|
|
Key: "salt",
|
|
Value: []byte("foo"),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return salt.NewSalt(context.Background(), inmemStorage, &salt.Config{
|
|
HMAC: sha256.New,
|
|
HMACType: "hmac-sha256",
|
|
})
|
|
}
|
|
|
|
func TestHashString(t *testing.T) {
|
|
salter := &testSalter{}
|
|
|
|
out, err := hashString(context.Background(), salter, "foo")
|
|
if err != nil {
|
|
t.Fatalf("Error instantiating salt: %s", err)
|
|
}
|
|
if out != "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a" {
|
|
t.Fatalf("err: hashString output did not match expected")
|
|
}
|
|
}
|
|
|
|
func TestHashAuth(t *testing.T) {
|
|
cases := map[string]struct {
|
|
Input *logical.Auth
|
|
Output *auth
|
|
HMACAccessor bool
|
|
}{
|
|
"no-accessor-hmac": {
|
|
&logical.Auth{
|
|
ClientToken: "foo",
|
|
Accessor: "very-accessible",
|
|
LeaseOptions: logical.LeaseOptions{
|
|
TTL: 1 * time.Hour,
|
|
},
|
|
TokenType: logical.TokenTypeService,
|
|
},
|
|
&auth{
|
|
ClientToken: "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a",
|
|
Accessor: "very-accessible",
|
|
TokenTTL: 3600,
|
|
TokenType: "service",
|
|
RemainingUses: 5,
|
|
},
|
|
false,
|
|
},
|
|
"accessor-hmac": {
|
|
&logical.Auth{
|
|
Accessor: "very-accessible",
|
|
ClientToken: "foo",
|
|
LeaseOptions: logical.LeaseOptions{
|
|
TTL: 1 * time.Hour,
|
|
},
|
|
TokenType: logical.TokenTypeBatch,
|
|
},
|
|
&auth{
|
|
ClientToken: "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a",
|
|
Accessor: "hmac-sha256:5d6d7c8da5b699ace193ea453bbf77082a8aaca42a474436509487d646a7c0af",
|
|
TokenTTL: 3600,
|
|
TokenType: "batch",
|
|
RemainingUses: 5,
|
|
},
|
|
true,
|
|
},
|
|
}
|
|
|
|
inmemStorage := &logical.InmemStorage{}
|
|
err := inmemStorage.Put(context.Background(), &logical.StorageEntry{
|
|
Key: "salt",
|
|
Value: []byte("foo"),
|
|
})
|
|
require.NoError(t, err)
|
|
salter := &testSalter{}
|
|
for _, tc := range cases {
|
|
auditAuth, err := newAuth(tc.Input, 5)
|
|
require.NoError(t, err)
|
|
err = hashAuth(context.Background(), salter, auditAuth, tc.HMACAccessor)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.Output, auditAuth)
|
|
}
|
|
}
|
|
|
|
type testOptMarshaler struct {
|
|
S string
|
|
I int
|
|
}
|
|
|
|
func (o *testOptMarshaler) MarshalJSONWithOptions(options *logical.MarshalOptions) ([]byte, error) {
|
|
return json.Marshal(&testOptMarshaler{S: options.ValueHasher(o.S), I: o.I})
|
|
}
|
|
|
|
var _ logical.OptMarshaler = &testOptMarshaler{}
|
|
|
|
func TestHashRequest(t *testing.T) {
|
|
cases := []struct {
|
|
Input *logical.Request
|
|
Output *request
|
|
NonHMACDataKeys []string
|
|
HMACAccessor bool
|
|
}{
|
|
{
|
|
&logical.Request{
|
|
Data: map[string]interface{}{
|
|
"foo": "bar",
|
|
"baz": "foobar",
|
|
"private_key_type": certutil.PrivateKeyType("rsa"),
|
|
"om": &testOptMarshaler{S: "bar", I: 1},
|
|
},
|
|
},
|
|
&request{
|
|
Data: map[string]interface{}{
|
|
"foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
|
|
"baz": "foobar",
|
|
"private_key_type": "hmac-sha256:995230dca56fffd310ff591aa404aab52b2abb41703c787cfa829eceb4595bf1",
|
|
"om": json.RawMessage(`{"S":"hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317","I":1}`),
|
|
},
|
|
Namespace: &namespace{
|
|
ID: nshelper.RootNamespace.ID,
|
|
Path: nshelper.RootNamespace.Path,
|
|
},
|
|
},
|
|
[]string{"baz"},
|
|
false,
|
|
},
|
|
}
|
|
|
|
inmemStorage := &logical.InmemStorage{}
|
|
err := inmemStorage.Put(context.Background(), &logical.StorageEntry{
|
|
Key: "salt",
|
|
Value: []byte("foo"),
|
|
})
|
|
require.NoError(t, err)
|
|
salter := &testSalter{}
|
|
for _, tc := range cases {
|
|
auditReq, err := newRequest(tc.Input, nshelper.RootNamespace)
|
|
require.NoError(t, err)
|
|
err = hashRequest(context.Background(), salter, auditReq, tc.HMACAccessor, tc.NonHMACDataKeys)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.Output, auditReq)
|
|
}
|
|
}
|
|
|
|
func TestHashResponse(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
resp := &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"foo": "bar",
|
|
"baz": "foobar",
|
|
// Responses can contain time values, so test that with a known fixed value.
|
|
"bar": now,
|
|
"om": &testOptMarshaler{S: "bar", I: 1},
|
|
},
|
|
WrapInfo: &wrapping.ResponseWrapInfo{
|
|
TTL: 1 * time.Minute,
|
|
Token: "bar",
|
|
Accessor: "flimflam",
|
|
CreationTime: now,
|
|
WrappedAccessor: "bar",
|
|
},
|
|
Auth: &logical.Auth{
|
|
ClientToken: "hvs.QWERTY-T1q5lEjIWux1Tjx-VGqAYJdd4FZtbp1wpD5Ym9pGh4KHGh2cy5TSjRndGoxaU44NzNscm5MSlRLQXZ0ZGg",
|
|
Accessor: "ABClk9ZNLGOCuTrOEIAooJG3",
|
|
TokenType: logical.TokenTypeService,
|
|
},
|
|
Secret: &logical.Secret{
|
|
LeaseOptions: logical.LeaseOptions{
|
|
TTL: 3,
|
|
MaxTTL: 5,
|
|
Renewable: false,
|
|
Increment: 1,
|
|
IssueTime: now,
|
|
},
|
|
InternalData: map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
LeaseID: "abc",
|
|
},
|
|
}
|
|
|
|
req := &logical.Request{MountPoint: "/foo/bar"}
|
|
req.SetMountClass("kv")
|
|
req.SetMountIsExternalPlugin(true)
|
|
req.SetMountRunningVersion("123")
|
|
req.SetMountRunningSha256("256-256!")
|
|
|
|
nonHMACDataKeys := []string{"baz"}
|
|
|
|
expected := &response{
|
|
Auth: &auth{
|
|
Accessor: "hmac-sha256:253184715b2d5a6c3a2fc7afe0d2294085f5e886a1275ca735646a6f23be2587",
|
|
ClientToken: "hmac-sha256:2ce541100a8bcd687e8ec7712c8bb4c975a8d8599c02d98945e63ecd413bf0f3",
|
|
TokenType: "service",
|
|
},
|
|
Data: map[string]interface{}{
|
|
"foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
|
|
"baz": "foobar",
|
|
"bar": now.Format(time.RFC3339Nano),
|
|
"om": json.RawMessage(`{"S":"hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317","I":1}`),
|
|
},
|
|
WrapInfo: &responseWrapInfo{
|
|
TTL: 60,
|
|
Token: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
|
|
Accessor: "hmac-sha256:7c9c6fe666d0af73b3ebcfbfabe6885015558213208e6635ba104047b22f6390",
|
|
CreationTime: now.UTC().Format(time.RFC3339Nano),
|
|
WrappedAccessor: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317",
|
|
},
|
|
MountClass: "kv",
|
|
MountIsExternalPlugin: true,
|
|
MountPoint: "/foo/bar",
|
|
MountRunningVersion: "123",
|
|
MountRunningSha256: "256-256!",
|
|
Secret: &secret{
|
|
LeaseID: "abc",
|
|
},
|
|
}
|
|
|
|
inmemStorage := &logical.InmemStorage{}
|
|
err := inmemStorage.Put(context.Background(), &logical.StorageEntry{
|
|
Key: "salt",
|
|
Value: []byte("foo"),
|
|
})
|
|
require.NoError(t, err)
|
|
salter := &testSalter{}
|
|
auditResp, err := newResponse(resp, req, false)
|
|
require.NoError(t, err)
|
|
err = hashResponse(context.Background(), salter, auditResp, true, nonHMACDataKeys)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, auditResp)
|
|
}
|
|
|
|
func TestHashWalker(t *testing.T) {
|
|
replaceText := "foo"
|
|
|
|
cases := []struct {
|
|
Input map[string]interface{}
|
|
Output map[string]interface{}
|
|
}{
|
|
{
|
|
map[string]interface{}{
|
|
"hello": "foo",
|
|
},
|
|
map[string]interface{}{
|
|
"hello": replaceText,
|
|
},
|
|
},
|
|
|
|
{
|
|
map[string]interface{}{
|
|
"hello": []interface{}{"world"},
|
|
},
|
|
map[string]interface{}{
|
|
"hello": []interface{}{replaceText},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
err := hashStructure(tc.Input, func(string) string {
|
|
return replaceText
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("err: %s\n\n%#v", err, tc.Input)
|
|
}
|
|
if !reflect.DeepEqual(tc.Input, tc.Output) {
|
|
t.Fatalf("bad:\n\n%#v\n\n%#v", tc.Input, tc.Output)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHashWalker_TimeStructs(t *testing.T) {
|
|
replaceText := "bar"
|
|
|
|
now := time.Now()
|
|
cases := []struct {
|
|
Input map[string]interface{}
|
|
Output map[string]interface{}
|
|
}{
|
|
// Should not touch map keys of type time.Time.
|
|
{
|
|
map[string]interface{}{
|
|
"hello": map[time.Time]struct{}{
|
|
now: {},
|
|
},
|
|
},
|
|
map[string]interface{}{
|
|
"hello": map[time.Time]struct{}{
|
|
now: {},
|
|
},
|
|
},
|
|
},
|
|
// Should handle map values of type time.Time.
|
|
{
|
|
map[string]interface{}{
|
|
"hello": now,
|
|
},
|
|
map[string]interface{}{
|
|
"hello": now.Format(time.RFC3339Nano),
|
|
},
|
|
},
|
|
// Should handle slice values of type time.Time.
|
|
{
|
|
map[string]interface{}{
|
|
"hello": []interface{}{"foo", now, "foo2"},
|
|
},
|
|
map[string]interface{}{
|
|
"hello": []interface{}{"foobar", now.Format(time.RFC3339Nano), "foo2bar"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
err := hashStructure(tc.Input, func(s string) string {
|
|
return s + replaceText
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("err: %v\n\n%#v", err, tc.Input)
|
|
}
|
|
if !reflect.DeepEqual(tc.Input, tc.Output) {
|
|
t.Fatalf("bad:\n\n%#v\n\n%#v", tc.Input, tc.Output)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCopy_request_EnterpriseTokenFields verifies that copystructure.Copy
|
|
// correctly deep-copies a logical.Request that carries enterprise token fields,
|
|
// including EnterpriseTokenAuthorizationDetails which is []map[string]any and
|
|
// would silently lose data under a shallow copy.
|
|
func TestCopy_request_EnterpriseTokenFields(t *testing.T) {
|
|
expected := logical.Request{
|
|
Data: map[string]interface{}{
|
|
"foo": "bar",
|
|
},
|
|
EnterpriseTokenMetadata: "test-token-abc",
|
|
EnterpriseTokenIssuer: "https://issuer.example.com",
|
|
EnterpriseTokenAudience: []string{"vault", "api"},
|
|
EnterpriseTokenAuthorizationDetails: []logical.AuthorizationDetail{
|
|
{
|
|
"type": "vault:path_access",
|
|
"path_constraint": "secret/data/users/alice",
|
|
"action": "read",
|
|
},
|
|
{
|
|
"type": "vault:path_access",
|
|
"path_constraint": "secret/data/config/general",
|
|
"action": "update",
|
|
},
|
|
},
|
|
}
|
|
arg := expected
|
|
|
|
dup, err := copystructure.Copy(&arg)
|
|
require.NoError(t, err)
|
|
|
|
arg2 := dup.(*logical.Request)
|
|
require.EqualValues(t, expected, *arg2)
|
|
}
|
|
|
|
// TestHashRequest_EnterpriseTokenFieldsInMetadata verifies that enterprise token
|
|
// fields stored in auth.Metadata are not HMAC'd by hashAuth. These values are
|
|
// not secrets and must appear as cleartext in the audit log.
|
|
func TestHashRequest_EnterpriseTokenFieldsInMetadata(t *testing.T) {
|
|
// Enterprise token fields are now stored in auth.Metadata by createEntry().
|
|
// Verify that hashAuth does not HMAC metadata values.
|
|
auditAuth := &auth{
|
|
ClientToken: "secret-token",
|
|
Metadata: map[string]string{
|
|
"enterprise_token_metadata": "test-token-xyz",
|
|
"enterprise_token_issuer": "https://issuer.example.com",
|
|
"actor_entity_id": "actor-123",
|
|
"actor_entity_name": "actor-service",
|
|
},
|
|
}
|
|
|
|
salter := &testSalter{}
|
|
err := hashAuth(context.Background(), salter, auditAuth, false)
|
|
require.NoError(t, err)
|
|
|
|
// ClientToken must be HMAC'd — it is a secret.
|
|
require.NotEqual(t, "secret-token", auditAuth.ClientToken)
|
|
require.Contains(t, auditAuth.ClientToken, "hmac-sha256:")
|
|
|
|
// Metadata values must pass through unchanged — they are not secrets.
|
|
require.Equal(t, "test-token-xyz", auditAuth.Metadata["enterprise_token_metadata"])
|
|
require.Equal(t, "https://issuer.example.com", auditAuth.Metadata["enterprise_token_issuer"])
|
|
require.Equal(t, "actor-123", auditAuth.Metadata["actor_entity_id"])
|
|
require.Equal(t, "actor-service", auditAuth.Metadata["actor_entity_name"])
|
|
}
|