From f84991c0f14b6fefd9dd32132bfc9940e399b0f7 Mon Sep 17 00:00:00 2001 From: Todd Knight Date: Fri, 7 Aug 2020 10:18:40 -0700 Subject: [PATCH] Use authenticate and Auth Method repo functions (#242) --- internal/auth/auth.go | 26 +-- internal/auth/password/options.go | 8 + internal/auth/password/options_test.go | 6 + .../auth/password/repository_authmethod.go | 25 ++- .../password/repository_authmethod_test.go | 36 +++++ internal/cmd/base/servers.go | 74 ++++++--- .../cmd/commands/controller/controller.go | 40 +++-- internal/cmd/commands/dev/dev.go | 31 +++- internal/gen/controller.swagger.json | 2 +- .../resources/authmethods/auth_method.pb.go | 2 +- .../authmethods/v1/auth_method.proto | 2 +- internal/servers/controller/handler.go | 2 +- internal/servers/controller/handler_test.go | 11 +- .../authenticate/authenticate_service.go | 78 ++++----- .../authenticate/authenticate_service_test.go | 150 ++++++++---------- .../controller/handlers/subtypes_test.go | 2 +- internal/servers/controller/testing.go | 13 ++ 17 files changed, 324 insertions(+), 184 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 63855bf94..98efdf6b9 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -367,19 +367,21 @@ func (v verifier) performAuthCheck() (aclResults *perms.ACLResults, userId strin userId = "u_anon" // Validate the token and fetch the corresponding user ID - tokenRepo, err := v.authTokenRepoFn() - if err != nil { - retErr = fmt.Errorf("perform auth check: failed to get authtoken repo: %w", err) - return - } + if v.requestInfo.TokenFormat != AuthTokenTypeUnknown { + tokenRepo, err := v.authTokenRepoFn() + if err != nil { + retErr = fmt.Errorf("perform auth check: failed to get authtoken repo: %w", err) + return + } - at, err := tokenRepo.ValidateToken(v.ctx, v.requestInfo.PublicId, v.requestInfo.Token) - if err != nil { - retErr = fmt.Errorf("perform auth check: failed to validate token: %w", err) - return - } - if at != nil { - userId = at.GetIamUserId() + at, err := tokenRepo.ValidateToken(v.ctx, v.requestInfo.PublicId, v.requestInfo.Token) + if err != nil { + retErr = fmt.Errorf("perform auth check: failed to validate token: %w", err) + return + } + if at != nil { + userId = at.GetIamUserId() + } } // Fetch and parse grants for this user ID (which may include grants for diff --git a/internal/auth/password/options.go b/internal/auth/password/options.go index dcf2519be..375936ede 100644 --- a/internal/auth/password/options.go +++ b/internal/auth/password/options.go @@ -18,6 +18,7 @@ type options struct { withDescription string withLimit int withConfig Configuration + withPublicId string password string withPassword bool } @@ -28,6 +29,13 @@ func getDefaultOptions() options { } } +// WithPublicId provides an optional public id +func WithPublicId(id string) Option { + return func(o *options) { + o.withPublicId = id + } +} + // WithDescription provides an optional description. func WithDescription(desc string) Option { return func(o *options) { diff --git a/internal/auth/password/options_test.go b/internal/auth/password/options_test.go index 3609c5f2d..01aca8896 100644 --- a/internal/auth/password/options_test.go +++ b/internal/auth/password/options_test.go @@ -9,6 +9,12 @@ import ( func Test_GetOpts(t *testing.T) { t.Parallel() + t.Run("WithPublicId", func(t *testing.T) { + opts := getOpts(WithPublicId("test id")) + testOpts := getDefaultOptions() + testOpts.withPublicId = "test id" + assert.Equal(t, opts, testOpts) + }) t.Run("WithName", func(t *testing.T) { opts := getOpts(WithName("test")) testOpts := getDefaultOptions() diff --git a/internal/auth/password/repository_authmethod.go b/internal/auth/password/repository_authmethod.go index e60dd6e05..2dbecfbed 100644 --- a/internal/auth/password/repository_authmethod.go +++ b/internal/auth/password/repository_authmethod.go @@ -16,8 +16,8 @@ import ( // contain a valid ScopeId. m must not contain a PublicId. The PublicId is // generated and assigned by this method. // -// WithConfiguration is the only valid option. All other options are -// ignored. +// WithConfiguration and WithPublicId are the only valid options. All other +// options are ignored. // // Both m.Name and m.Description are optional. If m.Name is set, it must be // unique within m.ScopeId. @@ -36,13 +36,21 @@ func (r *Repository) CreateAuthMethod(ctx context.Context, m *AuthMethod, opt .. } m = m.clone() - id, err := newAuthMethodId() - if err != nil { - return nil, fmt.Errorf("create: password auth method: %w", err) - } - m.PublicId = id - opts := getOpts(opt...) + + if opts.withPublicId != "" { + if !strings.HasPrefix(opts.withPublicId, AuthMethodPrefix+"_") { + return nil, fmt.Errorf("create: password auth method: passed-in public ID %q has wrong prefix, should be %q: %w", opts.withPublicId, AuthMethodPrefix, db.ErrInvalidPublicId) + } + m.PublicId = opts.withPublicId + } else { + id, err := newAuthMethodId() + if err != nil { + return nil, fmt.Errorf("create: password auth method: %w", err) + } + m.PublicId = id + } + c, ok := opts.withConfig.(*Argon2Configuration) if !ok { return nil, fmt.Errorf("create: password auth method: unknown configuration: %w", ErrUnsupportedConfiguration) @@ -51,6 +59,7 @@ func (r *Repository) CreateAuthMethod(ctx context.Context, m *AuthMethod, opt .. return nil, fmt.Errorf("create: password auth method: %w", err) } + var err error c.PrivateId, err = newArgon2ConfigurationId() if err != nil { return nil, fmt.Errorf("create: password auth method: %w", err) diff --git a/internal/auth/password/repository_authmethod_test.go b/internal/auth/password/repository_authmethod_test.go index 91734b0ba..eaef678cf 100644 --- a/internal/auth/password/repository_authmethod_test.go +++ b/internal/auth/password/repository_authmethod_test.go @@ -215,6 +215,42 @@ func TestRepository_CreateAuthMethod(t *testing.T) { assert.Equal(in2.Description, got2.Description) assert.Equal(got2.CreateTime, got2.UpdateTime) }) + + t.Run("valid-with-publicid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, wrapper) + require.NoError(err) + require.NotNil(repo) + + org1, _ := iam.TestScopes(t, conn) + in := allocAuthMethod() + + amId, err := newAuthMethodId() + require.NoError(err) + + in.ScopeId = org1.GetPublicId() + got, err := repo.CreateAuthMethod(context.Background(), &in, WithPublicId(amId)) + require.NoError(err) + require.NotNil(got) + assert.Equal(amId, got.GetPublicId()) + assert.Equal(got.CreateTime, got.UpdateTime) + }) + + t.Run("invalid-with-badpublicid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, wrapper) + require.NoError(err) + require.NotNil(repo) + + org1, _ := iam.TestScopes(t, conn) + in := allocAuthMethod() + + in.ScopeId = org1.GetPublicId() + got, err := repo.CreateAuthMethod(context.Background(), &in, WithPublicId("invalid_idwithabadprefix")) + assert.Error(err) + assert.Nil(got) + assert.True(errors.Is(err, db.ErrInvalidPublicId)) + }) } func TestRepository_LookupAuthMethod(t *testing.T) { diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index c97c5d679..5a01df57f 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -21,13 +21,14 @@ import ( "github.com/hashicorp/vault/internalshared/configutil" "github.com/hashicorp/vault/internalshared/gatedwriter" "github.com/hashicorp/vault/internalshared/reloadutil" + "github.com/hashicorp/vault/sdk/helper/base62" "github.com/hashicorp/vault/sdk/helper/logging" "github.com/hashicorp/vault/sdk/helper/mlock" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/watchtower/globals" + "github.com/hashicorp/watchtower/internal/auth/password" "github.com/hashicorp/watchtower/internal/db" "github.com/hashicorp/watchtower/internal/iam" - "github.com/hashicorp/watchtower/internal/servers/controller/handlers/authenticate" "github.com/hashicorp/watchtower/version" "github.com/jinzhu/gorm" "github.com/mitchellh/cli" @@ -62,6 +63,8 @@ type Server struct { DefaultOrgId string DevAuthMethodId string + DevUsername string + DevPassword string DevDatabaseUrl string DevDatabaseCleanupFunc func() error @@ -381,8 +384,13 @@ func (b *Server) RunShutdownFuncs() error { func (b *Server) CreateDevDatabase(dialect string) error { c, url, container, err := db.InitDbInDocker(dialect) - if err != nil { + // In case of an error, run the cleanup function. If we pass all errors, c should be set to a noop + // function before returning from this method + defer func() { c() + }() + + if err != nil { return fmt.Errorf("unable to start dev database with dialect %s: %w", dialect, err) } @@ -398,7 +406,6 @@ func (b *Server) CreateDevDatabase(dialect string) error { dbase, err := gorm.Open(dialect, url) if err != nil { - c() return fmt.Errorf("unable to create db object with dialect %s: %w", dialect, err) } b.Database = dbase @@ -410,7 +417,6 @@ func (b *Server) CreateDevDatabase(dialect string) error { rw := db.New(b.Database) repo, err := iam.NewRepository(rw, rw, b.ControllerKMS) if err != nil { - c() return fmt.Errorf("unable to create repo for org id: %w", err) } @@ -424,7 +430,6 @@ func (b *Server) CreateDevDatabase(dialect string) error { if b.DefaultOrgId != "" { orgScope, err = repo.LookupScope(ctx, b.DefaultOrgId) if err != nil { - c() return fmt.Errorf("error looking up existing scope with org ID %q: %w", b.DefaultOrgId, err) } } @@ -432,17 +437,14 @@ func (b *Server) CreateDevDatabase(dialect string) error { if orgScope == nil { orgScope, err = iam.NewOrg() if err != nil { - c() return fmt.Errorf("error creating new org scope: %w", err) } orgScope, err = repo.CreateScope(ctx, orgScope, iam.WithPublicId(b.DefaultOrgId)) if err != nil { - c() return fmt.Errorf("error persisting new org scope: %w", err) } if b.DefaultOrgId != "" { if orgScope.GetPublicId() != b.DefaultOrgId { - c() return fmt.Errorf("expected org ID %q, got %q after persisting", b.DefaultOrgId, orgScope.GetPublicId()) } } else { @@ -480,27 +482,59 @@ func (b *Server) CreateDevDatabase(dialect string) error { return fmt.Errorf("error adding principal to role for default dev grants: %w", err) } - // TODO: Remove this when Auth Account repo is in place. - authenticate.Scope = orgScope.GetPublicId() - insert := `insert into auth_method - (public_id, scope_id) - values - ($1, $2);` + pwRepo, err := password.NewRepository(rw, rw, b.ControllerKMS) + if err != nil { + return fmt.Errorf("error creating password repo: %w", err) + } + authMethod, err := password.NewAuthMethod(orgScope.GetPublicId()) + if err != nil { + return fmt.Errorf("error creating new in memory auth method: %w", err) + } + amId := b.DevAuthMethodId if amId == "" { - amId = "am_1234567890" + amId = "paum_1234567890" } - authenticate.RWDb.Store(rw) - _, err = b.Database.DB().Exec(insert, amId, orgScope.GetPublicId()) + authMethod, err = pwRepo.CreateAuthMethod(ctx, authMethod, password.WithPublicId(amId)) if err != nil { - c() - return err + return fmt.Errorf("error saving auth method to the db: %w", err) } - b.InfoKeys = append(b.InfoKeys, "dev org id", "dev auth method id") + acctUserName := b.DevUsername + if acctUserName == "" { + acctUserName, err = base62.Random(10) + if err != nil { + return fmt.Errorf("unable to generate dev username: %w", err) + } + acctUserName = strings.ToLower(acctUserName) + } + + pw := b.DevPassword + if pw == "" { + pw, err = base62.Random(20) + if err != nil { + return fmt.Errorf("unable to generate dev password: %w", err) + } + } + + acct, err := password.NewAccount(amId, acctUserName) + if err != nil { + return fmt.Errorf("error creating new in memory auth account: %w", err) + } + acct, err = pwRepo.CreateAccount(ctx, acct, password.WithPassword(pw)) + if err != nil { + return fmt.Errorf("error saving auth account to the db: %w", err) + } + + b.InfoKeys = append(b.InfoKeys, "dev org id", "dev auth method id", "dev username", "dev password") b.Info["dev org id"] = b.DefaultOrgId b.Info["dev auth method id"] = amId + b.Info["dev username"] = acct.GetUserName() + b.Info["dev password"] = pw + // now that we have passed all the error cases, reset c to be a noop so the + // defer doesn't do anything. + c = func() error { return nil } return nil } diff --git a/internal/cmd/commands/controller/controller.go b/internal/cmd/commands/controller/controller.go index fe67b1dde..3cd4b7c23 100644 --- a/internal/cmd/commands/controller/controller.go +++ b/internal/cmd/commands/controller/controller.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/vault/sdk/helper/mlock" "github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/watchtower/globals" + "github.com/hashicorp/watchtower/internal/auth/password" "github.com/hashicorp/watchtower/internal/cmd/base" "github.com/hashicorp/watchtower/internal/cmd/config" "github.com/hashicorp/watchtower/internal/servers/controller" @@ -44,7 +45,8 @@ type Command struct { flagLogFormat string flagCombineLogs bool flagDev bool - flagDevAdminPassword string + flagDevUsername string + flagDevPassword string flagDevControllerAPIListenAddr string flagDevControllerClusterListenAddr string flagDevOrgId string @@ -129,13 +131,21 @@ func (c *Command) Flags() *base.FlagSets { }) f.StringVar(&base.StringVar{ - Name: "dev-admin-password", - Target: &c.flagDevAdminPassword, - EnvVar: "WATCHTWER_DEV_ADMIN_PASSWORD", + Name: "dev-password", + Target: &c.flagDevPassword, + EnvVar: "WATCHTWER_DEV_PASSWORD", Usage: "Initial admin password. This only applies when running in \"dev\" " + "mode.", }) + f.StringVar(&base.StringVar{ + Name: "dev-username", + Target: &c.flagDevUsername, + EnvVar: "WATCHTWER_DEV_USERNAME", + Usage: "Initial admin username. This only applies when running in \"dev\" " + + "mode.", + }) + f.StringVar(&base.StringVar{ Name: "dev-api-listen-address", Target: &c.flagDevControllerAPIListenAddr, @@ -322,11 +332,16 @@ func (c *Command) ParseFlagsAndConfig(args []string) int { case len(c.flagConfig) == 0: c.UI.Error("Must specify a config file using -config") return 1 - case c.flagDevAdminPassword != "": + case c.flagDevPassword != "": c.UI.Warn(base.WrapAtLength( "You cannot specify a custom admin password outside of \"dev\" mode. " + "Your request has been ignored.")) - c.flagDevAdminPassword = "" + c.flagDevPassword = "" + case c.flagDevUsername != "": + c.UI.Warn(base.WrapAtLength( + "You cannot specify a custom admin username outside of \"dev\" mode. " + + "Your request has been ignored.")) + c.flagDevUsername = "" } c.Config, err = config.LoadFile(c.flagConfig, c.configKMS) @@ -346,16 +361,23 @@ func (c *Command) ParseFlagsAndConfig(args []string) int { c.Config.DefaultOrgId = c.flagDevOrgId } if c.flagDevAuthMethodId != "" { - if !strings.HasPrefix(c.flagDevAuthMethodId, "am_") { - c.UI.Error(fmt.Sprintf("Invalid dev auth method ID, must start with %q", "am_")) + prefix := password.AuthMethodPrefix + "_" + if !strings.HasPrefix(c.flagDevAuthMethodId, prefix) { + c.UI.Error(fmt.Sprintf("Invalid dev auth method ID, must start with %q", prefix)) return 1 } if len(c.flagDevAuthMethodId) != 13 { - c.UI.Error(fmt.Sprintf("Invalid dev auth method ID, must be 10 base62 characters after %q", "am_")) + c.UI.Error(fmt.Sprintf("Invalid dev auth method ID, must be 10 base62 characters after %q", prefix)) return 1 } c.DevAuthMethodId = c.flagDevAuthMethodId } + if c.flagDevUsername != "" { + c.DevUsername = c.flagDevUsername + } + if c.flagDevPassword != "" { + c.DevPassword = c.flagDevPassword + } c.Config.PassthroughDirectory = c.flagDevPassthroughDirectory diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index 19a5e4ad8..02b61cfca 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -6,6 +6,7 @@ import ( "strings" "sync" + "github.com/hashicorp/watchtower/internal/auth/password" "github.com/hashicorp/watchtower/internal/cmd/base" controllercmd "github.com/hashicorp/watchtower/internal/cmd/commands/controller" workercmd "github.com/hashicorp/watchtower/internal/cmd/commands/worker" @@ -34,7 +35,8 @@ type Command struct { flagLogFormat string flagCombineLogs bool flagDev bool - flagDevAdminPassword string + flagDevUsername string + flagDevPassword string flagDevOrgId string flagDevAuthMethodId string flagDevControllerAPIListenAddr string @@ -102,13 +104,21 @@ func (c *Command) Flags() *base.FlagSets { }) f.StringVar(&base.StringVar{ - Name: "dev-admin-password", - Target: &c.flagDevAdminPassword, - EnvVar: "WATCHTWER_DEV_ADMIN_PASSWORD", + Name: "dev-password", + Target: &c.flagDevPassword, + EnvVar: "WATCHTWER_DEV_PASSWORD", Usage: "Initial admin password. This only applies when running in \"dev\" " + "mode.", }) + f.StringVar(&base.StringVar{ + Name: "dev-username", + Target: &c.flagDevUsername, + EnvVar: "WATCHTWER_DEV_USERNAME", + Usage: "Initial admin username. This only applies when running in \"dev\" " + + "mode.", + }) + f.StringVar(&base.StringVar{ Name: "dev-api-listen-address", Target: &c.flagDevControllerAPIListenAddr, @@ -165,16 +175,23 @@ func (c *Command) Run(args []string) int { devConfig.DefaultOrgId = c.flagDevOrgId } if c.flagDevAuthMethodId != "" { - if !strings.HasPrefix(c.flagDevAuthMethodId, "am_") { - c.UI.Error(fmt.Sprintf("Invalid dev auth method ID, must start with %q", "am_")) + prefix := password.AuthMethodPrefix + "_" + if !strings.HasPrefix(c.flagDevAuthMethodId, prefix) { + c.UI.Error(fmt.Sprintf("Invalid dev auth method ID, must start with %q", prefix)) return 1 } if len(c.flagDevAuthMethodId) != 13 { - c.UI.Error(fmt.Sprintf("Invalid dev auth method ID, must be 10 base62 characters after %q", "am_")) + c.UI.Error(fmt.Sprintf("Invalid dev auth method ID, must be 10 base62 characters after %q", prefix)) return 1 } c.DevAuthMethodId = c.flagDevAuthMethodId } + if c.flagDevUsername != "" { + c.DevUsername = c.flagDevUsername + } + if c.flagDevPassword != "" { + c.DevPassword = c.flagDevPassword + } devConfig.PassthroughDirectory = c.flagDevPassthroughDirectory diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index af2b2624f..4790e7ca0 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -1879,7 +1879,7 @@ }, "description": { "type": "string", - "title": "Optional user-set descripton for identification purposes" + "title": "Optional user-set description for identification purposes" }, "created_time": { "type": "string", diff --git a/internal/gen/controller/api/resources/authmethods/auth_method.pb.go b/internal/gen/controller/api/resources/authmethods/auth_method.pb.go index 14a24d032..e46460b14 100644 --- a/internal/gen/controller/api/resources/authmethods/auth_method.pb.go +++ b/internal/gen/controller/api/resources/authmethods/auth_method.pb.go @@ -44,7 +44,7 @@ type AuthMethod struct { Scope *scopes.ScopeInfo `protobuf:"bytes,2,opt,name=scope,proto3" json:"scope,omitempty"` // Optional name for identification purposes Name *wrappers.StringValue `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - // Optional user-set descripton for identification purposes + // Optional user-set description for identification purposes Description *wrappers.StringValue `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // The time this resource was created // Output only. diff --git a/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto b/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto index d0bc6f38c..74c6ee910 100644 --- a/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto +++ b/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto @@ -23,7 +23,7 @@ message AuthMethod { // Optional name for identification purposes google.protobuf.StringValue name = 3 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"name" that: "name"}]; - // Optional user-set descripton for identification purposes + // Optional user-set description for identification purposes google.protobuf.StringValue description = 4 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"description" that: "description"}]; // The time this resource was created diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index e2d7a1529..b9f744207 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -89,7 +89,7 @@ func handleGrpcGateway(c *Controller) (http.Handler, error) { if err := services.RegisterAccountServiceHandlerServer(ctx, mux, accts); err != nil { return nil, fmt.Errorf("failed to register account service handler: %w", err) } - auths, err := authenticate.NewService(c.IamRepoFn, c.AuthTokenRepoFn) + auths, err := authenticate.NewService(c.PasswordAuthRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) if err != nil { return nil, fmt.Errorf("failed to create authentication handler service: %w", err) } diff --git a/internal/servers/controller/handler_test.go b/internal/servers/controller/handler_test.go index 01b7524c4..1bbfea7a9 100644 --- a/internal/servers/controller/handler_test.go +++ b/internal/servers/controller/handler_test.go @@ -13,11 +13,16 @@ import ( ) func TestAuthenticationHandler(t *testing.T) { - c := NewTestController(t, &TestControllerOpts{DefaultOrgId: "o_1234567890"}) + c := NewTestController(t, &TestControllerOpts{ + DefaultOrgId: "o_1234567890", + DisableAuthorizationFailures: true, + DefaultUsername: "admin", + DefaultPassword: "password123", + }) defer c.Shutdown() - resp, err := http.Post(fmt.Sprintf("%s/v1/scopes/o_1234567890/auth-methods/am_1234567890:authenticate", c.ApiAddrs()[0]), "application/json", - strings.NewReader(`{"token_type": null, "credentials": {"name":"test", "password": "test"}}`)) + resp, err := http.Post(fmt.Sprintf("%s/v1/scopes/o_1234567890/auth-methods/paum_1234567890:authenticate", c.ApiAddrs()[0]), "application/json", + strings.NewReader(`{"token_type": null, "credentials": {"name":"admin", "password": "password123"}}`)) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode, "Got response: %v", resp) diff --git a/internal/servers/controller/handlers/authenticate/authenticate_service.go b/internal/servers/controller/handlers/authenticate/authenticate_service.go index 6f9756d96..65fc03bc2 100644 --- a/internal/servers/controller/handlers/authenticate/authenticate_service.go +++ b/internal/servers/controller/handlers/authenticate/authenticate_service.go @@ -2,66 +2,66 @@ package authenticate import ( "context" - "crypto/sha256" - "encoding/base32" "fmt" "regexp" "strings" - "sync/atomic" + "github.com/hashicorp/watchtower/internal/auth" "github.com/hashicorp/watchtower/internal/authtoken" - "github.com/hashicorp/watchtower/internal/db" pba "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/authtokens" "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services" "github.com/hashicorp/watchtower/internal/iam" - iamStore "github.com/hashicorp/watchtower/internal/iam/store" "github.com/hashicorp/watchtower/internal/servers/controller/common" "github.com/hashicorp/watchtower/internal/servers/controller/handlers" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -const scopeIdFieldName = "scope_id" +const ( + nameKey = "name" + pwKey = "password" +) var ( reInvalidID = regexp.MustCompile("[^A-Za-z0-9]") - - // TODO: Remove once auth methods are in - Scope string - RWDb = new(atomic.Value) ) -func init() { - RWDb.Store((*db.Db)(nil)) -} - // Service handles request as described by the pbs.OrgServiceServer interface. type Service struct { + pwRepo common.PasswordAuthRepoFactory iamRepo common.IamRepoFactory authTokenRepo common.AuthTokenRepoFactory } // NewService returns an org service which handles org related requests to watchtower. -func NewService(iamRepo common.IamRepoFactory, atRepo common.AuthTokenRepoFactory) (Service, error) { +func NewService(pwRepo common.PasswordAuthRepoFactory, iamRepo common.IamRepoFactory, atRepo common.AuthTokenRepoFactory) (Service, error) { if iamRepo == nil { return Service{}, fmt.Errorf("nil iam repository provided") } if atRepo == nil { return Service{}, fmt.Errorf("nil auth token repository provided") } + if pwRepo == nil { + return Service{}, fmt.Errorf("nil password repository provided") + } - return Service{iamRepo: iamRepo, authTokenRepo: atRepo}, nil + return Service{pwRepo: pwRepo, iamRepo: iamRepo, authTokenRepo: atRepo}, nil } var _ pbs.AuthenticationServiceServer = Service{} // Authenticate implements the interface pbs.AuthenticationServiceServer. func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest) (*pbs.AuthenticateResponse, error) { + authResults := auth.Verify(ctx) + if !authResults.Valid { + return nil, handlers.ForbiddenError() + } if err := validateAuthenticateRequest(req); err != nil { return nil, err } - tok, err := s.authenticateWithRepo(ctx, req) + creds := req.GetCredentials().GetFields() + tok, err := s.authenticateWithRepo(ctx, req.GetAuthMethodId(), creds[nameKey].GetStringValue(), creds[pwKey].GetStringValue()) if err != nil { return nil, err } @@ -73,7 +73,7 @@ func (s Service) Deauthenticate(ctx context.Context, req *pbs.DeauthenticateRequ return nil, status.Error(codes.Unimplemented, "Requested method is unimplemented.") } -func (s Service) authenticateWithRepo(ctx context.Context, req *pbs.AuthenticateRequest) (*pba.AuthToken, error) { +func (s Service) authenticateWithRepo(ctx context.Context, authMethodId, name, pw string) (*pba.AuthToken, error) { iamRepo, err := s.iamRepo() if err != nil { return nil, err @@ -82,29 +82,24 @@ func (s Service) authenticateWithRepo(ctx context.Context, req *pbs.Authenticate if err != nil { return nil, err } - // Place holder for making a request to authenticate - creds := req.GetCredentials().GetFields() - pwName, password := creds["name"], creds["password"] - if pwName.GetStringValue() == "wrong" && password.GetStringValue() == "wrong" { - return nil, status.Error(codes.Unauthenticated, "Unable to authenticate.") - } - // Get back a password.Account with a CredentialId string and a public Id - // Create a fake auth account ID for the moment - acctIdBytes := sha256.Sum256([]byte(pwName.GetStringValue())) - acctId := fmt.Sprintf("aa_%s", base32.StdEncoding.EncodeToString(acctIdBytes[:])[0:10]) - - aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{ - PublicId: acctId, - ScopeId: Scope, - AuthMethodId: req.AuthMethodId, - }} - RWDb.Load().(*db.Db).Create(ctx, aAcct) - - u, err := iamRepo.LookupUserWithLogin(ctx, acctId, iam.WithAutoVivify(true)) + pwRepo, err := s.pwRepo() if err != nil { return nil, err } - tok, err := atRepo.CreateAuthToken(ctx, u.GetPublicId(), acctId) + + acct, err := pwRepo.Authenticate(ctx, authMethodId, name, pw) + if err != nil { + return nil, err + } + if acct == nil { + return nil, status.Error(codes.Unauthenticated, "Unable to authenticate.") + } + + u, err := iamRepo.LookupUserWithLogin(ctx, acct.GetPublicId(), iam.WithAutoVivify(true)) + if err != nil { + return nil, err + } + tok, err := atRepo.CreateAuthToken(ctx, u.GetPublicId(), acct.GetPublicId()) if err != nil { return nil, err } @@ -156,6 +151,13 @@ func validateAuthenticateRequest(req *pbs.AuthenticateRequest) error { if req.GetCredentials() == nil { badFields["credentials"] = "This is a required field." } + creds := req.GetCredentials().GetFields() + if _, ok := creds[nameKey]; !ok { + badFields["credentials.name"] = "This is a required field." + } + if _, ok := creds[pwKey]; !ok { + badFields["credentials.password"] = "This is a required field." + } // TODO: Update this when we enable split cookie token types. tType := strings.ToLower(strings.TrimSpace(req.GetTokenType())) if tType != "" && tType != "token" { diff --git a/internal/servers/controller/handlers/authenticate/authenticate_service_test.go b/internal/servers/controller/handlers/authenticate/authenticate_service_test.go index d11c263c3..d12e51159 100644 --- a/internal/servers/controller/handlers/authenticate/authenticate_service_test.go +++ b/internal/servers/controller/handlers/authenticate/authenticate_service_test.go @@ -2,59 +2,46 @@ package authenticate import ( "context" - "crypto/sha256" - "encoding/base32" - "fmt" "strings" "testing" structpb "github.com/golang/protobuf/ptypes/struct" + "github.com/hashicorp/watchtower/internal/auth" + "github.com/hashicorp/watchtower/internal/auth/password" "github.com/hashicorp/watchtower/internal/authtoken" "github.com/hashicorp/watchtower/internal/db" pba "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/authtokens" pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services" "github.com/hashicorp/watchtower/internal/iam" - iamStore "github.com/hashicorp/watchtower/internal/iam/store" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -func getAuthMethodAndAccountId(t *testing.T, org *iam.Scope, rw *db.Db) (string, string) { - // TODO: Remove this when Auth Account repo is in place. - insert := `insert into auth_method - (public_id, scope_id) - values - ($1, $2);` - amId, err := db.NewPublicId("am") - require.NoError(t, err) - innerDb, err := rw.DB() - require.NoError(t, err) - _, err = innerDb.Exec(insert, amId, org.GetPublicId()) - require.NoError(t, err) - aAcctId, err := db.NewPublicId("aa") - require.NoError(t, err) - - aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{ - PublicId: aAcctId, - ScopeId: org.GetPublicId(), - AuthMethodId: amId, - }} - require.NoError(t, rw.Create(context.Background(), aAcct)) - return aAcct.GetAuthMethodId(), aAcct.GetPublicId() -} +const ( + testPassword = "thetestpassword" + testUsername = "default" +) func TestAuthenticate(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) - RWDb.Store(rw) wrapper := db.TestWrapper(t) o, _ := iam.TestScopes(t, conn) authTokenRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(rw, rw, wrapper) } iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, wrapper) } - amId, _ := getAuthMethodAndAccountId(t, o, rw) + passwordRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, wrapper) } + am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + + acct, err := password.NewAccount(am.GetPublicId(), testUsername) + require.NoError(t, err) + + pwRepo, err := passwordRepoFn() + require.NoError(t, err) + acct, err = pwRepo.CreateAccount(context.Background(), acct, password.WithPassword(testPassword)) + require.NoError(t, err) cases := []struct { name string @@ -65,45 +52,45 @@ func TestAuthenticate(t *testing.T) { { name: "basic", request: &pbs.AuthenticateRequest{ - AuthMethodId: amId, + AuthMethodId: am.GetPublicId(), TokenType: "token", Credentials: func() *structpb.Struct { creds := map[string]*structpb.Value{ - "name": {Kind: &structpb.Value_StringValue{StringValue: "admin"}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}}, + "name": {Kind: &structpb.Value_StringValue{StringValue: testUsername}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }, want: &pbs.AuthenticateResponse{Item: &pba.AuthToken{ - AuthMethodId: amId, + AuthMethodId: am.GetPublicId(), }}, }, { name: "no-token-type", request: &pbs.AuthenticateRequest{ - AuthMethodId: amId, + AuthMethodId: am.GetPublicId(), Credentials: func() *structpb.Struct { creds := map[string]*structpb.Value{ - "name": {Kind: &structpb.Value_StringValue{StringValue: "admin"}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}}, + "name": {Kind: &structpb.Value_StringValue{StringValue: testUsername}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }, want: &pbs.AuthenticateResponse{Item: &pba.AuthToken{ - AuthMethodId: amId, + AuthMethodId: am.GetPublicId(), }}, }, { name: "bad-token-type", request: &pbs.AuthenticateRequest{ - AuthMethodId: amId, + AuthMethodId: am.GetPublicId(), TokenType: "email", Credentials: func() *structpb.Struct { creds := map[string]*structpb.Value{ - "name": {Kind: &structpb.Value_StringValue{StringValue: "admin"}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}}, + "name": {Kind: &structpb.Value_StringValue{StringValue: testUsername}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), @@ -115,8 +102,8 @@ func TestAuthenticate(t *testing.T) { request: &pbs.AuthenticateRequest{ Credentials: func() *structpb.Struct { creds := map[string]*structpb.Value{ - "name": {Kind: &structpb.Value_StringValue{StringValue: "admin"}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}}, + "name": {Kind: &structpb.Value_StringValue{StringValue: testUsername}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), @@ -124,14 +111,29 @@ func TestAuthenticate(t *testing.T) { wantErr: status.Error(codes.InvalidArgument, "invalid argument"), }, { - name: "wrong-username-password", + name: "wrong-password", request: &pbs.AuthenticateRequest{ - AuthMethodId: amId, + AuthMethodId: am.GetPublicId(), + TokenType: "token", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "name": {Kind: &structpb.Value_StringValue{StringValue: testUsername}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: status.Error(codes.Unauthenticated, "unauthenticated"), + }, + { + name: "wrong-username", + request: &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), TokenType: "token", Credentials: func() *structpb.Struct { creds := map[string]*structpb.Value{ "name": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), @@ -143,22 +145,10 @@ func TestAuthenticate(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := NewService(iamRepoFn, authTokenRepoFn) + s, err := NewService(passwordRepoFn, iamRepoFn, authTokenRepoFn) require.NoError(err) - // TODO: remove when we stop faking things - name := tc.request.Credentials.Fields["name"].GetStringValue() - acctIdBytes := sha256.Sum256([]byte(name)) - acctId := fmt.Sprintf("aa_%s", base32.StdEncoding.EncodeToString(acctIdBytes[:])[0:10]) - - aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{ - PublicId: acctId, - ScopeId: o.PublicId, - AuthMethodId: amId, - }} - RWDb.Load().(*db.Db).Create(context.Background(), aAcct) - - resp, err := s.Authenticate(context.Background(), tc.request) + resp, err := s.Authenticate(auth.DisabledAuthTestContext(auth.WithScopeId(o.GetPublicId())), tc.request) if tc.wantErr != nil { assert.Error(err) assert.Equal(status.Code(tc.wantErr), status.Code(err)) @@ -169,7 +159,7 @@ func TestAuthenticate(t *testing.T) { assert.NotEmpty(aToken.GetId()) assert.NotEmpty(aToken.GetToken()) assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) - assert.Equal(amId, aToken.GetAuthMethodId()) + assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) assert.Equal(aToken.GetCreatedTime(), aToken.GetUpdatedTime()) assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime()) }) @@ -180,40 +170,36 @@ func TestAuthenticate_AuthAccountConnectedToIamUser(t *testing.T) { assert, require := assert.New(t), require.New(t) conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) - RWDb.Store(rw) wrapper := db.TestWrapper(t) o, _ := iam.TestScopes(t, conn) + passwordRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, wrapper) } authTokenRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(rw, rw, wrapper) } iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, wrapper) } - iamRepo, err := iamRepoFn() + + am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + acct, err := password.NewAccount(am.GetPublicId(), testUsername) + require.NoError(err) + + pwRepo, err := passwordRepoFn() + require.NoError(err) + acct, err = pwRepo.CreateAccount(context.Background(), acct, password.WithPassword(testPassword)) require.NoError(err) // connected to an account. - amId, _ := getAuthMethodAndAccountId(t, o, rw) - - name := "admin" - acctIdBytes := sha256.Sum256([]byte(name)) - acctId := fmt.Sprintf("aa_%s", base32.StdEncoding.EncodeToString(acctIdBytes[:])[0:10]) - - aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{ - PublicId: acctId, - ScopeId: o.PublicId, - AuthMethodId: amId, - }} - RWDb.Load().(*db.Db).Create(context.Background(), aAcct) - - iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acctId, iam.WithAutoVivify(true)) + iamRepo, err := iamRepoFn() + require.NoError(err) + iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acct.GetPublicId(), iam.WithAutoVivify(true)) require.NoError(err) - s, err := NewService(iamRepoFn, authTokenRepoFn) + s, err := NewService(passwordRepoFn, iamRepoFn, authTokenRepoFn) require.NoError(err) - resp, err := s.Authenticate(context.Background(), &pbs.AuthenticateRequest{ - AuthMethodId: amId, + resp, err := s.Authenticate(auth.DisabledAuthTestContext(auth.WithScopeId(o.GetPublicId())), &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), Credentials: func() *structpb.Struct { creds := map[string]*structpb.Value{ - "name": {Kind: &structpb.Value_StringValue{StringValue: name}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}}, + "name": {Kind: &structpb.Value_StringValue{StringValue: testUsername}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), @@ -222,7 +208,7 @@ func TestAuthenticate_AuthAccountConnectedToIamUser(t *testing.T) { aToken := resp.GetItem() assert.Equal(iamUser.GetPublicId(), aToken.GetUserId()) - assert.Equal(amId, aToken.GetAuthMethodId()) + assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) assert.NotEmpty(aToken.GetId()) assert.NotEmpty(aToken.GetToken()) diff --git a/internal/servers/controller/handlers/subtypes_test.go b/internal/servers/controller/handlers/subtypes_test.go index b16eea61e..80eb3a21d 100644 --- a/internal/servers/controller/handlers/subtypes_test.go +++ b/internal/servers/controller/handlers/subtypes_test.go @@ -5,7 +5,7 @@ import ( structpb "github.com/golang/protobuf/ptypes/struct" "github.com/google/go-cmp/cmp" - authpb "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/auth" + authpb "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/authmethods" hostpb "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/hosts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index 7bd8c9716..9feea8a30 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -114,6 +114,12 @@ type TestControllerOpts struct { // in the normal config. DefaultOrgId string + // DefaultUsername is the username used when creating the default account. + DefaultUsername string + + // DefaultPassword is the password used when creating the default account. + DefaultPassword string + // DisableDatabaseCreation can be set true to disable creating a dev // database DisableDatabaseCreation bool @@ -163,6 +169,13 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { tc.b.DefaultOrgId = opts.DefaultOrgId } + if opts.DefaultUsername != "" { + tc.b.DevUsername = opts.DefaultUsername + } + if opts.DefaultPassword != "" { + tc.b.DevPassword = opts.DefaultPassword + } + // Start a logger tc.b.Logger = hclog.New(&hclog.LoggerOptions{ Level: hclog.Trace,