Files
threshold/internal/daemon/controller/handlers/accounts/account_service_test.go
T
b6df5693b5 Add generic read/update/delete commands (#3992)
* Add generic read/update/delete commands

This adds three generic commands that use the resource prefix to
determine the subcommand to run and pass through args over to that
subcommand. Since this occurs very early on and to avoid loading
essentially every internal resource package, I moved subtype definitions
to globals.

This replaces #3967

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>

* De-duplicate Subtype code (#4031)

Co-authored-by: Timothy Messier <tim.messier@gmail.com>

---------

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
Co-authored-by: Timothy Messier <tim.messier@gmail.com>
2023-11-15 19:55:46 -05:00

3153 lines
101 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package accounts_test
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth/ldap"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
requestauth "github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts"
"github.com/hashicorp/boundary/internal/db"
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/types/action"
"github.com/hashicorp/boundary/internal/types/scope"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/accounts"
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/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const domain = "auth"
var (
pwAuthorizedActions = []string{
action.NoOp.String(),
action.Read.String(),
action.Update.String(),
action.Delete.String(),
action.SetPassword.String(),
action.ChangePassword.String(),
}
oidcAuthorizedActions = []string{
action.NoOp.String(),
action.Read.String(),
action.Update.String(),
action.Delete.String(),
}
ldapAuthorizedActions = []string{
action.NoOp.String(),
action.Read.String(),
action.Update.String(),
action.Delete.String(),
}
)
func TestNewService(t *testing.T) {
ctx := context.TODO()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
cases := []struct {
name string
pwRepo common.PasswordAuthRepoFactory
oidcRepo common.OidcAuthRepoFactory
wantErr bool
}{
{
name: "nil-all",
wantErr: true,
},
{
name: "nil-pw-repo",
oidcRepo: oidcRepoFn,
wantErr: true,
},
{
name: "nil-oidc-repo",
pwRepo: pwRepoFn,
wantErr: true,
},
{
name: "success",
pwRepo: pwRepoFn,
oidcRepo: oidcRepoFn,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := accounts.NewService(ctx, tc.pwRepo, tc.oidcRepo, ldapRepoFn)
if tc.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestGet(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Couldn't create new auth token service.")
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
am := password.TestAuthMethods(t, conn, org.GetPublicId(), 1)[0]
pwA := password.TestAccount(t, conn, am.GetPublicId(), "name1")
pwWireAccount := pb.Account{
Id: pwA.GetPublicId(),
AuthMethodId: pwA.GetAuthMethodId(),
CreatedTime: pwA.GetCreateTime().GetTimestamp(),
UpdatedTime: pwA.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: password.Subtype.String(),
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{LoginName: pwA.GetLoginName()},
},
AuthorizedActions: pwAuthorizedActions,
}
databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
oidcAm := oidc.TestAuthMethod(
t, conn, databaseWrapper, org.PublicId, oidc.ActivePrivateState,
"alice-rp", "fido",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]),
oidc.WithSigningAlgs(oidc.RS256),
oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]),
)
oidcA := oidc.TestAccount(t, conn, oidcAm, "test-subject")
// Create a managed group that will always match, so we can test that it is
// returned in results
mg := oidc.TestManagedGroup(t, conn, oidcAm, `"/token/sub" matches ".*"`)
oidcWireAccount := pb.Account{
Id: oidcA.GetPublicId(),
AuthMethodId: oidcA.GetAuthMethodId(),
CreatedTime: oidcA.GetCreateTime().GetTimestamp(),
UpdatedTime: oidcA.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Issuer: oidcAm.GetIssuer(),
Subject: "test-subject",
},
},
AuthorizedActions: oidcAuthorizedActions,
ManagedGroupIds: []string{mg.GetPublicId()},
}
ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
ldapAcct := ldap.TestAccount(t, conn, ldapAm, "test-acct",
ldap.WithMemberOfGroups(ctx, "admin"),
ldap.WithFullName(ctx, "test-name"),
ldap.WithEmail(ctx, "test-email"),
ldap.WithDn(ctx, "test-dn"),
)
ldapMg := ldap.TestManagedGroup(t, conn, ldapAm, []string{"admin"})
ldapWireAccount := pb.Account{
Id: ldapAcct.GetPublicId(),
AuthMethodId: ldapAm.GetPublicId(),
CreatedTime: ldapAcct.GetCreateTime().GetTimestamp(),
UpdatedTime: ldapAcct.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: ldapAcct.GetLoginName(),
FullName: ldapAcct.GetFullName(),
Email: ldapAcct.GetEmail(),
Dn: ldapAcct.GetDn(),
MemberOfGroups: []string{"admin"},
},
},
Type: ldap.Subtype.String(),
AuthorizedActions: ldapAuthorizedActions,
ManagedGroupIds: []string{ldapMg.GetPublicId()},
}
cases := []struct {
name string
req *pbs.GetAccountRequest
res *pbs.GetAccountResponse
err error
}{
{
name: "Get an ldap account",
req: &pbs.GetAccountRequest{Id: ldapWireAccount.GetId()},
res: &pbs.GetAccountResponse{Item: &ldapWireAccount},
},
{
name: "Get a password account",
req: &pbs.GetAccountRequest{Id: pwWireAccount.GetId()},
res: &pbs.GetAccountResponse{Item: &pwWireAccount},
},
{
name: "Get an oidc account",
req: &pbs.GetAccountRequest{Id: oidcWireAccount.GetId()},
res: &pbs.GetAccountResponse{Item: &oidcWireAccount},
},
{
name: "Get a non existing old password account",
req: &pbs.GetAccountRequest{Id: globals.PasswordAccountPreviousPrefix + "_DoesntExis"},
res: nil,
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Get a non existing new password account",
req: &pbs.GetAccountRequest{Id: globals.PasswordAccountPrefix + "_DoesntExis"},
res: nil,
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Get a non existing oidc account",
req: &pbs.GetAccountRequest{Id: globals.OidcAccountPrefix + "_DoesntExis"},
res: nil,
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Get a non existing ldap account",
req: &pbs.GetAccountRequest{Id: globals.LdapAccountPrefix + "_DoesntExis"},
res: nil,
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Wrong id prefix",
req: &pbs.GetAccountRequest{Id: "j_1234567890"},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "space in id",
req: &pbs.GetAccountRequest{Id: globals.AuthTokenPrefix + "_1 23456789"},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
if globals.ResourceInfoFromPrefix(tc.req.Id).Subtype == oidc.Subtype {
// Set up managed groups before getting. First get the current
// managed group to make sure we have the right version.
oidcRepo, err := oidcRepoFn()
require.NoError(err)
currMg, err := oidcRepo.LookupManagedGroup(ctx, mg.GetPublicId())
require.NoError(err)
_, _, err = oidcRepo.SetManagedGroupMemberships(ctx, oidcAm, oidcA, []*oidc.ManagedGroup{currMg})
require.NoError(err)
}
got, gErr := s.GetAccount(requestauth.DisabledAuthTestContext(iamRepoFn, org.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "GetAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
return
}
require.NoError(gErr)
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetAccount(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestListPassword(t *testing.T) {
ctx := context.TODO()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kms)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kms)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kms)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kms)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
ams := password.TestAuthMethods(t, conn, o.GetPublicId(), 3)
amNoAccounts, amSomeAccounts, amOtherAccounts := ams[0], ams[1], ams[2]
var wantSomeAccounts []*pb.Account
for _, aa := range password.TestMultipleAccounts(t, conn, amSomeAccounts.GetPublicId(), 3) {
wantSomeAccounts = append(wantSomeAccounts, &pb.Account{
Id: aa.GetPublicId(),
AuthMethodId: aa.GetAuthMethodId(),
CreatedTime: aa.GetCreateTime().GetTimestamp(),
UpdatedTime: aa.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
PasswordAccountAttributes: &pb.PasswordAccountAttributes{LoginName: aa.GetLoginName()},
},
AuthorizedActions: pwAuthorizedActions,
})
}
var wantOtherAccounts []*pb.Account
for _, aa := range password.TestMultipleAccounts(t, conn, amOtherAccounts.GetPublicId(), 3) {
wantOtherAccounts = append(wantOtherAccounts, &pb.Account{
Id: aa.GetPublicId(),
AuthMethodId: aa.GetAuthMethodId(),
CreatedTime: aa.GetCreateTime().GetTimestamp(),
UpdatedTime: aa.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{LoginName: aa.GetLoginName()},
},
AuthorizedActions: pwAuthorizedActions,
})
}
cases := []struct {
name string
req *pbs.ListAccountsRequest
res *pbs.ListAccountsResponse
err error
skipAnon bool
}{
{
name: "List Some Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts},
},
{
name: "List Other Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amOtherAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{Items: wantOtherAccounts},
},
{
name: "List No Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amNoAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{},
},
{
name: "Unfound Auth Method",
req: &pbs.ListAccountsRequest{AuthMethodId: globals.PasswordAuthMethodPrefix + "_DoesntExis"},
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Filter Some Accounts",
req: &pbs.ListAccountsRequest{
AuthMethodId: amSomeAccounts.GetPublicId(),
Filter: fmt.Sprintf(`"/item/attributes/login_name"==%q`, wantSomeAccounts[1].GetPasswordAccountAttributes().LoginName),
},
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]},
skipAnon: true,
},
{
name: "Filter All Accounts",
req: &pbs.ListAccountsRequest{
AuthMethodId: amSomeAccounts.GetPublicId(),
Filter: `"/item/id"=="noaccountmatchesthis"`,
},
res: &pbs.ListAccountsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(err, "Couldn't create new user service.")
// Test non-anon first
got, gErr := s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListAccounts() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err)
return
} else {
require.NoError(gErr)
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res)
// Now test with anon
if tc.skipAnon {
return
}
got, gErr = s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId(), requestauth.WithUserId(globals.AnonymousUserId)), tc.req)
require.NoError(gErr)
assert.Len(got.Items, len(tc.res.Items))
for _, g := range got.GetItems() {
assert.Nil(g.Attrs)
assert.Nil(g.CreatedTime)
assert.Nil(g.UpdatedTime)
assert.Empty(g.Version)
}
})
}
}
func TestListOidc(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
amNoAccounts := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, "noAccounts", "fido",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.noaccounts.com")[0]), oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]))
amSomeAccounts := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, "someAccounts", "fido",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.someaccounts.com")[0]), oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]))
amOtherAccounts := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, "otherAccounts", "fido",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.otheraccounts.com")[0]), oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]))
var wantSomeAccounts []*pb.Account
for i := 0; i < 3; i++ {
subId := fmt.Sprintf("test-subject%d", i)
aa := oidc.TestAccount(t, conn, amSomeAccounts, subId)
wantSomeAccounts = append(wantSomeAccounts, &pb.Account{
Id: aa.GetPublicId(),
AuthMethodId: aa.GetAuthMethodId(),
CreatedTime: aa.GetCreateTime().GetTimestamp(),
UpdatedTime: aa.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Issuer: amSomeAccounts.GetIssuer(),
Subject: subId,
},
},
AuthorizedActions: oidcAuthorizedActions,
})
}
var wantOtherAccounts []*pb.Account
for i := 0; i < 3; i++ {
subId := fmt.Sprintf("test-subject%d", i)
aa := oidc.TestAccount(t, conn, amOtherAccounts, subId)
wantOtherAccounts = append(wantOtherAccounts, &pb.Account{
Id: aa.GetPublicId(),
AuthMethodId: aa.GetAuthMethodId(),
CreatedTime: aa.GetCreateTime().GetTimestamp(),
UpdatedTime: aa.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Issuer: amOtherAccounts.GetIssuer(),
Subject: subId,
},
},
AuthorizedActions: oidcAuthorizedActions,
})
}
cases := []struct {
name string
req *pbs.ListAccountsRequest
res *pbs.ListAccountsResponse
err error
skipAnon bool
}{
{
name: "List Some Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts},
},
{
name: "List Other Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amOtherAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{Items: wantOtherAccounts},
},
{
name: "List No Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amNoAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{},
},
{
name: "Unfound Auth Method",
req: &pbs.ListAccountsRequest{AuthMethodId: globals.OidcAuthMethodPrefix + "_DoesntExis"},
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Filter Some Accounts",
req: &pbs.ListAccountsRequest{
AuthMethodId: amSomeAccounts.GetPublicId(),
Filter: fmt.Sprintf(`"/item/attributes/subject"==%q`, wantSomeAccounts[1].GetOidcAccountAttributes().Subject),
},
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]},
skipAnon: true,
},
{
name: "Filter All Accounts",
req: &pbs.ListAccountsRequest{
AuthMethodId: amSomeAccounts.GetPublicId(),
Filter: `"/item/id"=="noaccountmatchesthis"`,
},
res: &pbs.ListAccountsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(err, "Couldn't create new user service.")
got, gErr := s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListAccounts() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err)
return
} else {
require.NoError(gErr)
}
sort.Slice(got.Items, func(i, j int) bool {
return strings.Compare(got.Items[i].GetOidcAccountAttributes().Subject,
got.Items[j].GetOidcAccountAttributes().Subject) < 0
})
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res)
// Now test with anon
if tc.skipAnon {
return
}
got, gErr = s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId(), requestauth.WithUserId(globals.AnonymousUserId)), tc.req)
require.NoError(gErr)
assert.Len(got.Items, len(tc.res.Items))
for _, g := range got.GetItems() {
assert.Nil(g.Attrs)
assert.Nil(g.CreatedTime)
assert.Nil(g.UpdatedTime)
assert.Empty(g.Version)
}
})
}
}
func TestListLdap(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
amNoAccounts := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://no-accounts"})
amSomeAccounts := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://some-accounts"})
amOtherAccounts := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://other-accounts"})
var wantSomeAccounts []*pb.Account
for i := 0; i < 3; i++ {
loginName := fmt.Sprintf("test-login-name%d", i)
aa := ldap.TestAccount(t, conn, amSomeAccounts, loginName)
wantSomeAccounts = append(wantSomeAccounts, &pb.Account{
Id: aa.GetPublicId(),
AuthMethodId: aa.GetAuthMethodId(),
CreatedTime: aa.GetCreateTime().GetTimestamp(),
UpdatedTime: aa.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: loginName,
},
},
AuthorizedActions: ldapAuthorizedActions,
})
}
var wantOtherAccounts []*pb.Account
for i := 0; i < 3; i++ {
loginName := fmt.Sprintf("test-login-name%d", i)
aa := ldap.TestAccount(t, conn, amOtherAccounts, loginName)
wantOtherAccounts = append(wantOtherAccounts, &pb.Account{
Id: aa.GetPublicId(),
AuthMethodId: aa.GetAuthMethodId(),
CreatedTime: aa.GetCreateTime().GetTimestamp(),
UpdatedTime: aa.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: loginName,
},
},
AuthorizedActions: ldapAuthorizedActions,
})
}
cases := []struct {
name string
req *pbs.ListAccountsRequest
res *pbs.ListAccountsResponse
err error
skipAnon bool
}{
{
name: "List Some Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts},
},
{
name: "List Other Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amOtherAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{Items: wantOtherAccounts},
},
{
name: "List No Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amNoAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{},
},
{
name: "Unfound Auth Method",
req: &pbs.ListAccountsRequest{AuthMethodId: globals.OidcAuthMethodPrefix + "_DoesntExis"},
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Filter Some Accounts",
req: &pbs.ListAccountsRequest{
AuthMethodId: amSomeAccounts.GetPublicId(),
Filter: fmt.Sprintf(`"/item/attributes/login_name"==%q`, wantSomeAccounts[1].GetLdapAccountAttributes().LoginName),
},
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]},
skipAnon: true,
},
{
name: "Filter All Accounts",
req: &pbs.ListAccountsRequest{
AuthMethodId: amSomeAccounts.GetPublicId(),
Filter: `"/item/id"=="noaccountmatchesthis"`,
},
res: &pbs.ListAccountsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(err, "Couldn't create new user service.")
got, gErr := s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListAccounts() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err)
return
} else {
require.NoError(gErr)
}
sort.Slice(got.Items, func(i, j int) bool {
return strings.Compare(got.Items[i].GetLdapAccountAttributes().LoginName,
got.Items[j].GetLdapAccountAttributes().LoginName) < 0
})
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res)
// Now test with anon
if tc.skipAnon {
return
}
got, gErr = s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId(), requestauth.WithUserId(globals.AnonymousUserId)), tc.req)
require.NoError(gErr)
assert.Len(got.Items, len(tc.res.Items))
for _, g := range got.GetItems() {
assert.Nil(g.Attrs)
assert.Nil(g.CreatedTime)
assert.Nil(g.UpdatedTime)
assert.Empty(g.Version)
}
})
}
}
func TestDelete(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
am1 := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0]
ac := password.TestAccount(t, conn, am1.GetPublicId(), "name1")
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
oidcAm := oidc.TestAuthMethod(
t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState,
"alice-rp", "fido",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]),
oidc.WithSigningAlgs(oidc.RS256),
oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]),
)
oidcA := oidc.TestAccount(t, conn, oidcAm, "test-subject")
ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"})
ldapAcct := ldap.TestAccount(t, conn, ldapAm, "test-account")
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new user service.")
cases := []struct {
name string
scope string
req *pbs.DeleteAccountRequest
res *pbs.DeleteAccountResponse
err error
errContains string
}{
{
name: "Delete an existing pw account",
req: &pbs.DeleteAccountRequest{
Id: ac.GetPublicId(),
},
},
{
name: "Delete an existing oidc account",
req: &pbs.DeleteAccountRequest{
Id: oidcA.GetPublicId(),
},
},
{
name: "Delete an existing ldap account",
req: &pbs.DeleteAccountRequest{
Id: ldapAcct.GetPublicId(),
},
},
{
name: "Delete bad old pw account id",
req: &pbs.DeleteAccountRequest{
Id: globals.PasswordAccountPreviousPrefix + "_doesntexis",
},
err: handlers.ApiErrorWithCode(codes.NotFound),
errContains: "Resource not found.",
},
{
name: "Delete bad new pw account id",
req: &pbs.DeleteAccountRequest{
Id: globals.PasswordAccountPrefix + "_doesntexis",
},
err: handlers.ApiErrorWithCode(codes.NotFound),
errContains: "Resource not found.",
},
{
name: "Delete bad oidc account id",
req: &pbs.DeleteAccountRequest{
Id: globals.OidcAccountPrefix + "_doesntexis",
},
err: handlers.ApiErrorWithCode(codes.NotFound),
errContains: "Resource not found.",
},
{
name: "Delete bad ldap account id",
req: &pbs.DeleteAccountRequest{
Id: globals.LdapAccountPrefix + "_doesntexis",
},
err: handlers.ApiErrorWithCode(codes.NotFound),
errContains: "Resource not found.",
},
{
name: "Bad account id formatting",
req: &pbs.DeleteAccountRequest{
Id: "bad_format",
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "Incorrectly formatted identifier.",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, gErr := s.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "DeleteAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
require.NotEmpty(tc.errContains)
assert.Contains(gErr.Error(), tc.errContains)
}
assert.EqualValuesf(tc.res, got, "DeleteAccount(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestDelete_twice(t *testing.T) {
ctx := context.TODO()
assert, require := assert.New(t), require.New(t)
conn, _ := db.TestSetup(t, "postgres")
wrap := db.TestWrapper(t)
rw := db.New(conn)
kms := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kms)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kms)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kms)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kms)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0]
ac := password.TestAccount(t, conn, am.GetPublicId(), "name1")
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(err, "Error when getting new user service")
req := &pbs.DeleteAccountRequest{
Id: ac.GetPublicId(),
}
_, gErr := s.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), req)
assert.NoError(gErr, "First attempt")
_, gErr = s.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), req)
assert.Error(gErr, "Second attempt")
assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected not found for the second delete.")
}
func TestCreatePassword(t *testing.T) {
ctx := context.TODO()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kms)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kms)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kms)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kms)
}
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new account service.")
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0]
defaultAccount := password.TestAccount(t, conn, am.GetPublicId(), "name1")
defaultCreated := defaultAccount.GetCreateTime().GetTimestamp()
require.NoError(t, err, "Error converting proto to timestamp.")
cases := []struct {
name string
req *pbs.CreateAccountRequest
res *pbs.CreateAccountResponse
err error
}{
{
name: "Create a valid Account",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "validaccount",
Password: nil,
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.PasswordAccountPrefix),
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "validaccount",
Password: nil,
},
},
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "Create a valid Account without type defined",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "notypedefined",
Password: nil,
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.PasswordAccountPrefix),
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "notypedefined",
Password: nil,
},
},
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "Create a valid Account with password defined",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Name: &wrapperspb.StringValue{Value: "name_with_password"},
Description: &wrapperspb.StringValue{Value: "desc"},
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "haspassword",
Password: &wrapperspb.StringValue{Value: "somepassword"},
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.PasswordAccountPrefix),
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Name: &wrapperspb.StringValue{Value: "name_with_password"},
Description: &wrapperspb.StringValue{Value: "desc"},
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "haspassword",
Password: nil,
},
},
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "Cant specify mismatching type",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Type: "wrong",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "nopwprovided",
Password: nil,
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Can't specify Id",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Id: globals.PasswordAccountPrefix + "_notallowed",
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "cantprovideid",
Password: nil,
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Can't specify Created Time",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
CreatedTime: timestamppb.Now(),
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "nocreatedtime",
Password: nil,
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Can't specify Update Time",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
UpdatedTime: timestamppb.Now(),
Type: "password",
Attrs: &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "noupdatetime",
Password: nil,
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Must specify login name for password type",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: defaultAccount.GetAuthMethodId(),
Type: "password",
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, gErr := s.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "CreateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
} else {
require.NoError(gErr)
}
if got != nil {
assert.Contains(got.GetUri(), tc.res.Uri)
assert.True(strings.HasPrefix(got.GetItem().GetId(), globals.PasswordAccountPrefix+"_"))
gotCreateTime := got.GetItem().GetCreatedTime()
require.NoError(err, "Error converting proto to timestamp.")
gotUpdateTime := got.GetItem().GetUpdatedTime()
require.NoError(err, "Error converting proto to timestamp.")
// Verify it is a user created after the test setup's default user
assert.True(gotCreateTime.AsTime().After(defaultCreated.AsTime()), "New account should have been created after default user. Was created %v, which is after %v", gotCreateTime, defaultCreated)
assert.True(gotUpdateTime.AsTime().After(defaultCreated.AsTime()), "New account should have been updated after default user. Was updated %v, which is after %v", gotUpdateTime, defaultCreated)
// Clear all values which are hard to compare against.
got.Uri, tc.res.Uri = "", ""
got.Item.Id, tc.res.Item.Id = "", ""
got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestCreateOidc(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new account service.")
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
am := oidc.TestAuthMethod(
t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState,
"alice-rp", "fido",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]),
oidc.WithSigningAlgs(oidc.RS256),
oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]),
)
cases := []struct {
name string
req *pbs.CreateAccountRequest
res *pbs.CreateAccountResponse
err error
}{
{
name: "Create a valid Account",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "valid-account",
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.OidcAccountPrefix),
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "valid-account",
Issuer: am.GetIssuer(),
},
},
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "Create a valid Account without type defined",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "no type defined",
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.OidcAccountPrefix),
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "no type defined",
Issuer: am.GetIssuer(),
},
},
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "Create Account With Overwritten Issuer",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "overwritten issuer"},
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "overwritten-issuer",
Issuer: "https://overwrite.com",
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.OidcAccountPrefix),
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "overwritten issuer"},
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "overwritten-issuer",
Issuer: "https://overwrite.com",
},
},
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "Cant specify mismatching type",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: password.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "cant-specify-mismatching-type",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Can't specify Id",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Id: globals.OidcAccountPrefix + "_notallowed",
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "cant-specify-id",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Can't specify Created Time",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
CreatedTime: timestamppb.Now(),
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "cant-specify-created-time",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Can't specify Update Time",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
UpdatedTime: timestamppb.Now(),
Type: oidc.Subtype.String(),
Attrs: &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Subject: "cant-specify-update-time",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Must specify subject",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: oidc.Subtype.String(),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, gErr := s.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "CreateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
return
}
require.NoError(gErr)
if got != nil {
assert.Contains(got.GetUri(), tc.res.Uri)
assert.True(strings.HasPrefix(got.GetItem().GetId(), globals.OidcAccountPrefix+"_"))
// Clear all values which are hard to compare against.
got.Uri, tc.res.Uri = "", ""
got.Item.Id, tc.res.Item.Id = "", ""
got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestCreateLdap(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new account service.")
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
am := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"})
cases := []struct {
name string
req *pbs.CreateAccountRequest
res *pbs.CreateAccountResponse
err error
errContains string
}{
{
name: "Create a valid Account",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "valid-account",
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.LdapAccountPrefix),
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "valid-account",
},
},
AuthorizedActions: ldapAuthorizedActions,
},
},
},
{
name: "Create a valid Account without type defined",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "no type defined",
},
},
},
},
res: &pbs.CreateAccountResponse{
Uri: fmt.Sprintf("accounts/%s_", globals.LdapAccountPrefix),
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "no type defined",
},
},
AuthorizedActions: ldapAuthorizedActions,
},
},
},
{
name: "Cant specify mismatching type",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: password.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "cant-specify-mismatching-type",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "Doesn't match the parent resource's type",
},
{
name: "Can't specify Id",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Id: globals.LdapAccountPrefix + "_notallowed",
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "cant-specify-mismatching-type",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"id\", desc: \"This is a read only field.\"",
},
{
name: "Can't specify Created Time",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
CreatedTime: timestamppb.Now(),
Type: oidc.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "cant-specify-mismatching-type",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"created_time\", desc: \"This is a read only field.\"",
},
{
name: "Can't specify Update Time",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
UpdatedTime: timestamppb.Now(),
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "cant-specify-mismatching-type",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"updated_time\", desc: \"This is a read only field.\"",
},
{
name: "Must specify login name",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"attributes.login_name\", desc: \"This is a required field for this type.",
},
{
name: "Can't specify full name",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "cant-specify-full-name",
FullName: "something",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"attributes.full_name\", desc: \"This is a read only field.\"",
},
{
name: "Can't specify email",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "cant-specify-email",
Email: "something",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"attributes.email\", desc: \"This is a read only field.\"",
},
{
name: "Can't specify dn",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "cant-specify-dn",
Dn: "something",
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"attributes.dn\", desc: \"This is a read only field.\"",
},
{
name: "Can't specify member of groups",
req: &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: ldap.Subtype.String(),
Attrs: &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "cant-specify-member-of",
MemberOfGroups: []string{"something"},
},
},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"attributes.member_of_groups\", desc: \"This is a read only field.\"",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, gErr := s.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "CreateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
require.NotEmpty(tc.errContains)
assert.Contains(gErr.Error(), tc.errContains)
return
}
require.NoError(gErr)
if got != nil {
assert.Contains(got.GetUri(), tc.res.Uri)
assert.True(strings.HasPrefix(got.GetItem().GetId(), globals.LdapAccountPrefix+"_"))
// Clear all values which are hard to compare against.
got.Uri, tc.res.Uri = "", ""
got.Item.Id, tc.res.Item.Id = "", ""
got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestUpdatePassword(t *testing.T) {
ctx := context.TODO()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kms)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kms)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kms)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kms)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0]
tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new accounts service.")
defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}
defaultAttributes := &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "default",
},
}
modifiedAttributes := &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "modified",
},
}
freshAccount := func(t *testing.T) (*pb.Account, func()) {
t.Helper()
acc, err := tested.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()),
&pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: wrapperspb.String("default"),
Description: wrapperspb.String("default"),
Type: "password",
Attrs: defaultAttributes,
},
},
)
require.NoError(t, err)
clean := func() {
_, err := tested.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()),
&pbs.DeleteAccountRequest{Id: acc.GetItem().GetId()})
require.NoError(t, err)
}
return acc.GetItem(), clean
}
cases := []struct {
name string
req *pbs.UpdateAccountRequest
res *pbs.UpdateAccountResponse
err error
}{
{
name: "Update an Existing AuthMethod",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField, globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: "password",
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: "password",
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "Multiple Paths in single string",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name,description"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: "password",
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: "password",
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "No Update Mask",
req: &pbs.UpdateAccountRequest{
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Cant change type",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: ""},
Type: "oidc",
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "No Paths in Mask",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{}},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Only non-existant paths in Mask",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Unset Name",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Description: &wrapperspb.StringValue{Value: "ignored"},
Attrs: modifiedAttributes,
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Description: &wrapperspb.StringValue{Value: "default"},
Type: "password",
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "Update Only Name",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated"},
Description: &wrapperspb.StringValue{Value: "ignored"},
Attrs: modifiedAttributes,
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "updated"},
Description: &wrapperspb.StringValue{Value: "default"},
Type: "password",
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "Update Only Description",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
Description: &wrapperspb.StringValue{Value: "notignored"},
Attrs: modifiedAttributes,
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "default"},
Description: &wrapperspb.StringValue{Value: "notignored"},
Type: "password",
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "Update Only LoginName",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.login_name"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
Description: &wrapperspb.StringValue{Value: "ignored"},
Attrs: modifiedAttributes,
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "default"},
Description: &wrapperspb.StringValue{Value: "default"},
Type: "password",
Attrs: modifiedAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: pwAuthorizedActions,
},
},
},
{
name: "Update a Non Existing Old ID Account",
req: &pbs.UpdateAccountRequest{
Id: globals.PasswordAccountPreviousPrefix + "_DoesntExis",
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
},
},
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Update a Non Existing New ID Account",
req: &pbs.UpdateAccountRequest{
Id: globals.PasswordAccountPrefix + "_DoesntExis",
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
},
},
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Cant change Id",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"id"},
},
Item: &pb.Account{
Id: globals.PasswordAccountPrefix + "_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.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"created_time"},
},
Item: &pb.Account{
CreatedTime: timestamppb.Now(),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Cant specify Updated Time",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"updated_time"},
},
Item: &pb.Account{
UpdatedTime: timestamppb.Now(),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Cant specify Type",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"type"},
},
Item: &pb.Account{
Type: "oidc",
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
acc, cleanup := freshAccount(t)
defer cleanup()
tc.req.Item.Version = 1
if tc.req.GetId() == "" {
tc.req.Id = acc.GetId()
}
if tc.res != nil && tc.res.Item != nil {
tc.res.Item.Id = acc.GetId()
tc.res.Item.CreatedTime = acc.GetCreatedTime()
}
got, gErr := tested.UpdateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "UpdateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
} else {
require.NoError(gErr)
}
if tc.res == nil {
require.Nil(got)
}
if got != nil {
assert.NotNilf(tc.res, "Expected UpdateAccount response to be nil, but was %v", got)
gotUpdateTime := got.GetItem().GetUpdatedTime()
require.NoError(err, "Error converting proto to timestamp")
created := acc.GetCreatedTime()
require.NoError(err, "Error converting proto to timestamp")
// Verify it is a auth_method updated after it was created
assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Updated account 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()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestUpdateOidc(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
am := oidc.TestAuthMethod(
t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState,
"alice-rp", "fido",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]),
oidc.WithSigningAlgs(oidc.RS256),
oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]))
tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new auth_method service.")
defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}
defaultAttributes := &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Issuer: "https://www.alice.com",
Subject: "test-subject",
},
}
modifiedAttributes := &pb.Account_OidcAccountAttributes{
&pb.OidcAccountAttributes{
Issuer: "https://www.changed.com",
Subject: "changed",
},
}
freshAccount := func(t *testing.T) (*oidc.Account, func()) {
t.Helper()
acc := oidc.TestAccount(t, conn, am, "test-subject", oidc.WithName("default"), oidc.WithDescription("default"))
clean := func() {
_, err := tested.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()),
&pbs.DeleteAccountRequest{Id: acc.GetPublicId()})
require.NoError(t, err)
}
return acc, clean
}
cases := []struct {
name string
req *pbs.UpdateAccountRequest
res *pbs.UpdateAccountResponse
err error
}{
{
name: "Update an Existing AuthMethod",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField, globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: oidc.Subtype.String(),
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: oidc.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "Multiple Paths in single string",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name,description"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: oidc.Subtype.String(),
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: oidc.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "No Update Mask",
req: &pbs.UpdateAccountRequest{
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Cant change type",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: ""},
Type: "oidc",
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "No Paths in Mask",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{}},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Only non-existant paths in Mask",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Unset Name",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Description: &wrapperspb.StringValue{Value: "ignored"},
Attrs: modifiedAttributes,
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Description: &wrapperspb.StringValue{Value: "default"},
Type: oidc.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "Update Only Name",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated"},
Description: &wrapperspb.StringValue{Value: "ignored"},
Attrs: modifiedAttributes,
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "updated"},
Description: &wrapperspb.StringValue{Value: "default"},
Type: oidc.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "Update Only Description",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
Description: &wrapperspb.StringValue{Value: "notignored"},
Attrs: modifiedAttributes,
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "default"},
Description: &wrapperspb.StringValue{Value: "notignored"},
Type: oidc.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: oidcAuthorizedActions,
},
},
},
{
name: "Update LoginName",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.login_name"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
Description: &wrapperspb.StringValue{Value: "ignored"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Update a Non Existing Account",
req: &pbs.UpdateAccountRequest{
Id: globals.PasswordAccountPrefix + "_DoesntExis",
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
},
},
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Cant change Id",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"id"},
},
Item: &pb.Account{
Id: globals.PasswordAccountPrefix + "_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.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"created_time"},
},
Item: &pb.Account{
CreatedTime: timestamppb.Now(),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Cant specify Updated Time",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"updated_time"},
},
Item: &pb.Account{
UpdatedTime: timestamppb.Now(),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Cant specify Type",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"type"},
},
Item: &pb.Account{
Type: "oidc",
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Update Issuer",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.issuer"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Update Subject",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.subject"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
Attrs: modifiedAttributes,
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
acc, cleanup := freshAccount(t)
defer cleanup()
tc.req.Item.Version = 1
if tc.req.GetId() == "" {
tc.req.Id = acc.GetPublicId()
}
if tc.res != nil && tc.res.Item != nil {
tc.res.Item.Id = acc.GetPublicId()
tc.res.Item.CreatedTime = acc.GetCreateTime().GetTimestamp()
}
got, gErr := tested.UpdateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "UpdateAccount(%+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 UpdateAccount response to be nil, but was %v", got)
gotUpdateTime := got.GetItem().GetUpdatedTime()
require.NoError(err, "Error converting proto to timestamp")
created := acc.GetCreateTime().GetTimestamp()
require.NoError(err, "Error converting proto to timestamp")
// Verify it is a auth_method updated after it was created
assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Updated account 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()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestUpdateLdap(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kmsCache)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
am := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap"})
tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new auth_method service.")
defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}
defaultAttributes := &pb.Account_LdapAccountAttributes{
LdapAccountAttributes: &pb.LdapAccountAttributes{
LoginName: "test-login",
},
}
freshAccount := func(t *testing.T) (*ldap.Account, func()) {
t.Helper()
acc := ldap.TestAccount(t, conn, am, "test-login", ldap.WithName(ctx, "default"), ldap.WithDescription(ctx, "default"))
clean := func() {
_, err := tested.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()),
&pbs.DeleteAccountRequest{Id: acc.GetPublicId()})
require.NoError(t, err)
}
return acc, clean
}
cases := []struct {
name string
req *pbs.UpdateAccountRequest
res *pbs.UpdateAccountResponse
err error
errContains string
}{
{
name: "update-existing",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField, globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: ldap.Subtype.String(),
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: ldap.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: ldapAuthorizedActions,
},
},
},
{
name: "Multiple Paths in single string",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name,description"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: ldap.Subtype.String(),
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
Type: ldap.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: ldapAuthorizedActions,
},
},
},
{
name: "No Update Mask",
req: &pbs.UpdateAccountRequest{
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "UpdateMask not provided but is required to update this resource",
},
{
name: "Cant change type",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: ""},
Type: "oidc",
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "Cannot modify the resource type",
},
{
name: "No Paths in Mask",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{}},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "missing mask",
},
{
name: "Only non-existent paths in Mask",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated name"},
Description: &wrapperspb.StringValue{Value: "updated desc"},
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "No valid fields included in the update mask",
},
{
name: "Unset Name",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Description: &wrapperspb.StringValue{Value: "ignored"},
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Description: &wrapperspb.StringValue{Value: "default"},
Type: ldap.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: ldapAuthorizedActions,
},
},
},
{
name: "Update Only Name",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.NameField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "updated"},
Description: &wrapperspb.StringValue{Value: "ignored"},
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "updated"},
Description: &wrapperspb.StringValue{Value: "default"},
Type: ldap.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: ldapAuthorizedActions,
},
},
},
{
name: "Update Only Description",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
Description: &wrapperspb.StringValue{Value: "notignored"},
},
},
res: &pbs.UpdateAccountResponse{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Name: &wrapperspb.StringValue{Value: "default"},
Description: &wrapperspb.StringValue{Value: "notignored"},
Type: ldap.Subtype.String(),
Attrs: defaultAttributes,
Scope: defaultScopeInfo,
AuthorizedActions: ldapAuthorizedActions,
},
},
},
{
name: "Update LoginName",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.login_name"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
Description: &wrapperspb.StringValue{Value: "ignored"},
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "Field cannot be updated.",
},
{
name: "Update a Non Existing Account",
req: &pbs.UpdateAccountRequest{
Id: globals.LdapAccountPrefix + "_DoesntExis",
UpdateMask: &field_mask.FieldMask{
Paths: []string{globals.DescriptionField},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "desc"},
},
},
err: handlers.ApiErrorWithCode(codes.NotFound),
errContains: "Resource not found.",
},
{
name: "Cant change Id",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"id"},
},
Item: &pb.Account{
Id: globals.LdapAccountPrefix + "_somethinge",
Name: &wrapperspb.StringValue{Value: "new"},
Description: &wrapperspb.StringValue{Value: "new desc"},
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"id\", desc: \"This is a read only field and cannot be specified in an update request.",
},
{
name: "Cant specify Created Time",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"created_time"},
},
Item: &pb.Account{
CreatedTime: timestamppb.Now(),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"created_time\", desc: \"This is a read only field and cannot be specified in an update request.",
},
{
name: "Cant specify Updated Time",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"updated_time"},
},
Item: &pb.Account{
UpdatedTime: timestamppb.Now(),
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"updated_time\", desc: \"This is a read only field and cannot be specified in an update request.",
},
{
name: "Cant specify Type",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"type"},
},
Item: &pb.Account{
Type: "oidc",
},
},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"type\", desc: \"Cannot modify the resource type.",
},
{
name: "Update Login name",
req: &pbs.UpdateAccountRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.login_name"},
},
Item: &pb.Account{
Name: &wrapperspb.StringValue{Value: "ignored"},
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
errContains: "name: \"attributes.login_name\", desc: \"Field cannot be updated.",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
acc, cleanup := freshAccount(t)
defer cleanup()
tc.req.Item.Version = 1
if tc.req.GetId() == "" {
tc.req.Id = acc.GetPublicId()
}
if tc.res != nil && tc.res.Item != nil {
tc.res.Item.Id = acc.GetPublicId()
tc.res.Item.CreatedTime = acc.GetCreateTime().GetTimestamp()
}
got, gErr := tested.UpdateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "UpdateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
require.NotEmpty(tc.errContains)
assert.Contains(gErr.Error(), tc.errContains)
return
}
require.NoError(gErr)
if tc.res == nil {
require.Nil(got)
}
if got != nil {
assert.NotNilf(tc.res, "Expected UpdateAccount response to be nil, but was %v", got)
gotUpdateTime := got.GetItem().GetUpdatedTime()
require.NoError(err, "Error converting proto to timestamp")
created := acc.GetCreateTime().GetTimestamp()
require.NoError(err, "Error converting proto to timestamp")
// Verify it is a auth_method updated after it was created
assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Updated account 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()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestSetPassword(t *testing.T) {
ctx := context.TODO()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kms)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kms)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kms)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kms)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new auth_method service.")
createAccount := func(t *testing.T, pw string) *pb.Account {
am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0]
pwAttrs := &pb.Account_PasswordAccountAttributes{
&pb.PasswordAccountAttributes{
LoginName: "testusername",
},
}
if pw != "" {
pwAttrs.PasswordAccountAttributes.Password = wrapperspb.String(pw)
}
createResp, err := tested.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: "password",
Attrs: pwAttrs,
},
})
require.NoError(t, err)
require.NotNil(t, createResp)
require.NotNil(t, createResp.GetItem())
return createResp.GetItem()
}
cases := []struct {
name string
oldPw string
newPw string
}{
{
name: "has old set new",
oldPw: "originalpassword",
newPw: "a different password",
},
{
name: "has old unset new",
oldPw: "originalpassword",
newPw: "",
},
{
name: "no old password set new",
oldPw: "",
newPw: "a different password",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
acct := createAccount(t, tt.oldPw)
setResp, err := tested.SetPassword(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.SetPasswordRequest{
Id: acct.GetId(),
Version: acct.GetVersion(),
Password: tt.newPw,
})
require.NoError(err)
assert.Equal(acct.GetVersion()+1, setResp.GetItem().GetVersion())
// clear uncomparable fields
acct.Version, setResp.GetItem().Version = 0, 0
acct.UpdatedTime, setResp.GetItem().UpdatedTime = nil, nil
assert.Empty(cmp.Diff(acct, setResp.GetItem(), protocmp.Transform()))
})
}
defaultAcct := createAccount(t, "")
badRequestCases := []struct {
name string
accountId string
version uint32
password string
}{
{
name: "empty account id",
accountId: "",
version: defaultAcct.GetVersion(),
password: "somepassword",
},
{
name: "unset version",
accountId: defaultAcct.GetId(),
version: 0,
password: "somepassword",
},
{
name: "notfound old account id",
accountId: globals.PasswordAccountPreviousPrefix + "_DoesntExis",
version: defaultAcct.GetVersion(),
password: "anewpassword",
},
{
name: "notfound new account id",
accountId: globals.PasswordAccountPrefix + "_DoesntExis",
version: defaultAcct.GetVersion(),
password: "anewpassword",
},
{
name: "password too short",
accountId: defaultAcct.GetId(),
version: defaultAcct.GetVersion(),
password: "123",
},
}
for _, tt := range badRequestCases {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
setResp, err := tested.SetPassword(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.SetPasswordRequest{
Id: tt.accountId,
Version: tt.version,
Password: tt.password,
})
assert.Error(err)
assert.Nil(setResp)
})
}
}
func TestChangePassword(t *testing.T) {
ctx := context.TODO()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrap)
pwRepoFn := func() (*password.Repository, error) {
return password.NewRepository(ctx, rw, rw, kms)
}
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kms)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kms)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kms)
}
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn)
require.NoError(t, err, "Error when getting new auth_method service.")
createAccount := func(t *testing.T, pw string) *pb.Account {
am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0]
pwAttrs := &pb.Account_PasswordAccountAttributes{&pb.PasswordAccountAttributes{
LoginName: "testusername",
}}
if pw != "" {
pwAttrs.PasswordAccountAttributes.Password = wrapperspb.String(pw)
}
createResp, err := tested.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.CreateAccountRequest{
Item: &pb.Account{
AuthMethodId: am.GetPublicId(),
Type: "password",
Attrs: pwAttrs,
},
})
require.NoError(t, err)
require.NotNil(t, createResp)
require.NotNil(t, createResp.GetItem())
return createResp.GetItem()
}
t.Run("valid update", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
acct := createAccount(t, "originalpassword")
changeResp, err := tested.ChangePassword(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.ChangePasswordRequest{
Id: acct.GetId(),
Version: acct.GetVersion(),
CurrentPassword: "originalpassword",
NewPassword: "a different password",
})
require.NoError(err)
assert.Equal(acct.GetVersion()+1, changeResp.GetItem().GetVersion())
// clear uncomparable fields
acct.Version, changeResp.GetItem().Version = 0, 0
acct.UpdatedTime, changeResp.GetItem().UpdatedTime = nil, nil
assert.Empty(cmp.Diff(acct, changeResp.GetItem(), protocmp.Transform()))
})
t.Run("unauthenticated update", func(t *testing.T) {
assert := assert.New(t)
acct := createAccount(t, "originalpassword")
changeResp, err := tested.ChangePassword(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.ChangePasswordRequest{
Id: acct.GetId(),
Version: acct.GetVersion(),
CurrentPassword: "thewrongpassword",
NewPassword: "a different password",
})
assert.Error(err)
assert.Nil(changeResp)
})
defaultAcct := createAccount(t, "")
badRequestCases := []struct {
name string
authMethodId string
accountId string
version uint32
oldPW string
newPW string
}{
{
name: "empty auth method",
authMethodId: "",
accountId: defaultAcct.GetId(),
version: defaultAcct.GetVersion(),
oldPW: "somepassword",
newPW: "anewpassword",
},
{
name: "empty account id",
authMethodId: defaultAcct.GetAuthMethodId(),
accountId: "",
version: defaultAcct.GetVersion(),
oldPW: "somepassword",
newPW: "anewpassword",
},
{
name: "unset version",
authMethodId: defaultAcct.GetAuthMethodId(),
accountId: defaultAcct.GetId(),
version: 0,
oldPW: "somepassword",
newPW: "anewpassword",
},
{
name: "no old password",
authMethodId: defaultAcct.GetAuthMethodId(),
accountId: defaultAcct.GetId(),
version: defaultAcct.GetVersion(),
oldPW: "",
newPW: "anewpassword",
},
{
name: "no new password",
authMethodId: defaultAcct.GetAuthMethodId(),
accountId: defaultAcct.GetId(),
version: defaultAcct.GetVersion(),
oldPW: "somepassword",
newPW: "",
},
{
name: "matching old and new passwords",
authMethodId: defaultAcct.GetAuthMethodId(),
accountId: defaultAcct.GetId(),
version: defaultAcct.GetVersion(),
oldPW: "somepassword",
newPW: "somepassword",
},
{
name: "notfound old account id",
authMethodId: defaultAcct.GetAuthMethodId(),
accountId: globals.PasswordAccountPreviousPrefix + "_DoesntExis",
version: defaultAcct.GetVersion(),
oldPW: "somepassword",
newPW: "anewpassword",
},
{
name: "notfound new account id",
authMethodId: defaultAcct.GetAuthMethodId(),
accountId: globals.PasswordAccountPrefix + "_DoesntExis",
version: defaultAcct.GetVersion(),
oldPW: "somepassword",
newPW: "anewpassword",
},
{
name: "new password too short",
authMethodId: defaultAcct.GetAuthMethodId(),
accountId: defaultAcct.GetId(),
version: defaultAcct.GetVersion(),
oldPW: "somepassword",
newPW: "123",
},
}
for _, tt := range badRequestCases {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
changeResp, err := tested.ChangePassword(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.ChangePasswordRequest{
Id: tt.accountId,
Version: tt.version,
CurrentPassword: tt.oldPW,
NewPassword: tt.newPW,
})
assert.Error(err)
assert.Nil(changeResp)
})
}
}