Lookup and List Repo Methods for Password Auth Accounts (#216)

This commit is contained in:
Todd Knight authored and GitHub committed 2020-07-24 11:38:27 -07:00
1 parent 7897c1cf52
commit 37d8f78091
7 files changed
+295 -8

No files matched your search

+6
View File
@@ -19,6 +19,12 @@ type Account struct {
CredentialID string `gorm:"-"`
}
func allocAccount() Account {
return Account{
Account: &store.Account{},
}
}
// NewAccount creates a new in memory Account with userName assigned to
// authMethodId. Name and description are the only valid options. All other
// options are ignored.
+10
View File
@@ -16,6 +16,7 @@ type Option func(*options)
type options struct {
withName string
withDescription string
withLimit int
}
func getDefaultOptions() options {
@@ -35,3 +36,12 @@ func WithName(name string) Option {
o.withName = name
}
}
// WithLimit provides an option to provide a limit. Intentionally allowing
// negative integers. If WithLimit < 0, then unlimited results are returned.
// If WithLimit == 0, then default limits are used for results.
func WithLimit(l int) Option {
return func(o *options) {
o.withLimit = l
}
}
+6
View File
@@ -20,4 +20,10 @@ func Test_GetOpts(t *testing.T) {
testOpts.withDescription = "test desc"
assert.Equal(t, opts, testOpts)
})
t.Run("WithLimit", func(t *testing.T) {
opts := getOpts(WithLimit(5))
testOpts := getDefaultOptions()
testOpts.withLimit = 5
assert.Equal(t, opts, testOpts)
})
}
+15 -5
View File
@@ -13,12 +13,15 @@ type Repository struct {
reader db.Reader
writer db.Writer
wrapper wrapping.Wrapper
// defaultLimit provides a default for limiting the number of results returned from the repo
defaultLimit int
}
// NewRepository creates a new Repository. The returned repository should
// only be used for one transaction and it is not safe for concurrent go
// routines to access it.
func NewRepository(r db.Reader, w db.Writer, wrapper wrapping.Wrapper) (*Repository, error) {
// routines to access it. WithLimit option is used as a repo wide default
// limit applied to all ListX methods.
func NewRepository(r db.Reader, w db.Writer, wrapper wrapping.Wrapper, opt ...Option) (*Repository, error) {
switch {
case r == nil:
return nil, fmt.Errorf("db.Reader: %w", db.ErrNilParameter)
@@ -28,9 +31,16 @@ func NewRepository(r db.Reader, w db.Writer, wrapper wrapping.Wrapper) (*Reposit
return nil, fmt.Errorf("wrapping.Wrapper: %w", db.ErrNilParameter)
}
opts := getOpts(opt...)
if opts.withLimit == 0 {
// zero signals the watchtower defaults should be used.
opts.withLimit = db.DefaultLimit
}
return &Repository{
reader: r,
writer: w,
wrapper: wrapper,
reader: r,
writer: w,
wrapper: wrapper,
defaultLimit: opts.withLimit,
}, nil
}
@@ -2,6 +2,7 @@ package password
import (
"context"
"errors"
"fmt"
"regexp"
@@ -62,6 +63,42 @@ func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Optio
return newAccount, nil
}
// LookupAccount will look up an account in the repository. If the account is not
// found, it will return nil, nil. All options are ignored.
func (r *Repository) LookupAccount(ctx context.Context, withPublicId string, opt ...Option) (*Account, error) {
if withPublicId == "" {
return nil, fmt.Errorf("lookup: password account: missing public id %w", db.ErrInvalidParameter)
}
a := allocAccount()
a.PublicId = withPublicId
if err := r.reader.LookupByPublicId(ctx, &a); err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return nil, nil
}
return nil, fmt.Errorf("lookup: password account: failed %w for %s", err, withPublicId)
}
return &a, nil
}
// ListAccounts in an auth method and supports WithLimit option.
func (r *Repository) ListAccounts(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*Account, error) {
if withAuthMethodId == "" {
return nil, fmt.Errorf("list: password account: missing auth method id %w", db.ErrInvalidParameter)
}
opts := getOpts(opt...)
limit := r.defaultLimit
if opts.withLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
}
var accts []*Account
err := r.reader.SearchWhere(ctx, &accts, "auth_method_id = ?", []interface{}{withAuthMethodId}, db.WithLimit(limit))
if err != nil {
return nil, fmt.Errorf("list: password account: %w", err)
}
return accts, nil
}
var reInvalidUserName = regexp.MustCompile("[^a-z0-9.]")
func validUserName(u string) bool {
@@ -3,10 +3,12 @@ package password
import (
"context"
"errors"
"fmt"
"testing"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -31,6 +33,35 @@ func TestCheckUserName(t *testing.T) {
}
}
func testAccounts(t *testing.T, conn *gorm.DB, scopeId, authMethodId string, count int) []*Account {
t.Helper()
assert, require := assert.New(t), require.New(t)
w := db.New(conn)
var auts []*Account
for i := 0; i < count; i++ {
cat, err := NewAccount(authMethodId, fmt.Sprintf("name%d", i))
assert.NoError(err)
require.NotNil(cat)
id, err := newAuthMethodId()
assert.NoError(err)
require.NotEmpty(id)
cat.PublicId = id
ctx := context.Background()
_, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, iw db.Writer) error {
return iw.Create(ctx, cat)
},
)
require.NoError(err2)
// TODO(toddknight): Figure out why the iw.Create call doesn't populate the scope id from the DB.
cat.ScopeId = scopeId
auts = append(auts, cat)
}
return auts
}
func TestRepository_CreateAccount(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
@@ -255,3 +286,173 @@ func TestRepository_CreateAccount(t *testing.T) {
assert.Equal(got2.CreateTime, got2.UpdateTime)
})
}
func TestRepository_LookupAccount(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
authMethod := testAuthMethods(t, conn, 1)[0]
account := testAccounts(t, conn, authMethod.GetScopeId(), authMethod.GetPublicId(), 1)[0]
newAcctId, err := newAccountId()
require.NoError(t, err)
var tests = []struct {
name string
in string
want *Account
wantIsErr error
}{
{
name: "With no public id",
wantIsErr: db.ErrInvalidParameter,
},
{
name: "With non existing account id",
in: newAcctId,
},
{
name: "With existing account id",
in: account.GetPublicId(),
want: account,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(rw, rw, wrapper)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.LookupAccount(context.Background(), tt.in)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(got)
return
}
require.NoError(err)
assert.EqualValues(tt.want, got)
})
}
}
func TestRepository_ListAccounts(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
authMethods := testAuthMethods(t, conn, 3)
accounts1 := testAccounts(t, conn, authMethods[0].GetScopeId(), authMethods[0].GetPublicId(), 3)
accounts2 := testAccounts(t, conn, authMethods[1].GetScopeId(), authMethods[1].GetPublicId(), 4)
_ = accounts2
var tests = []struct {
name string
in string
opts []Option
want []*Account
wantIsErr error
}{
{
name: "With no auth method id",
wantIsErr: db.ErrInvalidParameter,
},
{
name: "With no accounts id",
in: authMethods[2].GetPublicId(),
want: []*Account{},
},
{
name: "With first auth method id",
in: authMethods[0].GetPublicId(),
want: accounts1,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(rw, rw, wrapper)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListAccounts(context.Background(), tt.in, tt.opts...)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(got)
return
}
require.NoError(err)
assert.EqualValues(tt.want, got)
})
}
}
func TestRepository_ListAccounts_Limits(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
am := testAuthMethods(t, conn, 1)[0]
accountCount := 10
_ = testAccounts(t, conn, am.GetScopeId(), am.GetPublicId(), accountCount)
var tests = []struct {
name string
repoOpts []Option
listOpts []Option
wantLen int
}{
{
name: "With no limits",
wantLen: accountCount,
},
{
name: "With repo limit",
repoOpts: []Option{WithLimit(3)},
wantLen: 3,
},
{
name: "With negative repo limit",
repoOpts: []Option{WithLimit(-1)},
wantLen: accountCount,
},
{
name: "With List limit",
listOpts: []Option{WithLimit(3)},
wantLen: 3,
},
{
name: "With negative List limit",
listOpts: []Option{WithLimit(-1)},
wantLen: accountCount,
},
{
name: "With repo smaller than list limit",
repoOpts: []Option{WithLimit(2)},
listOpts: []Option{WithLimit(6)},
wantLen: 6,
},
{
name: "With repo larger than list limit",
repoOpts: []Option{WithLimit(6)},
listOpts: []Option{WithLimit(2)},
wantLen: 2,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(rw, rw, wrapper, tt.repoOpts...)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListAccounts(context.Background(), am.GetPublicId(), tt.listOpts...)
require.NoError(err)
assert.Len(got, tt.wantLen)
})
}
}
+20 -3
View File
@@ -20,6 +20,7 @@ func TestRepository_New(t *testing.T) {
r db.Reader
w db.Writer
wrapper wrapping.Wrapper
opts []Option
}
var tests = []struct {
@@ -36,9 +37,25 @@ func TestRepository_New(t *testing.T) {
wrapper: wrapper,
},
want: &Repository{
reader: rw,
writer: rw,
reader: rw,
writer: rw,
wrapper: wrapper,
defaultLimit: db.DefaultLimit,
},
},
{
name: "valid with limit",
args: args{
r: rw,
w: rw,
wrapper: wrapper,
opts: []Option{WithLimit(5)},
},
want: &Repository{
reader: rw,
writer: rw,
wrapper: wrapper,
defaultLimit: 5,
},
},
{
@@ -86,7 +103,7 @@ func TestRepository_New(t *testing.T) {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := NewRepository(tt.args.r, tt.args.w, tt.args.wrapper)
got, err := NewRepository(tt.args.r, tt.args.w, tt.args.wrapper, tt.args.opts...)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(got)