blob: 189c960098d3195b6eca53accfbe590868f6e690 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package authmetadata
import (
"context"
"fmt"
"reflect"
"testing"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
type environment struct {
ctx context.Context
storage logical.Storage
backend logical.Backend
}
func TestAcceptance(t *testing.T) {
ctx := context.Background()
storage := &logical.InmemStorage{}
b, err := backend(ctx, storage)
if err != nil {
t.Fatal(err)
}
env := &environment{
ctx: ctx,
storage: storage,
backend: b,
}
t.Run("test initial fields are default", env.TestInitialFieldsAreDefault)
t.Run("test fields can be unset", env.TestAuthMetadataCanBeUnset)
t.Run("test defaults can be restored", env.TestDefaultCanBeReused)
t.Run("test default plus more cannot be selected", env.TestDefaultPlusMoreCannotBeSelected)
t.Run("test only non-defaults can be selected", env.TestOnlyNonDefaultsCanBeSelected)
t.Run("test bad field results in useful error", env.TestAddingBadField)
}
func (e *environment) TestInitialFieldsAreDefault(t *testing.T) {
// On the first read of auth_metadata, when nothing has been touched,
// we should receive the default field(s) if a read is performed.
resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.ReadOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Data == nil {
t.Fatal("expected non-nil response")
}
if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"role_name"}) {
t.Fatal("expected default field of role_name to be returned")
}
// The auth should only have the default metadata.
resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
"role_name": "something",
},
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil {
t.Fatalf("expected alias metadata")
}
if len(resp.Auth.Alias.Metadata) != 1 {
t.Fatal("expected only 1 field")
}
if resp.Auth.Alias.Metadata["role_name"] != "something" {
t.Fatal("expected role_name to be something")
}
}
func (e *environment) TestAuthMetadataCanBeUnset(t *testing.T) {
// We should be able to set the auth_metadata to empty by sending an
// explicitly empty array.
resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
authMetadataFields.FieldName: []string{},
},
})
if err != nil {
t.Fatal(err)
}
if resp != nil {
t.Fatal("expected nil response")
}
// Now we should receive no fields for auth_metadata.
resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.ReadOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Data == nil {
t.Fatal("expected non-nil response")
}
if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{}) {
t.Fatal("expected no fields to be returned")
}
// The auth should have no metadata.
resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
"role_name": "something",
},
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil {
t.Fatal("expected alias metadata")
}
if len(resp.Auth.Alias.Metadata) != 0 {
t.Fatal("expected 0 fields")
}
}
func (e *environment) TestDefaultCanBeReused(t *testing.T) {
// Now if we set it to "default", the default fields should
// be restored.
resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
authMetadataFields.FieldName: []string{"default"},
},
})
if err != nil {
t.Fatal(err)
}
if resp != nil {
t.Fatal("expected nil response")
}
// Let's make sure we've returned to the default fields.
resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.ReadOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Data == nil {
t.Fatal("expected non-nil response")
}
if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"role_name"}) {
t.Fatal("expected default field of role_name to be returned")
}
// We should again only receive the default field on the login.
resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
"role_name": "something",
},
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil {
t.Fatal("expected alias metadata")
}
if len(resp.Auth.Alias.Metadata) != 1 {
t.Fatal("expected only 1 field")
}
if resp.Auth.Alias.Metadata["role_name"] != "something" {
t.Fatal("expected role_name to be something")
}
}
func (e *environment) TestDefaultPlusMoreCannotBeSelected(t *testing.T) {
// We should not be able to set it to "default" plus 1 optional field.
_, err := e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
authMetadataFields.FieldName: []string{"default", "remote_addr"},
},
})
if err == nil {
t.Fatal("expected err")
}
}
func (e *environment) TestOnlyNonDefaultsCanBeSelected(t *testing.T) {
// Omit all default fields and just select one.
resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
authMetadataFields.FieldName: []string{"remote_addr"},
},
})
if err != nil {
t.Fatal(err)
}
if resp != nil {
t.Fatal("expected nil response")
}
// Make sure that worked.
resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.ReadOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Data == nil {
t.Fatal("expected non-nil response")
}
if !reflect.DeepEqual(resp.Data[authMetadataFields.FieldName], []string{"remote_addr"}) {
t.Fatal("expected remote_addr to be returned")
}
// Ensure only the selected one is on logins.
// They both should now appear on the login.
resp, err = e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
"role_name": "something",
},
})
if err != nil {
t.Fatal(err)
}
if resp == nil || resp.Auth == nil || resp.Auth.Alias == nil || resp.Auth.Alias.Metadata == nil {
t.Fatal("expected alias metadata")
}
if len(resp.Auth.Alias.Metadata) != 1 {
t.Fatal("expected only 1 field")
}
if resp.Auth.Alias.Metadata["remote_addr"] != "http://foo.com" {
t.Fatal("expected remote_addr to be http://foo.com")
}
}
func (e *environment) TestAddingBadField(t *testing.T) {
// Try adding an unsupported field.
resp, err := e.backend.HandleRequest(e.ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Storage: e.storage,
Connection: &logical.Connection{
RemoteAddr: "http://foo.com",
},
Data: map[string]interface{}{
authMetadataFields.FieldName: []string{"asl;dfkj"},
},
})
if err == nil {
t.Fatal("expected err")
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if !resp.IsError() {
t.Fatal("expected error response")
}
}
// We expect people to embed the Handler on their
// config so it automatically makes its helper methods
// available and easy to find wherever the config is
// needed. Explicitly naming it in json avoids it
// automatically being named "Handler" by Go's JSON
// marshalling library.
type fakeConfig struct {
*Handler `json:"auth_metadata_handler"`
}
type fakeBackend struct {
*framework.Backend
}
// We expect each back-end to explicitly define the fields that
// will be included by default, and optionally available.
var authMetadataFields = &Fields{
FieldName: "some_field_name",
Default: []string{
"role_name", // This would likely never change because the alias is the role name.
},
AvailableToAdd: []string{
"remote_addr", // This would likely change with every new caller.
},
}
func configPath() *framework.Path {
return &framework.Path{
Pattern: "config",
Fields: map[string]*framework.FieldSchema{
authMetadataFields.FieldName: FieldSchema(authMetadataFields),
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) {
entryRaw, err := req.Storage.Get(ctx, "config")
if err != nil {
return nil, err
}
conf := &fakeConfig{
Handler: NewHandler(authMetadataFields),
}
if entryRaw != nil {
if err := entryRaw.DecodeJSON(conf); err != nil {
return nil, err
}
}
// Note that even if the config entry was nil, we return
// a populated response to give info on what the default
// auth metadata is when unconfigured.
return &logical.Response{
Data: map[string]interface{}{
authMetadataFields.FieldName: conf.AuthMetadata(),
},
}, nil
},
},
logical.UpdateOperation: &framework.PathOperation{
Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) {
entryRaw, err := req.Storage.Get(ctx, "config")
if err != nil {
return nil, err
}
conf := &fakeConfig{
Handler: NewHandler(authMetadataFields),
}
if entryRaw != nil {
if err := entryRaw.DecodeJSON(conf); err != nil {
return nil, err
}
}
// This is where we read in the user's given auth metadata.
if err := conf.ParseAuthMetadata(fd); err != nil {
// Since this will only error on bad input, it's best to give
// a 400 response with the explicit problem included.
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
entry, err := logical.StorageEntryJSON("config", conf)
if err != nil {
return nil, err
}
if err = req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return nil, nil
},
},
},
}
}
func loginPath() *framework.Path {
return &framework.Path{
Pattern: "login",
Fields: map[string]*framework.FieldSchema{
"role_name": {
Type: framework.TypeString,
Required: true,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: func(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) {
entryRaw, err := req.Storage.Get(ctx, "config")
if err != nil {
return nil, err
}
conf := &fakeConfig{
Handler: NewHandler(authMetadataFields),
}
if entryRaw != nil {
if err := entryRaw.DecodeJSON(conf); err != nil {
return nil, err
}
}
auth := &logical.Auth{
Alias: &logical.Alias{
Name: fd.Get("role_name").(string),
},
}
// Here we provide everything and let the method strip out
// the undesired stuff.
if err := conf.PopulateDesiredMetadata(auth, map[string]string{
"role_name": fd.Get("role_name").(string),
"remote_addr": req.Connection.RemoteAddr,
}); err != nil {
fmt.Println("unable to populate due to " + err.Error())
}
return &logical.Response{
Auth: auth,
}, nil
},
},
},
}
}
func backend(ctx context.Context, storage logical.Storage) (logical.Backend, error) {
b := &fakeBackend{
Backend: &framework.Backend{
Paths: []*framework.Path{
configPath(),
loginPath(),
},
},
}
if err := b.Setup(ctx, &logical.BackendConfig{
StorageView: storage,
Logger: hclog.Default(),
}); err != nil {
return nil, err
}
return b, nil
}