package authmethods_test import ( "context" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers/controller/auth" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods" authtokenpb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens" scopepb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) func TestUpdate_Password(t *testing.T) { ctx := context.TODO() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(rw, rw, kms) } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} freshAuthMethod := func() (*pb.AuthMethod, func()) { am, err := tested.CreateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: wrapperspb.String("default"), Description: wrapperspb.String("default"), Type: "password", }}) require.NoError(t, err) clean := func() { _, err := tested.DeleteAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.DeleteAuthMethodRequest{Id: am.GetItem().GetId()}) require.NoError(t, err) } return am.GetItem(), clean } cases := []struct { name string req *pbs.UpdateAuthMethodRequest res *pbs.UpdateAuthMethodResponse err error }{ { name: "Update an Existing AuthMethod", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: "password", }, }, res: &pbs.UpdateAuthMethodResponse{ Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, Scope: defaultScopeInfo, AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, }, { name: "Multiple Paths in single string", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name,description,type"}, }, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: "password", }, }, res: &pbs.UpdateAuthMethodResponse{ Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, Scope: defaultScopeInfo, AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, }, { name: "No Update Mask", req: &pbs.UpdateAuthMethodRequest{ Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "No Paths in Mask", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Only non-existant paths in Mask", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant change type", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{"name", "type"}}, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "updated name"}, Type: "oidc", }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant change is primary", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{"is_primary"}}, Item: &pb.AuthMethod{ IsPrimary: true, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unset Name", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name"}, }, Item: &pb.AuthMethod{ Description: &wrapperspb.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateAuthMethodResponse{ Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Description: &wrapperspb.StringValue{Value: "default"}, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, Scope: defaultScopeInfo, AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, }, { name: "Update Only Name", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name"}, }, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateAuthMethodResponse{ Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "default"}, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, Scope: defaultScopeInfo, AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, }, { name: "Update Only Description", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "ignored"}, Description: &wrapperspb.StringValue{Value: "notignored"}, }, }, res: &pbs.UpdateAuthMethodResponse{ Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: &wrapperspb.StringValue{Value: "default"}, Description: &wrapperspb.StringValue{Value: "notignored"}, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, Scope: defaultScopeInfo, AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, }, { name: "Update a Non Existing AuthMethod", req: &pbs.UpdateAuthMethodRequest{ Id: password.AuthMethodPrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Cant change Id", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"id"}, }, Item: &pb.AuthMethod{ Id: password.AuthMethodPrefix + "_somethinge", Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "new desc"}, }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Created Time", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"created_time"}, }, Item: &pb.AuthMethod{ CreatedTime: timestamppb.Now(), }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Updated Time", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"updated_time"}, }, Item: &pb.AuthMethod{ UpdatedTime: timestamppb.Now(), }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Type", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"type"}, }, Item: &pb.AuthMethod{ Type: "oidc", }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Update login name length", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"attributes.min_login_name_length"}, }, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "ignored"}, Description: &wrapperspb.StringValue{Value: "ignored"}, Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_login_name_length": structpb.NewNumberValue(42), "min_password_length": structpb.NewNumberValue(55555), }}, }, }, res: &pbs.UpdateAuthMethodResponse{ Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: &wrapperspb.StringValue{Value: "default"}, Description: &wrapperspb.StringValue{Value: "default"}, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(42), }}, Scope: defaultScopeInfo, AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, }, { name: "Update password length", req: &pbs.UpdateAuthMethodRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"attributes.min_password_length"}, }, Item: &pb.AuthMethod{ Name: &wrapperspb.StringValue{Value: "ignored"}, Description: &wrapperspb.StringValue{Value: "ignored"}, Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_login_name_length": structpb.NewNumberValue(5555), "min_password_length": structpb.NewNumberValue(42), }}, }, }, res: &pbs.UpdateAuthMethodResponse{ Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: &wrapperspb.StringValue{Value: "default"}, Description: &wrapperspb.StringValue{Value: "default"}, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "min_password_length": structpb.NewNumberValue(42), "min_login_name_length": structpb.NewNumberValue(3), }}, Scope: defaultScopeInfo, AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) am, cleanup := freshAuthMethod() defer cleanup() tc.req.Item.Version = 1 if tc.req.GetId() == "" { tc.req.Id = am.GetId() } if tc.res != nil && tc.res.Item != nil { tc.res.Item.Id = am.GetId() tc.res.Item.CreatedTime = am.GetCreatedTime() } got, gErr := tested.UpdateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "UpdateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } if tc.res == nil { require.Nil(got) } if got != nil { assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got) created := am.GetCreatedTime().AsTime() gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() // Verify it is a auth_method updated after it was created assert.True(gotUpdateTime.After(created), "Updated auth_method should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) // Clear all values which are hard to compare against. got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil assert.EqualValues(2, got.Item.Version) tc.res.Item.Version = 2 } assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } func TestAuthenticate_Password(t *testing.T) { ctx := context.TODO() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(rw, rw, kms) } am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, conn, wrapper), o, am.PublicId) acct, err := password.NewAccount(am.GetPublicId(), password.WithLoginName(testLoginName)) require.NoError(t, err) pwRepo, err := pwRepoFn() require.NoError(t, err) acct, err = pwRepo.CreateAccount(context.Background(), o.GetPublicId(), acct, password.WithPassword(testPassword)) require.NoError(t, err) require.NotNil(t, acct) cases := []struct { name string request *pbs.AuthenticateRequest wantType string wantErr error }{ { name: "basic", request: &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), TokenType: "token", Attributes: func() *structpb.Struct { creds := map[string]*structpb.Value{ "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }, wantType: "token", }, { name: "cookie-type", request: &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), TokenType: "cookie", Attributes: func() *structpb.Struct { creds := map[string]*structpb.Value{ "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }, wantType: "cookie", }, { name: "no-token-type", request: &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), Attributes: func() *structpb.Struct { creds := map[string]*structpb.Value{ "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }, }, { name: "bad-token-type", request: &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), TokenType: "email", Attributes: func() *structpb.Struct { creds := map[string]*structpb.Value{ "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }, wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "no-authmethod", request: &pbs.AuthenticateRequest{ Attributes: func() *structpb.Struct { creds := map[string]*structpb.Value{ "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }, wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "wrong-password", request: &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), TokenType: "token", Attributes: func() *structpb.Struct { creds := map[string]*structpb.Value{ "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, "password": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, } return &structpb.Struct{Fields: creds} }(), }, wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), }, { name: "wrong-login-name", request: &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), TokenType: "token", Attributes: func() *structpb.Struct { creds := map[string]*structpb.Value{ "login_name": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }, wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request) if tc.wantErr != nil { assert.Error(err) assert.Truef(errors.Is(err, tc.wantErr), "Got %#v, wanted %#v", err, tc.wantErr) return } require.NoError(err) aToken := &authtokenpb.AuthToken{} require.NoError(handlers.StructToProto(resp.GetAttributes(), aToken, handlers.WithDiscardUnknownFields(true))) assert.NotEmpty(aToken.GetId()) assert.NotEmpty(aToken.GetToken()) assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) assert.Equal(aToken.GetCreatedTime(), aToken.GetUpdatedTime()) assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime()) assert.Equal(acct.GetPublicId(), aToken.GetAccountId()) assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) assert.Equal(tc.wantType, resp.GetType()) }) } } func TestAuthenticate_AuthAccountConnectedToIamUser_Password(t *testing.T) { ctx := context.TODO() assert, require := assert.New(t), require.New(t) conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(rw, rw, kms) } am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] acct, err := password.NewAccount(am.GetPublicId(), password.WithLoginName(testLoginName)) require.NoError(err) pwRepo, err := pwRepoFn() require.NoError(err) acct, err = pwRepo.CreateAccount(context.Background(), o.GetPublicId(), acct, password.WithPassword(testPassword)) require.NoError(err) // connected to an account. iamRepo, err := iamRepoFn() require.NoError(err) iam.TestUser(t, iamRepo, am.ScopeId, iam.WithAccountIds(acct.PublicId)) iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acct.GetPublicId()) require.NoError(err) s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), Attributes: func() *structpb.Struct { creds := map[string]*structpb.Value{ "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, } return &structpb.Struct{Fields: creds} }(), }) require.NoError(err) aToken := &authtokenpb.AuthToken{} require.NoError(handlers.StructToProto(resp.GetAttributes(), aToken, handlers.WithDiscardUnknownFields(true))) assert.Equal(iamUser.GetPublicId(), aToken.GetUserId()) assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) assert.Equal(acct.GetPublicId(), aToken.GetAccountId()) assert.NotEmpty(aToken.GetId()) assert.NotEmpty(aToken.GetToken()) assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) }