Add base resources for password authentication (#213)

* Add base SQL and protobuf files

* Add options and public Ids

* Add AuthMethod and Account resources

* Add repository

* Add CreateAuthMethod to repository

* Add CreateAccount method to repository

* Remove redundant foreign keys

* Document relationships between iam and auth tables

* Document relationships between auth and auth_password tables

* Add documentation for subtype triggers

* Use wt_scope_id domain type for scope_id

* Refactor: move test helper function

* Remove check from SetTableName

* Remove check from another SetTableName

* Use org scope in tests

* Remove handling of CreateTime and UpdateTime from docs
This commit is contained in:
Michael Gaffney authored and GitHub committed 2020-07-24 10:23:35 -04:00
1 parent e03a0ff9d7
commit 7897c1cf52
22 files changed
+2307 -23

No files matched your search

+1
View File
@@ -78,6 +78,7 @@ protobuild:
@protoc-go-inject-tag -input=./internal/host/static/store/static.pb.go
@protoc-go-inject-tag -input=./internal/authtoken/store/authtoken.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/auth_account.pb.go
@protoc-go-inject-tag -input=./internal/auth/password/store/password.pb.go
@rm -R ${TMP_DIR}
protolint:
+3 -3
View File
@@ -77,9 +77,9 @@ create table if not exists test_auth_account (
`
insert = `
insert into test_auth_account
(public_id, auth_method_id, scope_id)
(public_id, auth_method_id)
values
($1, $2, $3);
($1, $2);
`
addTriggers = `
create trigger
@@ -118,7 +118,7 @@ values
require.NoError(err)
id := "l1Ocw0TpHn800CekIxIXlmQqRDgFDfYl"
_, err = db.Query(insert, id, meth_id, org.GetPublicId())
_, err = db.Query(insert, id, meth_id)
require.NoError(err)
var count int
+77
View File
@@ -0,0 +1,77 @@
package password
import (
"fmt"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/oplog"
"google.golang.org/protobuf/proto"
)
// An Account contains a user name. It is owned by an auth method.
type Account struct {
*store.Account
tableName string
// CredentialID is included when Authenticate or ChangePassword is
// called. A new CredentialID is generated when a password is changed.
CredentialID string `gorm:"-"`
}
// 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.
func NewAccount(authMethodId string, userName string, opt ...Option) (*Account, error) {
// NOTE(mgaffney): The scopeId in the embedded *store.Account is
// populated by a trigger in the database.
if authMethodId == "" {
return nil, fmt.Errorf("new: password account: no auth method id: %w", db.ErrInvalidParameter)
}
if userName == "" {
return nil, fmt.Errorf("new: password account: no user name: %w", db.ErrInvalidParameter)
}
opts := getOpts(opt...)
a := &Account{
Account: &store.Account{
AuthMethodId: authMethodId,
UserName: userName,
Name: opts.withName,
Description: opts.withDescription,
},
}
return a, nil
}
func (a *Account) clone() *Account {
cp := proto.Clone(a.Account)
return &Account{
Account: cp.(*store.Account),
}
}
// TableName returns the table name.
func (a *Account) TableName() string {
if a.tableName != "" {
return a.tableName
}
return "auth_password_account"
}
// SetTableName sets the table name.
func (a *Account) SetTableName(n string) {
a.tableName = n
}
func (a *Account) oplog(op oplog.OpType) oplog.Metadata {
metadata := oplog.Metadata{
"resource-public-id": []string{a.GetPublicId()},
"resource-type": []string{"password account"},
"op-type": []string{op.String()},
}
if a.AuthMethodId != "" {
metadata["auth-method-id"] = []string{a.AuthMethodId}
}
return metadata
}
+115
View File
@@ -0,0 +1,115 @@
package password
import (
"context"
"testing"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAccount_New(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
w := db.New(conn)
auts := testAuthMethods(t, conn, 1)
aut := auts[0]
type args struct {
authMethodId string
userName string
opts []Option
}
var tests = []struct {
name string
args args
want *Account
wantErr bool
}{
{
name: "blank-authMethodId",
args: args{
authMethodId: "",
},
want: nil,
wantErr: true,
},
{
name: "valid-no-options",
args: args{
authMethodId: aut.GetPublicId(),
userName: "kazmierczak",
},
want: &Account{
Account: &store.Account{
AuthMethodId: aut.GetPublicId(),
UserName: "kazmierczak",
},
},
},
{
name: "valid-with-name",
args: args{
authMethodId: aut.GetPublicId(),
userName: "kazmierczak1",
opts: []Option{
WithName("test-name"),
},
},
want: &Account{
Account: &store.Account{
AuthMethodId: aut.GetPublicId(),
UserName: "kazmierczak1",
Name: "test-name",
},
},
},
{
name: "valid-with-description",
args: args{
authMethodId: aut.GetPublicId(),
userName: "kazmierczak2",
opts: []Option{
WithDescription("test-description"),
},
},
want: &Account{
Account: &store.Account{
AuthMethodId: aut.GetPublicId(),
UserName: "kazmierczak2",
Description: "test-description",
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := NewAccount(tt.args.authMethodId, tt.args.userName, tt.args.opts...)
if tt.wantErr {
assert.Error(err)
require.Nil(got)
return
}
assert.NoError(err)
require.NotNil(got)
assert.Emptyf(got.PublicId, "PublicId set")
assert.Equal(tt.want, got)
id, err := newAccountId()
assert.NoError(err)
tt.want.PublicId = id
got.PublicId = id
err2 := w.Create(context.Background(), got)
assert.NoError(err2)
})
}
}
+70
View File
@@ -0,0 +1,70 @@
package password
import (
"fmt"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/oplog"
"google.golang.org/protobuf/proto"
)
// A AuthMethod contains accounts and password configurations. It is owned
// by a scope.
type AuthMethod struct {
*store.AuthMethod
tableName string
}
// NewAuthMethod creates a new in memory AuthMethod assigned to scopeId.
// Name and description are the only valid options. All other options are
// ignored.
func NewAuthMethod(scopeId string, opt ...Option) (*AuthMethod, error) {
if scopeId == "" {
return nil, fmt.Errorf("new: password auth method: no scope id: %w", db.ErrInvalidParameter)
}
opts := getOpts(opt...)
a := &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: scopeId,
Name: opts.withName,
Description: opts.withDescription,
MinUserNameLength: 5,
MinPasswordLength: 8,
},
}
return a, nil
}
func (a *AuthMethod) clone() *AuthMethod {
cp := proto.Clone(a.AuthMethod)
return &AuthMethod{
AuthMethod: cp.(*store.AuthMethod),
}
}
// TableName returns the table name.
func (a *AuthMethod) TableName() string {
if a.tableName != "" {
return a.tableName
}
return "auth_password_method"
}
// SetTableName sets the table name.
func (a *AuthMethod) SetTableName(n string) {
a.tableName = n
}
func (a *AuthMethod) oplog(op oplog.OpType) oplog.Metadata {
metadata := oplog.Metadata{
"resource-public-id": []string{a.GetPublicId()},
"resource-type": []string{"password auth method"},
"op-type": []string{op.String()},
}
if a.ScopeId != "" {
metadata["scope-id"] = []string{a.ScopeId}
}
return metadata
}
+144
View File
@@ -0,0 +1,144 @@
package password
import (
"context"
"testing"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testAuthMethods(t *testing.T, conn *gorm.DB, count int) []*AuthMethod {
t.Helper()
assert, require := assert.New(t), require.New(t)
w := db.New(conn)
org, _ := iam.TestScopes(t, conn)
var auts []*AuthMethod
for i := 0; i < count; i++ {
cat, err := NewAuthMethod(org.GetPublicId())
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)
auts = append(auts, cat)
}
return auts
}
func TestAuthMethod_New(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
w := db.New(conn)
type args struct {
scopeId string
opts []Option
}
var tests = []struct {
name string
args args
want *AuthMethod
wantErr bool
}{
{
name: "valid-no-options",
args: args{},
want: &AuthMethod{
AuthMethod: &store.AuthMethod{
MinUserNameLength: 5,
MinPasswordLength: 8,
},
},
},
{
name: "valid-with-name",
args: args{
opts: []Option{
WithName("test-name"),
},
},
want: &AuthMethod{
AuthMethod: &store.AuthMethod{
Name: "test-name",
MinUserNameLength: 5,
MinPasswordLength: 8,
},
},
},
{
name: "valid-with-description",
args: args{
opts: []Option{
WithDescription("test-description"),
},
},
want: &AuthMethod{
AuthMethod: &store.AuthMethod{
Description: "test-description",
MinUserNameLength: 5,
MinPasswordLength: 8,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
org, _ := iam.TestScopes(t, conn)
got, err := NewAuthMethod(org.GetPublicId(), tt.args.opts...)
if tt.wantErr {
assert.Error(err)
require.Nil(got)
return
}
require.NoError(err)
require.NotNil(got)
tt.want.ScopeId = org.GetPublicId()
assert.Emptyf(got.PublicId, "PublicId set")
assert.Equal(tt.want, got)
id, err := newAuthMethodId()
assert.NoError(err)
tt.want.PublicId = id
got.PublicId = id
conn.LogMode(true)
ctx := context.Background()
_, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, iw db.Writer) error {
return iw.Create(ctx, got)
},
)
conn.LogMode(false)
assert.NoError(err2)
})
}
t.Run("blank-scopeId", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := NewAuthMethod("")
assert.Error(err)
require.Nil(got)
})
}
+37
View File
@@ -0,0 +1,37 @@
package password
// getOpts - iterate the inbound Options and return a struct.
func getOpts(opt ...Option) options {
opts := getDefaultOptions()
for _, o := range opt {
o(&opts)
}
return opts
}
// Option - how Options are passed as arguments.
type Option func(*options)
// options = how options are represented
type options struct {
withName string
withDescription string
}
func getDefaultOptions() options {
return options{}
}
// WithDescription provides an optional description.
func WithDescription(desc string) Option {
return func(o *options) {
o.withDescription = desc
}
}
// WithName provides an optional name.
func WithName(name string) Option {
return func(o *options) {
o.withName = name
}
}
+23
View File
@@ -0,0 +1,23 @@
package password
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("WithName", func(t *testing.T) {
opts := getOpts(WithName("test"))
testOpts := getDefaultOptions()
testOpts.withName = "test"
assert.Equal(t, opts, testOpts)
})
t.Run("WithDescription", func(t *testing.T) {
opts := getOpts(WithDescription("test desc"))
testOpts := getDefaultOptions()
testOpts.withDescription = "test desc"
assert.Equal(t, opts, testOpts)
})
}
+29
View File
@@ -0,0 +1,29 @@
package password
import (
"fmt"
"github.com/hashicorp/watchtower/internal/db"
)
// PublicId prefixes for the resources in the password package.
const (
AuthMethodPrefix = "paum"
AccountPrefix = "pacc"
)
func newAuthMethodId() (string, error) {
id, err := db.NewPublicId(AuthMethodPrefix)
if err != nil {
return "", fmt.Errorf("new password auth method id: %w", err)
}
return id, err
}
func newAccountId() (string, error) {
id, err := db.NewPublicId(AccountPrefix)
if err != nil {
return "", fmt.Errorf("new password account id: %w", err)
}
return id, err
}
+22
View File
@@ -0,0 +1,22 @@
package password
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_PublicIds(t *testing.T) {
t.Run("authMethod", func(t *testing.T) {
id, err := newAuthMethodId()
require.NoError(t, err)
assert.True(t, strings.HasPrefix(id, AuthMethodPrefix+"_"))
})
t.Run("account", func(t *testing.T) {
id, err := newAccountId()
require.NoError(t, err)
assert.True(t, strings.HasPrefix(id, AccountPrefix+"_"))
})
}
+36
View File
@@ -0,0 +1,36 @@
package password
import (
"fmt"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/watchtower/internal/db"
)
// A Repository stores and retrieves the persistent types in the password
// package. It is not safe to use a repository concurrently.
type Repository struct {
reader db.Reader
writer db.Writer
wrapper wrapping.Wrapper
}
// 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) {
switch {
case r == nil:
return nil, fmt.Errorf("db.Reader: %w", db.ErrNilParameter)
case w == nil:
return nil, fmt.Errorf("db.Writer: %w", db.ErrNilParameter)
case wrapper == nil:
return nil, fmt.Errorf("wrapping.Wrapper: %w", db.ErrNilParameter)
}
return &Repository{
reader: r,
writer: w,
wrapper: wrapper,
}, nil
}
@@ -0,0 +1,72 @@
package password
import (
"context"
"fmt"
"regexp"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/oplog"
)
// CreateAccount inserts a into the repository and returns a new
// Account containing the account's PublicId. a is not changed. a must
// contain a valid AuthMethodId. a must not contain a PublicId. The PublicId is
// generated and assigned by this method.
//
// Both a.Name and a.Description are optional. If a.Name is set, it must be
// unique within a.AuthMethodId.
func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Option) (*Account, error) {
if a == nil {
return nil, fmt.Errorf("create: password account: %w", db.ErrNilParameter)
}
if a.Account == nil {
return nil, fmt.Errorf("create: password account: embedded Account: %w", db.ErrNilParameter)
}
if a.AuthMethodId == "" {
return nil, fmt.Errorf("create: password account: no auth method id: %w", db.ErrInvalidParameter)
}
if a.PublicId != "" {
return nil, fmt.Errorf("create: password account: public id not empty: %w", db.ErrInvalidParameter)
}
if !validUserName(a.UserName) {
return nil, fmt.Errorf("create: password account: invalid user name: %w", db.ErrInvalidParameter)
}
a = a.clone()
id, err := newAccountId()
if err != nil {
return nil, fmt.Errorf("create: password account: %w", err)
}
a.PublicId = id
var newAccount *Account
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
newAccount = a.clone()
if err := w.Create(ctx, newAccount, db.WithOplog(r.wrapper, a.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil {
return err
}
return nil
},
)
if err != nil {
if db.IsUniqueError(err) {
return nil, fmt.Errorf("create: password account: in auth method: %s: name %s already exists: %w",
a.AuthMethodId, a.Name, db.ErrNotUnique)
}
return nil, fmt.Errorf("create: password account: in auth method: %s: %w", a.AuthMethodId, err)
}
return newAccount, nil
}
var reInvalidUserName = regexp.MustCompile("[^a-z0-9.]")
func validUserName(u string) bool {
if u == "" {
return false
}
return !reInvalidUserName.Match([]byte(u))
}
@@ -0,0 +1,257 @@
package password
import (
"context"
"errors"
"testing"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheckUserName(t *testing.T) {
var tests = []struct {
in string
want bool
}{
{"", false},
{" leading-spaces", false},
{"trailing-spaces ", false},
{"contains spaces", false},
{"NotLowerCase", false},
{"valid.username", true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.in, func(t *testing.T) {
assert.Equal(t, tt.want, validUserName(tt.in))
})
}
}
func TestRepository_CreateAccount(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
authMethods := testAuthMethods(t, conn, 1)
authMethod := authMethods[0]
var tests = []struct {
name string
in *Account
opts []Option
want *Account
wantIsErr error
}{
{
name: "nil-Account",
wantIsErr: db.ErrNilParameter,
},
{
name: "nil-embedded-Account",
in: &Account{},
wantIsErr: db.ErrNilParameter,
},
{
name: "invalid-no-scope-id",
in: &Account{
Account: &store.Account{},
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-public-id-set",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
PublicId: "sthc_OOOOOOOOOO",
},
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-username-uppercase",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "KaZmiErcZak11",
},
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-username-leading-space",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: " kazmierczak12",
},
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-username-trailing-space",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kazmierczak13 ",
},
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-username-space-in-name",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kazmier czak14",
},
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "valid-no-options",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kazmierczak",
},
},
want: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kazmierczak",
},
},
},
{
name: "valid-with-name",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
Name: "test-name-repo",
UserName: "kazmierczak1",
},
},
want: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
Name: "test-name-repo",
UserName: "kazmierczak1",
},
},
},
{
name: "valid-with-description",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
Description: ("test-description-repo"),
UserName: "kazmierczak2",
},
},
want: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
Description: ("test-description-repo"),
UserName: "kazmierczak2",
},
},
},
}
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.CreateAccount(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.Empty(tt.in.PublicId)
require.NotNil(got)
assertPublicId(t, AccountPrefix, got.PublicId)
assert.NotSame(tt.in, got)
assert.Equal(tt.want.Name, got.Name)
assert.Equal(tt.want.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
})
}
t.Run("invalid-duplicate-names", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(rw, rw, wrapper)
assert.NoError(err)
require.NotNil(repo)
authMethods := testAuthMethods(t, conn, 1)
authMethod := authMethods[0]
in := &Account{
Account: &store.Account{
AuthMethodId: authMethod.GetPublicId(),
Name: "test-name-repo",
UserName: "kazmierczak3",
},
}
got, err := repo.CreateAccount(context.Background(), in)
require.NoError(err)
require.NotNil(got)
assertPublicId(t, AccountPrefix, got.PublicId)
assert.NotSame(in, got)
assert.Equal(in.Name, got.Name)
assert.Equal(in.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
got2, err := repo.CreateAccount(context.Background(), in)
assert.Truef(errors.Is(err, db.ErrNotUnique), "want err: %v got: %v", db.ErrNotUnique, err)
assert.Nil(got2)
})
t.Run("valid-duplicate-names-diff-parents", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(rw, rw, wrapper)
assert.NoError(err)
require.NotNil(repo)
authMethods := testAuthMethods(t, conn, 2)
authMethoda, authMethodb := authMethods[0], authMethods[1]
in := &Account{
Account: &store.Account{
Name: "test-name-repo",
UserName: "kazmierczak4",
},
}
in2 := in.clone()
in.AuthMethodId = authMethoda.GetPublicId()
got, err := repo.CreateAccount(context.Background(), in)
require.NoError(err)
require.NotNil(got)
assertPublicId(t, AccountPrefix, got.PublicId)
assert.NotSame(in, got)
assert.Equal(in.Name, got.Name)
assert.Equal(in.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
in2.AuthMethodId = authMethodb.GetPublicId()
got2, err := repo.CreateAccount(context.Background(), in2)
assert.NoError(err)
require.NotNil(got2)
assertPublicId(t, AccountPrefix, got2.PublicId)
assert.NotSame(in2, got2)
assert.Equal(in2.Name, got2.Name)
assert.Equal(in2.Description, got2.Description)
assert.Equal(got2.CreateTime, got2.UpdateTime)
})
}
@@ -0,0 +1,55 @@
package password
import (
"context"
"fmt"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/oplog"
)
// CreateAuthMethod inserts m into the repository and returns a new
// AuthMethod containing the auth method's PublicId. m is not changed. m must
// contain a valid ScopeId. m must not contain a PublicId. The PublicId is
// generated and assigned by this method.
//
// Both m.Name and m.Description are optional. If m.Name is set, it must be
// unique within m.ScopeId.
func (r *Repository) CreateAuthMethod(ctx context.Context, m *AuthMethod, opt ...Option) (*AuthMethod, error) {
if m == nil {
return nil, fmt.Errorf("create: password auth method: %w", db.ErrNilParameter)
}
if m.AuthMethod == nil {
return nil, fmt.Errorf("create: password auth method: embedded AuthMethod: %w", db.ErrNilParameter)
}
if m.ScopeId == "" {
return nil, fmt.Errorf("create: password auth method: no scope id: %w", db.ErrInvalidParameter)
}
if m.PublicId != "" {
return nil, fmt.Errorf("create: password auth method: public id not empty: %w", db.ErrInvalidParameter)
}
m = m.clone()
id, err := newAuthMethodId()
if err != nil {
return nil, fmt.Errorf("create: password auth method: %w", err)
}
m.PublicId = id
var newAuthMethod *AuthMethod
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
newAuthMethod = m.clone()
return w.Create(ctx, newAuthMethod, db.WithOplog(r.wrapper, m.oplog(oplog.OpType_OP_TYPE_CREATE)))
},
)
if err != nil {
if db.IsUniqueError(err) {
return nil, fmt.Errorf("create: password auth method: in scope: %s: name %s already exists: %w",
m.ScopeId, m.Name, db.ErrNotUnique)
}
return nil, fmt.Errorf("create: password auth method: in scope: %s: %w", m.ScopeId, err)
}
return newAuthMethod, nil
}
@@ -0,0 +1,196 @@
package password
import (
"context"
"errors"
"strings"
"testing"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRepository_CreateAuthMethod(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
org, _ := iam.TestScopes(t, conn)
var tests = []struct {
name string
in *AuthMethod
opts []Option
want *AuthMethod
wantIsErr error
}{
{
name: "nil-AuthMethod",
wantIsErr: db.ErrNilParameter,
},
{
name: "nil-embedded-AuthMethod",
in: &AuthMethod{},
wantIsErr: db.ErrNilParameter,
},
{
name: "invalid-no-scope-id",
in: &AuthMethod{
AuthMethod: &store.AuthMethod{},
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-public-id-set",
in: &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: org.PublicId,
PublicId: "sthc_OOOOOOOOOO",
},
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "valid-no-options",
in: &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: org.PublicId,
},
},
want: &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: org.PublicId,
},
},
},
{
name: "valid-with-name",
in: &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: org.PublicId,
Name: "test-name-repo",
},
},
want: &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: org.PublicId,
Name: "test-name-repo",
},
},
},
{
name: "valid-with-description",
in: &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: org.PublicId,
Description: ("test-description-repo"),
},
},
want: &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: org.PublicId,
Description: ("test-description-repo"),
},
},
},
}
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)
require.NoError(err)
require.NotNil(repo)
got, err := repo.CreateAuthMethod(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.Empty(tt.in.PublicId)
require.NotNil(got)
assertPublicId(t, AuthMethodPrefix, got.PublicId)
assert.NotSame(tt.in, got)
assert.Equal(tt.want.Name, got.Name)
assert.Equal(tt.want.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
})
}
t.Run("invalid-duplicate-names", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(rw, rw, wrapper)
require.NoError(err)
require.NotNil(repo)
org, _ := iam.TestScopes(t, conn)
in := &AuthMethod{
AuthMethod: &store.AuthMethod{
ScopeId: org.GetPublicId(),
Name: "test-name-repo",
},
}
got, err := repo.CreateAuthMethod(context.Background(), in)
require.NoError(err)
require.NotNil(got)
assertPublicId(t, AuthMethodPrefix, got.PublicId)
assert.NotSame(in, got)
assert.Equal(in.Name, got.Name)
assert.Equal(in.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
got2, err := repo.CreateAuthMethod(context.Background(), in)
assert.Truef(errors.Is(err, db.ErrNotUnique), "want err: %v got: %v", db.ErrNotUnique, err)
assert.Nil(got2)
})
t.Run("valid-duplicate-names-diff-scopes", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
repo, err := NewRepository(rw, rw, wrapper)
require.NoError(err)
require.NotNil(repo)
org1, _ := iam.TestScopes(t, conn)
in := &AuthMethod{
AuthMethod: &store.AuthMethod{
Name: "test-name-repo",
},
}
in2 := in.clone()
in.ScopeId = org1.GetPublicId()
got, err := repo.CreateAuthMethod(context.Background(), in)
require.NoError(err)
require.NotNil(got)
assertPublicId(t, AuthMethodPrefix, got.PublicId)
assert.NotSame(in, got)
assert.Equal(in.Name, got.Name)
assert.Equal(in.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
org2, _ := iam.TestScopes(t, conn)
in2.ScopeId = org2.GetPublicId()
got2, err := repo.CreateAuthMethod(context.Background(), in2)
require.NoError(err)
require.NotNil(got2)
assertPublicId(t, AuthMethodPrefix, got2.PublicId)
assert.NotSame(in2, got2)
assert.Equal(in2.Name, got2.Name)
assert.Equal(in2.Description, got2.Description)
assert.Equal(got2.CreateTime, got2.UpdateTime)
})
}
func assertPublicId(t *testing.T, prefix, actual string) {
t.Helper()
assert.NotEmpty(t, actual)
parts := strings.Split(actual, "_")
assert.Equalf(t, 2, len(parts), "want one '_' in PublicId, got multiple in %q", actual)
assert.Equalf(t, prefix, parts[0], "PublicId want prefix: %q, got: %q in %q", prefix, parts[0], actual)
}
+100
View File
@@ -0,0 +1,100 @@
package password
import (
"errors"
"testing"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/watchtower/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRepository_New(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
type args struct {
r db.Reader
w db.Writer
wrapper wrapping.Wrapper
}
var tests = []struct {
name string
args args
want *Repository
wantIsErr error
}{
{
name: "valid",
args: args{
r: rw,
w: rw,
wrapper: wrapper,
},
want: &Repository{
reader: rw,
writer: rw,
wrapper: wrapper,
},
},
{
name: "nil-reader",
args: args{
r: nil,
w: rw,
wrapper: wrapper,
},
want: nil,
wantIsErr: db.ErrNilParameter,
},
{
name: "nil-writer",
args: args{
r: rw,
w: nil,
wrapper: wrapper,
},
want: nil,
wantIsErr: db.ErrNilParameter,
},
{
name: "nil-wrapper",
args: args{
r: rw,
w: rw,
wrapper: nil,
},
want: nil,
wantIsErr: db.ErrNilParameter,
},
{
name: "all-nils",
args: args{
r: nil,
w: nil,
wrapper: nil,
},
want: nil,
wantIsErr: db.ErrNilParameter,
},
}
for _, tt := range tests {
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)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(got)
return
}
assert.NoError(err)
require.NotNil(got)
assert.Equal(tt.want, got)
})
}
}
+407
View File
@@ -0,0 +1,407 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.24.0
// protoc v3.12.3
// source: controller/storage/auth/password/store/v1/password.proto
// Package store provides protobufs for storing types in the password package.
package store
import (
proto "github.com/golang/protobuf/proto"
timestamp "github.com/hashicorp/watchtower/internal/db/timestamp"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type AuthMethod struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: `gorm:"primary_key"`
PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"`
// The create_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"`
// The update_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"`
// name is optional. If set, it must be unique within scope_id.
// @inject_tag: `gorm:"default:null"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"`
// description is optional.
// @inject_tag: `gorm:"default:null"`
Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"`
// The scope_id of the owning scope. Must be set.
// @inject_tag: `gorm:"not_null"`
ScopeId string `protobuf:"bytes,6,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"`
// @inject_tag: `gorm:"default:null"`
MinUserNameLength uint32 `protobuf:"varint,8,opt,name=min_user_name_length,json=minUserNameLength,proto3" json:"min_user_name_length,omitempty" gorm:"default:null"`
// @inject_tag: `gorm:"default:null"`
MinPasswordLength uint32 `protobuf:"varint,9,opt,name=min_password_length,json=minPasswordLength,proto3" json:"min_password_length,omitempty" gorm:"default:null"`
}
func (x *AuthMethod) Reset() {
*x = AuthMethod{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_storage_auth_password_store_v1_password_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AuthMethod) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthMethod) ProtoMessage() {}
func (x *AuthMethod) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_auth_password_store_v1_password_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AuthMethod.ProtoReflect.Descriptor instead.
func (*AuthMethod) Descriptor() ([]byte, []int) {
return file_controller_storage_auth_password_store_v1_password_proto_rawDescGZIP(), []int{0}
}
func (x *AuthMethod) GetPublicId() string {
if x != nil {
return x.PublicId
}
return ""
}
func (x *AuthMethod) GetCreateTime() *timestamp.Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *AuthMethod) GetUpdateTime() *timestamp.Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
func (x *AuthMethod) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *AuthMethod) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *AuthMethod) GetScopeId() string {
if x != nil {
return x.ScopeId
}
return ""
}
func (x *AuthMethod) GetMinUserNameLength() uint32 {
if x != nil {
return x.MinUserNameLength
}
return 0
}
func (x *AuthMethod) GetMinPasswordLength() uint32 {
if x != nil {
return x.MinPasswordLength
}
return 0
}
type Account struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: `gorm:"primary_key"`
PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"`
// The create_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"`
// The update_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"`
// name is optional. If set, it must be unique within scope_id.
// @inject_tag: `gorm:"default:null"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"`
// description is optional.
// @inject_tag: `gorm:"default:null"`
Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"`
// The scope_id of the owning scope. Must be set.
// @inject_tag: `gorm:"not_null"`
ScopeId string `protobuf:"bytes,6,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"`
// @inject_tag: `gorm:"not_null"`
AuthMethodId string `protobuf:"bytes,7,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty" gorm:"not_null"`
// @inject_tag: `gorm:"not_null"`
UserName string `protobuf:"bytes,8,opt,name=user_name,json=userName,proto3" json:"user_name,omitempty" gorm:"not_null"`
}
func (x *Account) Reset() {
*x = Account{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_storage_auth_password_store_v1_password_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Account) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Account) ProtoMessage() {}
func (x *Account) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_auth_password_store_v1_password_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Account.ProtoReflect.Descriptor instead.
func (*Account) Descriptor() ([]byte, []int) {
return file_controller_storage_auth_password_store_v1_password_proto_rawDescGZIP(), []int{1}
}
func (x *Account) GetPublicId() string {
if x != nil {
return x.PublicId
}
return ""
}
func (x *Account) GetCreateTime() *timestamp.Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *Account) GetUpdateTime() *timestamp.Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
func (x *Account) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Account) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *Account) GetScopeId() string {
if x != nil {
return x.ScopeId
}
return ""
}
func (x *Account) GetAuthMethodId() string {
if x != nil {
return x.AuthMethodId
}
return ""
}
func (x *Account) GetUserName() string {
if x != nil {
return x.UserName
}
return ""
}
var File_controller_storage_auth_password_store_v1_password_proto protoreflect.FileDescriptor
var file_controller_storage_auth_password_store_v1_password_proto_rawDesc = []byte{
0x0a, 0x38, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f,
0x72, 0x61, 0x67, 0x65, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x73, 0x73,
0x77, 0x6f, 0x72, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x29, 0x63, 0x6f, 0x6e, 0x74,
0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x61,
0x75, 0x74, 0x68, 0x2e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x2e, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf5, 0x02, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d,
0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12,
0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18,
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a,
0x14, 0x6d, 0x69, 0x6e, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6c,
0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6d, 0x69, 0x6e,
0x55, 0x73, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x2e,
0x0a, 0x13, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c,
0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6d, 0x69, 0x6e,
0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xd4,
0x02, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74,
0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63,
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74,
0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74,
0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63,
0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65,
0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65,
0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f,
0x64, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68,
0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65,
0x72, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x44, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61,
0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x2f,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
file_controller_storage_auth_password_store_v1_password_proto_rawDescOnce sync.Once
file_controller_storage_auth_password_store_v1_password_proto_rawDescData = file_controller_storage_auth_password_store_v1_password_proto_rawDesc
)
func file_controller_storage_auth_password_store_v1_password_proto_rawDescGZIP() []byte {
file_controller_storage_auth_password_store_v1_password_proto_rawDescOnce.Do(func() {
file_controller_storage_auth_password_store_v1_password_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_auth_password_store_v1_password_proto_rawDescData)
})
return file_controller_storage_auth_password_store_v1_password_proto_rawDescData
}
var file_controller_storage_auth_password_store_v1_password_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_controller_storage_auth_password_store_v1_password_proto_goTypes = []interface{}{
(*AuthMethod)(nil), // 0: controller.storage.auth.password.store.v1.AuthMethod
(*Account)(nil), // 1: controller.storage.auth.password.store.v1.Account
(*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp
}
var file_controller_storage_auth_password_store_v1_password_proto_depIdxs = []int32{
2, // 0: controller.storage.auth.password.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
2, // 1: controller.storage.auth.password.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
2, // 2: controller.storage.auth.password.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
2, // 3: controller.storage.auth.password.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_controller_storage_auth_password_store_v1_password_proto_init() }
func file_controller_storage_auth_password_store_v1_password_proto_init() {
if File_controller_storage_auth_password_store_v1_password_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_controller_storage_auth_password_store_v1_password_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AuthMethod); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_controller_storage_auth_password_store_v1_password_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Account); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_storage_auth_password_store_v1_password_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_controller_storage_auth_password_store_v1_password_proto_goTypes,
DependencyIndexes: file_controller_storage_auth_password_store_v1_password_proto_depIdxs,
MessageInfos: file_controller_storage_auth_password_store_v1_password_proto_msgTypes,
}.Build()
File_controller_storage_auth_password_store_v1_password_proto = out.File
file_controller_storage_auth_password_store_v1_password_proto_rawDesc = nil
file_controller_storage_auth_password_store_v1_password_proto_goTypes = nil
file_controller_storage_auth_password_store_v1_password_proto_depIdxs = nil
}
+303 -10
View File
@@ -1227,12 +1227,51 @@ commit;
bytes: []byte(`
begin;
-- Design influenced by:
-- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
--
-- iam_scope ←───── auth_method
-- ↑ ↑
-- iam_user ←───── auth_account
/*
┌────────────────┐ ┌────────────────┐
│ iam_scope │ │ auth_method │
├────────────────┤ ├────────────────┤
│ public_id (pk) │ ╱│ public_id (pk) │
│ │┼┼───────────○─│ scope_id (fk) │
│ │ ╲│ │
└────────────────┘ └────────────────┘
┼ ┼
┼ ┼
│ │
│ │ ▲fk1
│ │
○ ○
╱│╲ ╱│╲
┌────────────────┐ ┌──────────────────────────┐
│ iam_user │ │ auth_account │
├────────────────┤ ├──────────────────────────┤
│ public_id (pk) │ │ public_id (pk) │
│ scope_id (fk) │ ◀fk2 │ scope_id (fk1,fk2) │
│ │┼○──────○┼│ auth_method_id (fk1) │
│ │ │ iam_user_id (fk2) │
└────────────────┘ └──────────────────────────┘
An iam_scope can have 0 to many iam_users.
An iam_scope can have 0 to many auth_methods.
An iam_user belongs to 1 iam_scope.
An auth_method belongs to 1 iam_scope.
An iam_user can have 0 or 1 auth_account.
An auth_account belongs to 0 or 1 iam_user.
An auth_method can have 0 to many auth_accounts.
An auth_account belongs to 1 auth_account.
An auth_account can only be associated with an iam_user in the same scope of
the auth_account's auth_method. Including scope_id in fk1 and fk2 ensures this
restriction is not violated.
Design influenced by:
https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
*/
-- base table for auth methods
create table auth_method (
@@ -1255,18 +1294,19 @@ begin;
auth_method_id wt_public_id not null,
scope_id wt_scope_id not null,
iam_user_id wt_public_id,
foreign key (scope_id, auth_method_id)
-- including scope_id in fk1 and fk2 ensures the scope_id of the owning
-- auth_method and the scope_id of the owning iam_user are the same
foreign key (scope_id, auth_method_id) -- fk1
references auth_method (scope_id, public_id)
on delete cascade
on update cascade,
foreign key (scope_id, iam_user_id)
foreign key (scope_id, iam_user_id) -- fk2
references iam_user (scope_id, public_id)
on delete set null
on update cascade,
unique(scope_id, auth_method_id, public_id)
);
create or replace function
insert_auth_method_subtype()
returns trigger
@@ -1285,15 +1325,22 @@ begin;
returns trigger
as $$
begin
select auth_method.scope_id
into new.scope_id
from auth_method
where auth_method.public_id = new.auth_method_id;
insert into auth_account
(public_id, auth_method_id, scope_id)
values
(new.public_id, new.auth_method_id, new.scope_id);
return new;
end;
$$ language plpgsql;
commit;
`),
@@ -1584,6 +1631,252 @@ begin;
commit;
`),
},
"migrations/12_auth_password.down.sql": {
name: "12_auth_password.down.sql",
bytes: []byte(`
begin;
drop table auth_password_credential;
drop table auth_password_conf cascade;
drop table if exists auth_password_account;
drop table if exists auth_password_method;
drop function insert_auth_password_credential_subtype;
drop function insert_auth_password_conf_subtype;
commit;
`),
},
"migrations/12_auth_password.up.sql": {
name: "12_auth_password.up.sql",
bytes: []byte(`
begin;
/*
┌────────────────┐ ┌──────────────────────┐ ┌────────────────────────────┐
│ auth_method │ │ auth_password_method │ │ auth_password_conf │
├────────────────┤ ├──────────────────────┤ ├────────────────────────────┤
│ public_id (pk) │ │ public_id (pk,fk) │ ╱│ public_id (pk,fk) │
│ scope_id (fk) │┼┼─────────────○┼│ scope_id (fk) │┼┼─────────○─│ password_method_id (fk) │
│ │ │ ... │ ╲│ │
└────────────────┘ └──────────────────────┘ └────────────────────────────┘
┼ ┼ ┼
┼ ┼ ┼
│ │ │
│ ▲fk1 │ ▲fk1 │ ▲fk1
│ │ │
○ ○ ○
╱│╲ ╱│╲ ╱│╲
┌──────────────────────────┐ ┌──────────────────────────┐ ┌───────────────────────────────┐
│ auth_account │ │ auth_password_account │ │ auth_password_credential │
├──────────────────────────┤ ├──────────────────────────┤ ├───────────────────────────────┤
│ public_id (pk) │ │ public_id (pk,fk2) │ │ public_id (pk) │
│ scope_id (fk1,fk2) │ ◀fk2 │ scope_id (fk1,fk2) │ ◀fk2 │ password_method_id (fk1,fk2) │
│ auth_method_id (fk1) │┼┼──────○┼│ auth_method_id (fk1,fk2) │┼┼──────○┼│ password_conf_id (fk1) │
│ iam_user_id (fk2) │ │ ... │ │ password_account_id (fk2) │
└──────────────────────────┘ └──────────────────────────┘ └───────────────────────────────┘
An auth_method is "abstract". An auth_password_method is a concrete
auth_method. For every row in auth_password_method there is one row in
auth_method with the same public_id and scope_id.
Similarly, an auth_account is "abstract". An auth_password_account is a concrete
auth_account. For every row in auth_password_account there is one row in
auth_account with the same public_id, scope_id, and auth_method_id.
Both auth_password_conf and auth_password_credential are abstract. Each
password key derivation function will require a concrete auth_password_conf
and auth_password_credential table.
An auth_method can have 0 or 1 auth_password_method.
An auth_account can have 0 or 1 auth_password_account.
An auth_password_method belongs to 1 auth_method.
An auth_password_method can have 0 to many auth_password_accounts.
An auth_password_method can have 0 to many auth_password_confs.
An auth_password_account belongs to 1 auth_account.
An auth_password_account belongs to 1 auth_password_method.
An auth_password_account can have 0 or 1 auth_password_credential.
An auth_password_conf belongs to 1 auth_password_method.
An auth_password_conf can have 0 to many auth_password_credentials.
An auth_password_credential belongs to 1 auth_password_account.
An auth_password_credential belongs to 1 auth_password_conf.
*/
create table auth_password_method (
public_id wt_public_id primary key,
scope_id wt_scope_id not null,
name text,
description text,
create_time wt_timestamp,
update_time wt_timestamp,
min_user_name_length int not null default 5,
min_password_length int not null default 8,
foreign key (scope_id, public_id)
references auth_method (scope_id, public_id)
on delete cascade
on update cascade,
unique(scope_id, name),
unique(scope_id, public_id)
);
create trigger
insert_auth_method_subtype
before insert on auth_password_method
for each row execute procedure insert_auth_method_subtype();
create table auth_password_account (
public_id wt_public_id primary key,
auth_method_id wt_public_id not null,
-- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain
-- check is executed before the insert trigger which retrieves the scope_id
-- causing an insert to fail.
scope_id text not null,
name text,
description text,
create_time wt_timestamp,
update_time wt_timestamp,
user_name text not null
check(
lower(trim(user_name)) = user_name
and
length(user_name) > 0
),
foreign key (scope_id, auth_method_id)
references auth_password_method (scope_id, public_id)
on delete cascade
on update cascade,
foreign key (scope_id, auth_method_id, public_id)
references auth_account (scope_id, auth_method_id, public_id)
on delete cascade
on update cascade,
unique(auth_method_id, name),
unique(auth_method_id, user_name),
unique(auth_method_id, public_id)
);
create trigger
insert_auth_account_subtype
before insert on auth_password_account
for each row execute procedure insert_auth_account_subtype();
create table auth_password_conf (
public_id wt_public_id primary key,
password_method_id wt_public_id not null
references auth_password_method (public_id)
on delete cascade
on update cascade
deferrable initially deferred,
unique(password_method_id, public_id)
);
-- insert_auth_password_conf_subtype() is a trigger function for concrete
-- implementations of auth_password_conf
create or replace function
insert_auth_password_conf_subtype()
returns trigger
as $$
begin
insert into auth_password_conf
(public_id, password_method_id)
values
(new.public_id, new.password_method_id);
return new;
end;
$$ language plpgsql;
create table auth_password_credential (
public_id wt_public_id primary key,
password_account_id wt_public_id not null unique,
password_conf_id wt_public_id not null,
password_method_id wt_public_id not null,
foreign key (password_method_id, password_conf_id)
references auth_password_conf (password_method_id, public_id)
on delete cascade
on update cascade,
foreign key (password_method_id, password_account_id)
references auth_password_account (auth_method_id, public_id)
on delete cascade
on update cascade,
unique(password_method_id, password_conf_id, password_account_id)
);
-- insert_auth_password_credential_subtype() is a trigger function for concrete
-- implementations of auth_password_credential
create or replace function
insert_auth_password_credential_subtype()
returns trigger
as $$
begin
select auth_password_account.auth_method_id
into new.password_method_id
from auth_password_account
where auth_password_account.public_id = new.password_account_id;
insert into auth_password_credential
(public_id, password_account_id, password_conf_id, password_method_id)
values
(new.public_id, new.password_account_id, new.password_conf_id, new.password_method_id);
return new;
end;
$$ language plpgsql;
--
-- triggers for time columns
---
create trigger
update_time_column
before
update on auth_password_method
for each row execute procedure update_time_column();
create trigger
immutable_create_time
before
update on auth_password_method
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on auth_password_method
for each row execute procedure default_create_time();
create trigger
update_time_column
before
update on auth_password_account
for each row execute procedure update_time_column();
create trigger
immutable_create_time
before
update on auth_password_account
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on auth_password_account
for each row execute procedure default_create_time();
insert into oplog_ticket (name, version)
values
('auth_password_method', 1),
('auth_password_account', 1);
commit;
`),
},
}
+57 -10
View File
@@ -1,11 +1,50 @@
begin;
-- Design influenced by:
-- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
--
-- iam_scope ←───── auth_method
-- ↑ ↑
-- iam_user ←───── auth_account
/*
┌────────────────┐ ┌────────────────┐
│ iam_scope │ │ auth_method │
├────────────────┤ ├────────────────┤
│ public_id (pk) │ ╱│ public_id (pk) │
│ │┼┼───────────○─│ scope_id (fk) │
│ │ ╲│ │
└────────────────┘ └────────────────┘
┼ ┼
┼ ┼
│ │
│ │ ▲fk1
│ │
○ ○
╱│╲ ╱│╲
┌────────────────┐ ┌──────────────────────────┐
│ iam_user │ │ auth_account │
├────────────────┤ ├──────────────────────────┤
│ public_id (pk) │ │ public_id (pk) │
│ scope_id (fk) │ ◀fk2 │ scope_id (fk1,fk2) │
│ │┼○──────○┼│ auth_method_id (fk1) │
│ │ │ iam_user_id (fk2) │
└────────────────┘ └──────────────────────────┘
An iam_scope can have 0 to many iam_users.
An iam_scope can have 0 to many auth_methods.
An iam_user belongs to 1 iam_scope.
An auth_method belongs to 1 iam_scope.
An iam_user can have 0 or 1 auth_account.
An auth_account belongs to 0 or 1 iam_user.
An auth_method can have 0 to many auth_accounts.
An auth_account belongs to 1 auth_account.
An auth_account can only be associated with an iam_user in the same scope of
the auth_account's auth_method. Including scope_id in fk1 and fk2 ensures this
restriction is not violated.
Design influenced by:
https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
*/
-- base table for auth methods
create table auth_method (
@@ -28,18 +67,19 @@ begin;
auth_method_id wt_public_id not null,
scope_id wt_scope_id not null,
iam_user_id wt_public_id,
foreign key (scope_id, auth_method_id)
-- including scope_id in fk1 and fk2 ensures the scope_id of the owning
-- auth_method and the scope_id of the owning iam_user are the same
foreign key (scope_id, auth_method_id) -- fk1
references auth_method (scope_id, public_id)
on delete cascade
on update cascade,
foreign key (scope_id, iam_user_id)
foreign key (scope_id, iam_user_id) -- fk2
references iam_user (scope_id, public_id)
on delete set null
on update cascade,
unique(scope_id, auth_method_id, public_id)
);
create or replace function
insert_auth_method_subtype()
returns trigger
@@ -58,13 +98,20 @@ begin;
returns trigger
as $$
begin
select auth_method.scope_id
into new.scope_id
from auth_method
where auth_method.public_id = new.auth_method_id;
insert into auth_account
(public_id, auth_method_id, scope_id)
values
(new.public_id, new.auth_method_id, new.scope_id);
return new;
end;
$$ language plpgsql;
commit;
@@ -0,0 +1,11 @@
begin;
drop table auth_password_credential;
drop table auth_password_conf cascade;
drop table if exists auth_password_account;
drop table if exists auth_password_method;
drop function insert_auth_password_credential_subtype;
drop function insert_auth_password_conf_subtype;
commit;
@@ -0,0 +1,223 @@
begin;
/*
┌────────────────┐ ┌──────────────────────┐ ┌────────────────────────────┐
│ auth_method │ │ auth_password_method │ │ auth_password_conf │
├────────────────┤ ├──────────────────────┤ ├────────────────────────────┤
│ public_id (pk) │ │ public_id (pk,fk) │ ╱│ public_id (pk,fk) │
│ scope_id (fk) │┼┼─────────────○┼│ scope_id (fk) │┼┼─────────○─│ password_method_id (fk) │
│ │ │ ... │ ╲│ │
└────────────────┘ └──────────────────────┘ └────────────────────────────┘
┼ ┼ ┼
┼ ┼ ┼
│ │ │
│ ▲fk1 │ ▲fk1 │ ▲fk1
│ │ │
○ ○ ○
╱│╲ ╱│╲ ╱│╲
┌──────────────────────────┐ ┌──────────────────────────┐ ┌───────────────────────────────┐
│ auth_account │ │ auth_password_account │ │ auth_password_credential │
├──────────────────────────┤ ├──────────────────────────┤ ├───────────────────────────────┤
│ public_id (pk) │ │ public_id (pk,fk2) │ │ public_id (pk) │
│ scope_id (fk1,fk2) │ ◀fk2 │ scope_id (fk1,fk2) │ ◀fk2 │ password_method_id (fk1,fk2) │
│ auth_method_id (fk1) │┼┼──────○┼│ auth_method_id (fk1,fk2) │┼┼──────○┼│ password_conf_id (fk1) │
│ iam_user_id (fk2) │ │ ... │ │ password_account_id (fk2) │
└──────────────────────────┘ └──────────────────────────┘ └───────────────────────────────┘
An auth_method is "abstract". An auth_password_method is a concrete
auth_method. For every row in auth_password_method there is one row in
auth_method with the same public_id and scope_id.
Similarly, an auth_account is "abstract". An auth_password_account is a concrete
auth_account. For every row in auth_password_account there is one row in
auth_account with the same public_id, scope_id, and auth_method_id.
Both auth_password_conf and auth_password_credential are abstract. Each
password key derivation function will require a concrete auth_password_conf
and auth_password_credential table.
An auth_method can have 0 or 1 auth_password_method.
An auth_account can have 0 or 1 auth_password_account.
An auth_password_method belongs to 1 auth_method.
An auth_password_method can have 0 to many auth_password_accounts.
An auth_password_method can have 0 to many auth_password_confs.
An auth_password_account belongs to 1 auth_account.
An auth_password_account belongs to 1 auth_password_method.
An auth_password_account can have 0 or 1 auth_password_credential.
An auth_password_conf belongs to 1 auth_password_method.
An auth_password_conf can have 0 to many auth_password_credentials.
An auth_password_credential belongs to 1 auth_password_account.
An auth_password_credential belongs to 1 auth_password_conf.
*/
create table auth_password_method (
public_id wt_public_id primary key,
scope_id wt_scope_id not null,
name text,
description text,
create_time wt_timestamp,
update_time wt_timestamp,
min_user_name_length int not null default 5,
min_password_length int not null default 8,
foreign key (scope_id, public_id)
references auth_method (scope_id, public_id)
on delete cascade
on update cascade,
unique(scope_id, name),
unique(scope_id, public_id)
);
create trigger
insert_auth_method_subtype
before insert on auth_password_method
for each row execute procedure insert_auth_method_subtype();
create table auth_password_account (
public_id wt_public_id primary key,
auth_method_id wt_public_id not null,
-- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain
-- check is executed before the insert trigger which retrieves the scope_id
-- causing an insert to fail.
scope_id text not null,
name text,
description text,
create_time wt_timestamp,
update_time wt_timestamp,
user_name text not null
check(
lower(trim(user_name)) = user_name
and
length(user_name) > 0
),
foreign key (scope_id, auth_method_id)
references auth_password_method (scope_id, public_id)
on delete cascade
on update cascade,
foreign key (scope_id, auth_method_id, public_id)
references auth_account (scope_id, auth_method_id, public_id)
on delete cascade
on update cascade,
unique(auth_method_id, name),
unique(auth_method_id, user_name),
unique(auth_method_id, public_id)
);
create trigger
insert_auth_account_subtype
before insert on auth_password_account
for each row execute procedure insert_auth_account_subtype();
create table auth_password_conf (
public_id wt_public_id primary key,
password_method_id wt_public_id not null
references auth_password_method (public_id)
on delete cascade
on update cascade
deferrable initially deferred,
unique(password_method_id, public_id)
);
-- insert_auth_password_conf_subtype() is a trigger function for concrete
-- implementations of auth_password_conf
create or replace function
insert_auth_password_conf_subtype()
returns trigger
as $$
begin
insert into auth_password_conf
(public_id, password_method_id)
values
(new.public_id, new.password_method_id);
return new;
end;
$$ language plpgsql;
create table auth_password_credential (
public_id wt_public_id primary key,
password_account_id wt_public_id not null unique,
password_conf_id wt_public_id not null,
password_method_id wt_public_id not null,
foreign key (password_method_id, password_conf_id)
references auth_password_conf (password_method_id, public_id)
on delete cascade
on update cascade,
foreign key (password_method_id, password_account_id)
references auth_password_account (auth_method_id, public_id)
on delete cascade
on update cascade,
unique(password_method_id, password_conf_id, password_account_id)
);
-- insert_auth_password_credential_subtype() is a trigger function for concrete
-- implementations of auth_password_credential
create or replace function
insert_auth_password_credential_subtype()
returns trigger
as $$
begin
select auth_password_account.auth_method_id
into new.password_method_id
from auth_password_account
where auth_password_account.public_id = new.password_account_id;
insert into auth_password_credential
(public_id, password_account_id, password_conf_id, password_method_id)
values
(new.public_id, new.password_account_id, new.password_conf_id, new.password_method_id);
return new;
end;
$$ language plpgsql;
--
-- triggers for time columns
---
create trigger
update_time_column
before
update on auth_password_method
for each row execute procedure update_time_column();
create trigger
immutable_create_time
before
update on auth_password_method
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on auth_password_method
for each row execute procedure default_create_time();
create trigger
update_time_column
before
update on auth_password_account
for each row execute procedure update_time_column();
create trigger
immutable_create_time
before
update on auth_password_account
for each row execute procedure immutable_create_time_func();
create trigger
default_create_time_column
before
insert on auth_password_account
for each row execute procedure default_create_time();
insert into oplog_ticket (name, version)
values
('auth_password_method', 1),
('auth_password_account', 1);
commit;
@@ -0,0 +1,69 @@
syntax = "proto3";
// Package store provides protobufs for storing types in the password package.
package controller.storage.auth.password.store.v1;
option go_package = "github.com/hashicorp/watchtower/internal/auth/password/store;store";
import "controller/storage/timestamp/v1/timestamp.proto";
message AuthMethod {
// @inject_tag: `gorm:"primary_key"`
string public_id = 1;
// The create_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp create_time = 2;
// The update_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp update_time = 3;
// name is optional. If set, it must be unique within scope_id.
// @inject_tag: `gorm:"default:null"`
string name = 4;
// description is optional.
// @inject_tag: `gorm:"default:null"`
string description = 5;
// The scope_id of the owning scope. Must be set.
// @inject_tag: `gorm:"not_null"`
string scope_id = 6;
// @inject_tag: `gorm:"default:null"`
uint32 min_user_name_length = 8;
// @inject_tag: `gorm:"default:null"`
uint32 min_password_length = 9;
}
message Account {
// @inject_tag: `gorm:"primary_key"`
string public_id = 1;
// The create_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp create_time = 2;
// The update_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp update_time = 3;
// name is optional. If set, it must be unique within scope_id.
// @inject_tag: `gorm:"default:null"`
string name = 4;
// description is optional.
// @inject_tag: `gorm:"default:null"`
string description = 5;
// The scope_id of the owning scope. Must be set.
// @inject_tag: `gorm:"not_null"`
string scope_id = 6;
// @inject_tag: `gorm:"not_null"`
string auth_method_id = 7;
// @inject_tag: `gorm:"not_null"`
string user_name = 8;
}