481 lines
13 KiB
Go
481 lines
13 KiB
Go
// 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
|
|
}
|