feature (events): Classify auth method request/resp messages for audit events. (#1640)

* refactor (oidc): Stop emitting error for not found token id

Stop emitting errors when authtoken repo.IssueAuthToken is called and
there's if no pending token.  It's not an "normal" state and not
an error condition.

* feature (audit tagging): Add tags for auth method requests/responses

Including testing functions for asserting audit events created
when a service is called
pull/1644/head
Jim 5 years ago committed by GitHub
parent 3de16b6e21
commit a679300b50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -183,3 +183,4 @@ Note that if `max_connections` is set too low, it may result in sporadic test
failures if a connection cannot be established. In this case, reduce the number
of concurrent tests via `GOMAXPROCS` or selectively run tests.
## [Adding additional field to an existing API (or new API)](internal/adding-a-new-field-readme.md)

@ -18,7 +18,7 @@ require (
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe
github.com/golang/protobuf v1.5.2
github.com/google/go-cmp v0.5.6
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0
github.com/hashicorp/boundary/api v0.0.19
github.com/hashicorp/boundary/sdk v0.0.11
@ -26,7 +26,7 @@ require (
github.com/hashicorp/dawdle v0.4.0
github.com/hashicorp/dbassert v0.0.0-20210708202608-ecf920cf1ed8
github.com/hashicorp/eventlogger v0.1.0
github.com/hashicorp/eventlogger/filters/encrypt v0.1.4-0.20210928205053-80364fba97eb
github.com/hashicorp/eventlogger/filters/encrypt v0.1.5
github.com/hashicorp/go-bexpr v0.1.10
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v0.16.2

@ -458,9 +458,9 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/eventlogger v0.1.0 h1:S6xc4gZVzewuDUP4R4Ngko419h/CGDuV/b4ADL3XLik=
github.com/hashicorp/eventlogger v0.1.0/go.mod h1:a3IXf1aEJfpCPzseTOrwKj4fVW/Qn3oEmpQeaIznzH0=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.3/go.mod h1:8rcez7Kw1zanB0/074qnOuGu7zxmNh9Xr2ZI+K4xVIA=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.4-0.20210928205053-80364fba97eb h1:AhprXfDoXGI8hP5fUCyCB29jpfd01Ck996ErqG0nVGk=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.4-0.20210928205053-80364fba97eb/go.mod h1:8rcez7Kw1zanB0/074qnOuGu7zxmNh9Xr2ZI+K4xVIA=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.5-0.20211025115820-78e1ded4aea1/go.mod h1:8rcez7Kw1zanB0/074qnOuGu7zxmNh9Xr2ZI+K4xVIA=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.5 h1:kNkH4G6zzWlZSoI1I+B/ud4chVKTPL516C6jB7dRdlE=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.5/go.mod h1:8rcez7Kw1zanB0/074qnOuGu7zxmNh9Xr2ZI+K4xVIA=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=

@ -7,13 +7,16 @@ Once you've figured out that you need an additional field in Boundary's domain m
* Make schema changes:
* Define the new column and provide for the migration of existing data
* Create a new migration under `internal/db/schema/migrations/postgres`
* Run `make migrations` after modifying DDL in SQL files.
* Create a new migration under `internal/db/schema/migrations/oss/postgres`
* Add the new field to the storage protobuf
* storage protobufs are under: `internal/proto/local/controller/storage`
* Define a gorm tag for the new field via `@inject_tag`
* Define a gorm tag for the new field via `@gotags` (`@inject_tag` has been deprecated)
* Define a `custom_options.v1.mask_mapping` for the field which maps the storage `this` field to the API `that` field (yes, it's the opposite of how it's defined for the API protobuf)
* Run `make proto` after modifying storage protobuf
* Extend the existing repository function for Updating the resource to incorporate the new field. This could/may entail defining new options for the Update function.
@ -29,6 +32,12 @@ Now that the repository supports the new field, you can move on to adding this n
* Define a `custom_options.v1.generate_sdk_option` tag for the SDK
* Define a data classification/filter tag for the field via `@gotags` or the
`encrypt.Taggable` interface which specifies how sensitive/secret/public data
will be handled for the API's audit events. Please write unit tests to verify
the audit event is properly "redacted" (see the unit tests of
`TestAuthenticate_Tags` and `TestAuthenticate` for examples).
* Run `make proto` and `make api` after modifying the API/SDK protobufs
## Update the API handler service

@ -383,7 +383,7 @@ func (r *Repository) IssueAuthToken(ctx context.Context, tokenRequestId string)
// trigger to set ApproximateLastAccessTime to the commit timestamp.
rowsUpdated, err := w.Update(ctx, at, []string{"Status"}, []string{"ApproximateLastAccessTime"}, db.WithWhere("status = ?", PendingStatus))
if err != nil {
return errors.Wrap(ctx, err, op)
return errors.Wrap(ctx, err, op, errors.WithoutEvent())
}
if rowsUpdated == 0 {
return errors.New(ctx, errors.RecordNotFound, op, "pending auth token not found")

@ -30,7 +30,7 @@ type GetAuthMethodRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *GetAuthMethodRequest) Reset() {
@ -281,7 +281,7 @@ type CreateAuthMethodResponse struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"`
Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty" class:"public"` // @gotags: `class:"public"`
Item *authmethods.AuthMethod `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"`
}
@ -336,7 +336,7 @@ type UpdateAuthMethodRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"`
Item *authmethods.AuthMethod `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"`
UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,3,opt,name=update_mask,proto3" json:"update_mask,omitempty"`
}
@ -591,7 +591,7 @@ type ChangeStateRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"`
// Version is used to ensure this resource has not changed.
// The mutation will fail if the version does not match the latest known good version.
Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"`
@ -876,7 +876,7 @@ type AuthenticateRequest struct {
AuthMethodId string `protobuf:"bytes,1,opt,name=auth_method_id,proto3" json:"auth_method_id,omitempty" class:"public"` // @gotags: `class:"public"`
// This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order
// to keep it safe from rogue JS in the browser.
TokenType string `protobuf:"bytes,2,opt,name=token_type,proto3" json:"token_type,omitempty"`
TokenType string `protobuf:"bytes,2,opt,name=token_type,proto3" json:"token_type,omitempty" class:"public"` // @gotags: `class:"public"`
// Attributes are passed to the Auth Method; the valid keys and values depend on the type of Auth Method as well as the command.
Attributes *structpb.Struct `protobuf:"bytes,4,opt,name=attributes,proto3" json:"attributes,omitempty"`
// The command to perform.

@ -54,6 +54,22 @@ func (req *AuthenticateResponse) Tags() ([]encrypt.PointerTag, error) {
Pointer: "/Attributes/Fields/user_id",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/status",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/auth_url",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/token_id",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/final_redirect_url",
Classification: encrypt.PublicClassification,
},
// secret fields
{
Pointer: "/Attributes/Fields/token",
@ -61,3 +77,54 @@ func (req *AuthenticateResponse) Tags() ([]encrypt.PointerTag, error) {
},
}, nil
}
// Tags implements the encrypt.Taggable interface which allows
// AuthenticateRequest Attributes to be classified for the encrypt filter.
func (req *AuthenticateRequest) Tags() ([]encrypt.PointerTag, error) {
if req.Attributes == nil {
return nil, nil
}
return []encrypt.PointerTag{
// public fields
{
Pointer: "/Attributes/Fields/login_name",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/auth_url",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/token_id",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/state",
Classification: encrypt.PublicClassification,
},
// secret fields
{
Pointer: "/Attributes/Fields/password",
Classification: encrypt.SecretClassification,
},
{
Pointer: "/Attributes/Fields/code",
Classification: encrypt.SecretClassification,
},
}, nil
}
// Tags implements the encrypt.Taggable interface which allows
// ChangeStateRequest Attributes to be classified for the encrypt filter.
func (req *ChangeStateRequest) Tags() ([]encrypt.PointerTag, error) {
if req.Attributes == nil {
return nil, nil
}
return []encrypt.PointerTag{
// public fields
{
Pointer: "/Attributes/Fields/state",
Classification: encrypt.PublicClassification,
},
}, nil
}

@ -1,4 +1,4 @@
package services
package services_test
import (
"context"
@ -6,23 +6,24 @@ import (
"testing"
"time"
"github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/sdk/pbs/controller/api"
"github.com/hashicorp/boundary/sdk/wrapper"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/eventlogger/filters/encrypt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
structpb "google.golang.org/protobuf/types/known/structpb"
)
func TestScope_Tags(t *testing.T) {
// TestAuthenticate_Tags will test that the response filtering aligns with the
// AuthenticateResponse and AuthenticateResponse tags. See:
// internal/tests/api/authmethods/authenticate_test.go TestAuthenticate where
// the audit events produced using these tags is unit tested.
func TestAuthenticate_Tags(t *testing.T) {
ctx := context.Background()
now := time.Now()
wrapper := wrapper.TestWrapper(t)
testEncryptingFilter := &encrypt.Filter{
Wrapper: wrapper,
HmacSalt: []byte("salt"),
HmacInfo: []byte("info"),
}
testEncryptingFilter := api.NewEncryptFilter(t, wrapper)
tests := []struct {
name string
@ -30,11 +31,11 @@ func TestScope_Tags(t *testing.T) {
wantEvent *eventlogger.Event
}{
{
name: "validate-filtering",
name: "validate-response-filtering",
testEvent: &eventlogger.Event{
Type: "test",
CreatedAt: now,
Payload: &AuthenticateResponse{
Payload: &services.AuthenticateResponse{
Command: "public-command",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@ -49,6 +50,10 @@ func TestScope_Tags(t *testing.T) {
"token_type": structpb.NewStringValue("public-token_type"),
"updated_time": structpb.NewStringValue("public-updated_time"),
"user_id": structpb.NewStringValue("public-user_id"),
"status": structpb.NewStringValue("public-status"),
"auth_url": structpb.NewStringValue("public-auth_url"),
"token_id": structpb.NewStringValue("public-token_id"),
"final_redirect_url": structpb.NewStringValue("public-final_redirect_url"),
"token": structpb.NewStringValue("secret-token"),
},
},
@ -57,7 +62,7 @@ func TestScope_Tags(t *testing.T) {
wantEvent: &eventlogger.Event{
Type: "test",
CreatedAt: now,
Payload: &AuthenticateResponse{
Payload: &services.AuthenticateResponse{
Command: "public-command",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@ -72,12 +77,57 @@ func TestScope_Tags(t *testing.T) {
"token_type": structpb.NewStringValue("public-token_type"),
"updated_time": structpb.NewStringValue("public-updated_time"),
"user_id": structpb.NewStringValue("public-user_id"),
"status": structpb.NewStringValue("public-status"),
"auth_url": structpb.NewStringValue("public-auth_url"),
"token_id": structpb.NewStringValue("public-token_id"),
"final_redirect_url": structpb.NewStringValue("public-final_redirect_url"),
"token": structpb.NewStringValue("<REDACTED>"),
},
},
},
},
},
{
name: "validate-request-filtering",
testEvent: &eventlogger.Event{
Type: "test",
CreatedAt: now,
Payload: &services.AuthenticateRequest{
AuthMethodId: "public-auth-method-id",
TokenType: "public-token-type",
Command: "public-command",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"login_name": structpb.NewStringValue("public-login_name"),
"auth_url": structpb.NewStringValue("public-auth_url"),
"token_id": structpb.NewStringValue("public-token_id"),
"state": structpb.NewStringValue("public-state"),
"password": structpb.NewStringValue("secret-password"),
"code": structpb.NewStringValue("secret-code"),
},
},
},
},
wantEvent: &eventlogger.Event{
Type: "test",
CreatedAt: now,
Payload: &services.AuthenticateRequest{
AuthMethodId: "public-auth-method-id",
TokenType: "public-token-type",
Command: "public-command",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"login_name": structpb.NewStringValue("public-login_name"),
"auth_url": structpb.NewStringValue("public-auth_url"),
"token_id": structpb.NewStringValue("public-token_id"),
"state": structpb.NewStringValue("public-state"),
"password": structpb.NewStringValue("<REDACTED>"),
"code": structpb.NewStringValue("<REDACTED>"),
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

@ -9,6 +9,7 @@ import (
"log"
"net/url"
"os"
"reflect"
"runtime"
"strings"
"sync"
@ -19,8 +20,8 @@ import (
"github.com/hashicorp/eventlogger/filters/gated"
"github.com/hashicorp/eventlogger/formatter_filters/cloudevents"
"github.com/hashicorp/eventlogger/sinks/writer"
"github.com/hashicorp/go-hclog"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
const (
@ -137,6 +138,19 @@ func SysEventer() *Eventer {
return sysEventer
}
// NewAuditEncryptFilter returns a new encrypt filter which is initialized for
// audit events.
func NewAuditEncryptFilter(opt ...Option) (*encrypt.Filter, error) {
opts := getOpts(opt...)
return &encrypt.Filter{
Wrapper: opts.withAuditWrapper,
IgnoreTypes: []reflect.Type{
reflect.TypeOf(&fieldmaskpb.FieldMask{}),
},
}, nil
}
// NewEventer creates a new Eventer using the config. Supports options:
// WithNow, WithSerializationLock, WithBroker, WithAuditWrapper
func NewEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName string, c EventerConfig, opt ...Option) (*Eventer, error) {
@ -269,8 +283,9 @@ func NewEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName stri
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
encryptFilter := &encrypt.Filter{
Wrapper: opts.withAuditWrapper,
encryptFilter, err := NewAuditEncryptFilter(opt...)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
if len(s.AuditConfig.FilterOverrides) > 0 {
overrides := encrypt.DefaultFilterOperations()

@ -153,36 +153,36 @@ message OidcAuthMethodAttributes {
// The structure of the OIDC authenticate start response, in the JSON object
message OidcAuthMethodAuthenticateStartResponse {
// The returned authentication URL
string auth_url = 10 [json_name = "auth_url"];
string auth_url = 10 [json_name = "auth_url"]; // @gotags: `class:"public"`
// The returned token ID
string token_id = 30 [json_name = "token_id"];
string token_id = 30 [json_name = "token_id"]; // @gotags: `class:"public"`
}
// The structure of OIDC callback request parameters
message OidcAuthMethodAuthenticateCallbackRequest {
// The returned code
string code = 10 [json_name = "code"];
string code = 10 [json_name = "code"]; // @gotags: `class:"secret"`
// The returned state
string state = 20 [json_name = "state"];
string state = 20 [json_name = "state"]; // @gotags: `class:"public"`
// Error parameters, if they are returned
string error = 30 [json_name = "error"];
string error_description = 40 [json_name = "error_description"];
string error_uri = 50 [json_name = "error_uri"];
string error = 30 [json_name = "error"]; // @gotags: `class:"public"`
string error_description = 40 [json_name = "error_description"]; // @gotags: `class:"public"`
string error_uri = 50 [json_name = "error_uri"]; // @gotags: `class:"public"`
}
// The structure of OIDC callback response parameters
message OidcAuthMethodAuthenticateCallbackResponse {
// The final redirection URL
string final_redirect_url = 10 [json_name = "final_redirect_url"];
string final_redirect_url = 10 [json_name = "final_redirect_url"]; // @gotags: `class:"public"`
}
// The structure of OIDC token request parameters
message OidcAuthMethodAuthenticateTokenRequest {
// The ID of the pending token
string token_id = 10 [json_name = "token_id"];
string token_id = 10 [json_name = "token_id"]; // @gotags: `class:"private"`
}
// Internal only: the structure of a token response if it _does not_ contain a
@ -190,5 +190,5 @@ message OidcAuthMethodAuthenticateTokenRequest {
message OidcAuthMethodAuthenticateTokenResponse {
// The status. This will always be "unknown". It will never be forwarded to
// the consumer.
string status = 10;
string status = 10; // @gotags: `class:"public"`
}

@ -107,7 +107,7 @@ service AuthMethodService {
}
message GetAuthMethodRequest {
string id = 1;
string id = 1; // @gotags: `class:"public"`
}
message GetAuthMethodResponse {
@ -129,12 +129,12 @@ message CreateAuthMethodRequest {
}
message CreateAuthMethodResponse {
string uri = 1;
string uri = 1; // @gotags: `class:"public"`
resources.authmethods.v1.AuthMethod item = 2;
}
message UpdateAuthMethodRequest {
string id = 1;
string id = 1; // @gotags: `class:"public"`
resources.authmethods.v1.AuthMethod item = 2;
google.protobuf.FieldMask update_mask = 3 [json_name = "update_mask"];
}
@ -160,7 +160,7 @@ message OidcChangeStateAttributes {
}
message ChangeStateRequest {
string id = 1;
string id = 1; // @gotags: `class:"public"`
// Version is used to ensure this resource has not changed.
// The mutation will fail if the version does not match the latest known good version.
uint32 version = 2;
@ -203,7 +203,7 @@ message AuthenticateRequest {
string auth_method_id = 1 [json_name = "auth_method_id"]; // @gotags: `class:"public"`
// This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order
// to keep it safe from rogue JS in the browser.
string token_type = 2 [json_name = "token_type"];
string token_type = 2 [json_name = "token_type"]; // @gotags: `class:"public"`
// Attributes are passed to the Auth Method; the valid keys and values depend on the type of Auth Method as well as the command.
google.protobuf.Struct attributes = 4 [json_name = "attributes"];
// The command to perform.

@ -4,19 +4,41 @@ import (
"encoding/json"
"fmt"
"net/http"
"os"
"sync"
"testing"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/servers/controller"
tests_api "github.com/hashicorp/boundary/internal/tests/api"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAuthenticate tests the api calls and the audit events it should produce
func TestAuthenticate(t *testing.T) {
// this cannot run in parallel because it relies on envvar
// globals.BOUNDARY_DEVELOPER_ENABLE_EVENTS
event.TestEnableEventing(t, true)
assert, require := assert.New(t), require.New(t)
tc := controller.NewTestController(t, nil)
eventConfig := event.TestEventerConfig(t, "TestAuthenticateAuditEntry", event.TestWithAuditSink(t))
testLock := &sync.Mutex{}
testLogger := hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
})
require.NoError(event.InitSysEventer(testLogger, testLock, "TestAuthenticateAuditEntry", event.WithEventerConfig(&eventConfig.EventerConfig)))
tcConfig, err := config.DevController()
require.NoError(err)
tcConfig.Eventing = &eventConfig.EventerConfig
tc := controller.NewTestController(t, &controller.TestControllerOpts{Config: tcConfig})
defer tc.Shutdown()
client := tc.Client()
@ -49,4 +71,30 @@ func TestAuthenticate(t *testing.T) {
token := new(authtokens.AuthToken)
require.NoError(json.Unmarshal(result.GetRawAttributes(), token))
require.NotEmpty(token.Token)
require.NotNil(eventConfig.AuditEvents)
_ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls
tok, err = methods.Authenticate(tc.Context(), tc.Server().DevPasswordAuthMethodId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"})
require.NoError(err)
assert.NotNil(tok)
got := tests_api.CloudEventFromFile(t, eventConfig.AuditEvents.Name())
reqDetails := tests_api.GetEventDetails(t, got, "request")
tests_api.AssertRedactedValues(t, reqDetails)
tests_api.AssertRedactedValues(t, reqDetails["attributes"], "password")
respDetails := tests_api.GetEventDetails(t, got, "response")
tests_api.AssertRedactedValues(t, respDetails)
tests_api.AssertRedactedValues(t, respDetails["attributes"], "token")
_ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls
tok, err = methods.Authenticate(tc.Context(), tc.Server().DevPasswordAuthMethodId, "login", map[string]interface{}{"login_name": "user", "password": "bad-pass"})
require.Error(err)
assert.Nil(tok)
got = tests_api.CloudEventFromFile(t, eventConfig.AuditEvents.Name())
reqDetails = tests_api.GetEventDetails(t, got, "request")
tests_api.AssertRedactedValues(t, reqDetails)
tests_api.AssertRedactedValues(t, reqDetails["attributes"], "password")
}

@ -2,13 +2,19 @@ package authmethods_test
import (
"net/http"
"os"
"sync"
"testing"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/servers/controller"
tests_api "github.com/hashicorp/boundary/internal/tests/api"
capoidc "github.com/hashicorp/cap/oidc"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -16,8 +22,23 @@ import (
const global = "global"
func TestCrud(t *testing.T) {
// this cannot run in parallel because it relies on envvar
// globals.BOUNDARY_DEVELOPER_ENABLE_EVENTS
event.TestEnableEventing(t, true)
assert, require := assert.New(t), require.New(t)
tc := controller.NewTestController(t, nil)
eventConfig := event.TestEventerConfig(t, "TestCrud", event.TestWithAuditSink(t))
testLock := &sync.Mutex{}
testLogger := hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
})
require.NoError(event.InitSysEventer(testLogger, testLock, "TestCrud", event.WithEventerConfig(&eventConfig.EventerConfig)))
tcConfig, err := config.DevController()
require.NoError(err)
tcConfig.Eventing = &eventConfig.EventerConfig
tc := controller.NewTestController(t, &controller.TestControllerOpts{Config: tcConfig})
defer tc.Shutdown()
client := tc.Client()
@ -35,18 +56,44 @@ func TestCrud(t *testing.T) {
assert.EqualValues(wantedVersion, u.Version)
}
require.NotNil(eventConfig.AuditEvents)
_ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls
u, err := amClient.Create(tc.Context(), "password", global,
authmethods.WithName("bar"))
require.NoError(err)
checkAuthMethod("create", u.Item, "bar", 1)
got := tests_api.CloudEventFromFile(t, eventConfig.AuditEvents.Name())
reqItem := tests_api.GetEventDetails(t, got, "request")["item"].(map[string]interface{})
tests_api.AssertRedactedValues(t, reqItem)
respItem := tests_api.GetEventDetails(t, got, "response")["item"].(map[string]interface{})
tests_api.AssertRedactedValues(t, respItem)
tests_api.AssertRedactedValues(t, respItem["attributes"])
_ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls
u, err = amClient.Read(tc.Context(), u.Item.Id)
require.NoError(err)
checkAuthMethod("read", u.Item, "bar", 1)
got = tests_api.CloudEventFromFile(t, eventConfig.AuditEvents.Name())
tests_api.AssertRedactedValues(t, tests_api.GetEventDetails(t, got, "request"))
respItem = tests_api.GetEventDetails(t, got, "response")["item"].(map[string]interface{})
tests_api.AssertRedactedValues(t, respItem)
tests_api.AssertRedactedValues(t, respItem["attributes"])
_ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls
u, err = amClient.Update(tc.Context(), u.Item.Id, u.Item.Version, authmethods.WithName("buz"))
require.NoError(err)
checkAuthMethod("update", u.Item, "buz", 2)
got = tests_api.CloudEventFromFile(t, eventConfig.AuditEvents.Name())
tests_api.AssertRedactedValues(t, tests_api.GetEventDetails(t, got, "request")["item"].(map[string]interface{}))
respItem = tests_api.GetEventDetails(t, got, "response")["item"].(map[string]interface{})
tests_api.AssertRedactedValues(t, respItem)
tests_api.AssertRedactedValues(t, respItem["attributes"])
u, err = amClient.Update(tc.Context(), u.Item.Id, u.Item.Version, authmethods.DefaultName())
require.NoError(err)
@ -55,6 +102,7 @@ func TestCrud(t *testing.T) {
_, err = amClient.Delete(tc.Context(), u.Item.Id)
require.NoError(err)
_ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls
// OIDC auth methods
u, err = amClient.Create(tc.Context(), "oidc", global,
authmethods.WithName("foo"),
@ -64,6 +112,15 @@ func TestCrud(t *testing.T) {
authmethods.WithOidcAuthMethodClientId("client-id"))
require.NoError(err)
checkAuthMethod("create", u.Item, "foo", 1)
got = tests_api.CloudEventFromFile(t, eventConfig.AuditEvents.Name())
reqItem = tests_api.GetEventDetails(t, got, "request")["item"].(map[string]interface{})
tests_api.AssertRedactedValues(t, reqItem)
tests_api.AssertRedactedValues(t, reqItem["attributes"], "client_secret")
respItem = tests_api.GetEventDetails(t, got, "response")["item"].(map[string]interface{})
tests_api.AssertRedactedValues(t, respItem)
tests_api.AssertRedactedValues(t, respItem["attributes"])
u, err = amClient.Read(tc.Context(), u.Item.Id)
require.NoError(err)
@ -82,7 +139,23 @@ func TestCrud(t *testing.T) {
}
func TestCustomMethods(t *testing.T) {
tc := controller.NewTestController(t, nil)
// this cannot run in parallel because it relies on envvar
// globals.BOUNDARY_DEVELOPER_ENABLE_EVENTS
event.TestEnableEventing(t, true)
assert, require := assert.New(t), require.New(t)
eventConfig := event.TestEventerConfig(t, "TestCrud", event.TestWithAuditSink(t))
testLock := &sync.Mutex{}
testLogger := hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
})
require.NoError(event.InitSysEventer(testLogger, testLock, "TestCrud", event.WithEventerConfig(&eventConfig.EventerConfig)))
tcConfig, err := config.DevController()
require.NoError(err)
tcConfig.Eventing = &eventConfig.EventerConfig
tc := controller.NewTestController(t, &controller.TestControllerOpts{Config: tcConfig})
defer tc.Shutdown()
client := tc.Client()
@ -104,20 +177,30 @@ func TestCustomMethods(t *testing.T) {
authmethods.WithOidcAuthMethodClientId("client-id"),
authmethods.WithOidcAuthMethodSigningAlgorithms([]string{string("EdDSA")}),
authmethods.WithOidcAuthMethodIdpCaCerts([]string{tp.CACert()}))
require.NoError(t, err)
require.NoError(err)
const newState = "active-private"
nilU, err := amClient.ChangeState(tc.Context(), u.Item.Id, u.Item.Version, newState)
require.Error(t, err)
assert.Nil(t, nilU)
require.Error(err)
assert.Nil(nilU)
_ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls
u, err = amClient.ChangeState(tc.Context(), u.Item.Id, u.Item.Version, newState, authmethods.WithOidcAuthMethodDisableDiscoveredConfigValidation(true))
require.NoError(t, err)
assert.NotNil(t, u)
assert.Equal(t, newState, u.Item.Attributes["state"])
require.NoError(err)
assert.NotNil(u)
assert.Equal(newState, u.Item.Attributes["state"])
got := tests_api.CloudEventFromFile(t, eventConfig.AuditEvents.Name())
reqDetails := tests_api.GetEventDetails(t, got, "request")
tests_api.AssertRedactedValues(t, reqDetails)
tests_api.AssertRedactedValues(t, reqDetails["attributes"])
respItem := tests_api.GetEventDetails(t, got, "response")["item"].(map[string]interface{})
tests_api.AssertRedactedValues(t, respItem)
tests_api.AssertRedactedValues(t, respItem["attributes"])
_, err = amClient.ChangeState(tc.Context(), u.Item.Id, u.Item.Version, "", authmethods.WithOidcAuthMethodDisableDiscoveredConfigValidation(true))
assert.Error(t, err)
assert.Error(err)
}
func TestErrors(t *testing.T) {

@ -0,0 +1,80 @@
package api
import (
"encoding/json"
"io/ioutil"
"testing"
"github.com/hashicorp/eventlogger/filters/encrypt"
"github.com/hashicorp/eventlogger/formatter_filters/cloudevents"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// CloudEventFromFile will marshal a single cloud event from the provided file
// name
func CloudEventFromFile(t *testing.T, fileName string) *cloudevents.Event {
t.Helper()
b, err := ioutil.ReadFile(fileName)
assert.NoError(t, err)
got := &cloudevents.Event{}
err = json.Unmarshal(b, got)
require.NoErrorf(t, err, "json: %s", string(b))
return got
}
// GetEventDetails is a testing helper will return the details from the event
// payload for a given messageType (request or response)
func GetEventDetails(t *testing.T, e *cloudevents.Event, messageType string) map[string]interface{} {
t.Helper()
require := require.New(t)
require.NotNil(e)
require.NotEmpty(messageType)
data, ok := e.Data.(map[string]interface{})
if !ok {
return nil
}
msgType, ok := data[messageType].(map[string]interface{})
if !ok {
return nil
}
details, ok := msgType["details"].(map[string]interface{})
if !ok {
return nil
}
return details
}
// AssertRedactedValues will assert that the values for the given keys within
// the data have been redacted
func AssertRedactedValues(t *testing.T, data interface{}, keys ...string) {
t.Helper()
assert, require := assert.New(t), require.New(t)
require.NotNil(data)
dataMap, ok := data.(map[string]interface{})
require.Truef(ok, "data must be a map[string]interface{}")
rMap := make(map[string]bool, len(keys))
for _, s := range keys {
rMap[s] = true
}
for k, v := range dataMap {
switch typ := v.(type) {
case []interface{}:
for _, s := range typ {
if _, ok := rMap[k]; ok {
assert.Equalf(encrypt.RedactedData, s, "expected %s to be redacted and it was set to: %s", k, v)
} else {
assert.NotEqualf(encrypt.RedactedData, s, "did not expect %s to be redacted", k)
}
}
default:
if _, ok := rMap[k]; ok {
assert.Equalf(encrypt.RedactedData, v, "expected %s to be redacted and it was set to: %s", k, v)
} else {
assert.NotEqualf(encrypt.RedactedData, v, "did not expect %s to be redacted", k)
}
}
}
}

@ -7,7 +7,7 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/hashicorp/eventlogger v0.1.0
github.com/hashicorp/eventlogger/filters/encrypt v0.1.3
github.com/hashicorp/eventlogger/filters/encrypt v0.1.5
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-kms-wrapping v0.6.6
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect

@ -237,8 +237,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/eventlogger v0.1.0 h1:S6xc4gZVzewuDUP4R4Ngko419h/CGDuV/b4ADL3XLik=
github.com/hashicorp/eventlogger v0.1.0/go.mod h1:a3IXf1aEJfpCPzseTOrwKj4fVW/Qn3oEmpQeaIznzH0=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.3 h1:RKYUGplnpDoE/Cj2vupQqUJS/wWBVvmpxEYeEpGL88s=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.3/go.mod h1:8rcez7Kw1zanB0/074qnOuGu7zxmNh9Xr2ZI+K4xVIA=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.5 h1:kNkH4G6zzWlZSoI1I+B/ud4chVKTPL516C6jB7dRdlE=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.5/go.mod h1:8rcez7Kw1zanB0/074qnOuGu7zxmNh9Xr2ZI+K4xVIA=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=

@ -452,9 +452,9 @@ type OidcAuthMethodAuthenticateStartResponse struct {
unknownFields protoimpl.UnknownFields
// The returned authentication URL
AuthUrl string `protobuf:"bytes,10,opt,name=auth_url,proto3" json:"auth_url,omitempty"`
AuthUrl string `protobuf:"bytes,10,opt,name=auth_url,proto3" json:"auth_url,omitempty" class:"public"` // @gotags: `class:"public"`
// The returned token ID
TokenId string `protobuf:"bytes,30,opt,name=token_id,proto3" json:"token_id,omitempty"`
TokenId string `protobuf:"bytes,30,opt,name=token_id,proto3" json:"token_id,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *OidcAuthMethodAuthenticateStartResponse) Reset() {
@ -510,13 +510,13 @@ type OidcAuthMethodAuthenticateCallbackRequest struct {
unknownFields protoimpl.UnknownFields
// The returned code
Code string `protobuf:"bytes,10,opt,name=code,proto3" json:"code,omitempty"`
Code string `protobuf:"bytes,10,opt,name=code,proto3" json:"code,omitempty" class:"secret"` // @gotags: `class:"secret"`
// The returned state
State string `protobuf:"bytes,20,opt,name=state,proto3" json:"state,omitempty"`
State string `protobuf:"bytes,20,opt,name=state,proto3" json:"state,omitempty" class:"public"` // @gotags: `class:"public"`
// Error parameters, if they are returned
Error string `protobuf:"bytes,30,opt,name=error,proto3" json:"error,omitempty"`
ErrorDescription string `protobuf:"bytes,40,opt,name=error_description,proto3" json:"error_description,omitempty"`
ErrorUri string `protobuf:"bytes,50,opt,name=error_uri,proto3" json:"error_uri,omitempty"`
Error string `protobuf:"bytes,30,opt,name=error,proto3" json:"error,omitempty" class:"public"` // @gotags: `class:"public"`
ErrorDescription string `protobuf:"bytes,40,opt,name=error_description,proto3" json:"error_description,omitempty" class:"public"` // @gotags: `class:"public"`
ErrorUri string `protobuf:"bytes,50,opt,name=error_uri,proto3" json:"error_uri,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *OidcAuthMethodAuthenticateCallbackRequest) Reset() {
@ -593,7 +593,7 @@ type OidcAuthMethodAuthenticateCallbackResponse struct {
unknownFields protoimpl.UnknownFields
// The final redirection URL
FinalRedirectUrl string `protobuf:"bytes,10,opt,name=final_redirect_url,proto3" json:"final_redirect_url,omitempty"`
FinalRedirectUrl string `protobuf:"bytes,10,opt,name=final_redirect_url,proto3" json:"final_redirect_url,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *OidcAuthMethodAuthenticateCallbackResponse) Reset() {
@ -642,7 +642,7 @@ type OidcAuthMethodAuthenticateTokenRequest struct {
unknownFields protoimpl.UnknownFields
// The ID of the pending token
TokenId string `protobuf:"bytes,10,opt,name=token_id,proto3" json:"token_id,omitempty"`
TokenId string `protobuf:"bytes,10,opt,name=token_id,proto3" json:"token_id,omitempty" class:"private"` // @gotags: `class:"private"`
}
func (x *OidcAuthMethodAuthenticateTokenRequest) Reset() {
@ -693,7 +693,7 @@ type OidcAuthMethodAuthenticateTokenResponse struct {
// The status. This will always be "unknown". It will never be forwarded to
// the consumer.
Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty"`
Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *OidcAuthMethodAuthenticateTokenResponse) Reset() {

@ -45,6 +45,10 @@ func (req *AuthMethod) Tags() ([]encrypt.PointerTag, error) {
Pointer: "/Attributes/Fields/signing_algorithms",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/idp_ca_certs",
Classification: encrypt.PublicClassification,
},
{
Pointer: "/Attributes/Fields/api_url_prefix",
Classification: encrypt.PublicClassification,

@ -6,9 +6,9 @@ import (
"testing"
"time"
"github.com/hashicorp/boundary/sdk/pbs/controller/api"
"github.com/hashicorp/boundary/sdk/wrapper"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/eventlogger/filters/encrypt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
@ -19,11 +19,7 @@ func TestAuthMethod_Tags(t *testing.T) {
ctx := context.Background()
now := time.Now()
wrapper := wrapper.TestWrapper(t)
testEncryptingFilter := &encrypt.Filter{
Wrapper: wrapper,
HmacSalt: []byte("salt"),
HmacInfo: []byte("info"),
}
testEncryptingFilter := api.NewEncryptFilter(t, wrapper)
tests := []struct {
name string
@ -49,6 +45,7 @@ func TestAuthMethod_Tags(t *testing.T) {
"client_secret_hmac": structpb.NewStringValue("public-client_secret_hmac"),
"max_age": structpb.NewStringValue("public-max_age"),
"signing_algorithms": structpb.NewStringValue("public-signing_algorithms"),
"idp_ca_certs": structpb.NewStringValue("public-signing_algorithms"),
"api_url_prefix": structpb.NewStringValue("public-api_url_prefix"),
"callback_url": structpb.NewStringValue("public-callback_url"),
"allowed_audiences": structpb.NewStringValue("public-allowed_audiences"),
@ -87,6 +84,7 @@ func TestAuthMethod_Tags(t *testing.T) {
"client_secret_hmac": structpb.NewStringValue("public-client_secret_hmac"),
"max_age": structpb.NewStringValue("public-max_age"),
"signing_algorithms": structpb.NewStringValue("public-signing_algorithms"),
"idp_ca_certs": structpb.NewStringValue("public-signing_algorithms"),
"api_url_prefix": structpb.NewStringValue("public-api_url_prefix"),
"callback_url": structpb.NewStringValue("public-callback_url"),
"allowed_audiences": structpb.NewStringValue("public-allowed_audiences"),
@ -119,7 +117,7 @@ func TestAuthMethod_Tags(t *testing.T) {
ScopeId: "scope-id",
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "description"},
Type: "oidc",
Type: "password",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"min_login_name_length": structpb.NewStringValue("public-min_login_name_length"),
@ -145,7 +143,7 @@ func TestAuthMethod_Tags(t *testing.T) {
ScopeId: "scope-id",
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "description"},
Type: "oidc",
Type: "password",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"min_login_name_length": structpb.NewStringValue("public-min_login_name_length"),

@ -0,0 +1,23 @@
package api
import (
"reflect"
"testing"
"github.com/hashicorp/eventlogger/filters/encrypt"
wrapping "github.com/hashicorp/go-kms-wrapping"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// NewEncryptFilter is a copy of event.NewEncryptFilter since importing it would
// case circular deps. The primary reason for this test func is to make sure
// the proper IgnoreTypes are included for testing.
func NewEncryptFilter(t *testing.T, w wrapping.Wrapper) *encrypt.Filter {
t.Helper()
return &encrypt.Filter{
Wrapper: w,
IgnoreTypes: []reflect.Type{
reflect.TypeOf(&fieldmaskpb.FieldMask{}),
},
}
}
Loading…
Cancel
Save