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 }