diff --git a/Makefile b/Makefile index dbef8774f2..dd08121efc 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,7 @@ protobuild: @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 + @protoc-go-inject-tag -input=./internal/auth/password/store/argon2.pb.go @rm -R ${TMP_DIR} protolint: diff --git a/internal/auth/password/argon2.go b/internal/auth/password/argon2.go new file mode 100644 index 0000000000..79ad57c2a8 --- /dev/null +++ b/internal/auth/password/argon2.go @@ -0,0 +1,102 @@ +package password + +import ( + "strings" + + "github.com/hashicorp/watchtower/internal/auth/password/store" + "github.com/hashicorp/watchtower/internal/oplog" + "google.golang.org/protobuf/proto" +) + +// Argon2Configuration is a configuration for using the argon2id key +// derivation function. It is owned by an AuthMethod. +// +// Iterations, Memory, and Threads are the cost parameters. The cost +// parameters should be increased as memory latency and CPU parallelism +// increases. +// +// For a detailed specification of Argon2 see: +// https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf +type Argon2Configuration struct { + *store.Argon2Configuration + tableName string +} + +// NewArgon2Configuration creates a new in memory Argon2Configuration with +// reasonable default settings. +func NewArgon2Configuration() *Argon2Configuration { + return &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + Iterations: 3, + Memory: 64 * 1024, + Threads: 1, + SaltLength: 32, + KeyLength: 32, + }, + } +} + +func (c *Argon2Configuration) validate() error { + switch { + case c == nil, c.Argon2Configuration == nil: + return ErrInvalidConfiguration + case c.Iterations == 0, c.Memory == 0, c.Threads == 0, c.SaltLength == 0, c.KeyLength == 0: + return ErrInvalidConfiguration + default: + return nil + } +} + +// AuthMethodId returns the Id of the AuthMethod which owns c. +func (c *Argon2Configuration) AuthMethodId() string { + if c != nil && c.Argon2Configuration != nil { + return c.PasswordMethodId + } + return "" +} + +func (c *Argon2Configuration) clone() *Argon2Configuration { + cp := proto.Clone(c.Argon2Configuration) + return &Argon2Configuration{ + Argon2Configuration: cp.(*store.Argon2Configuration), + } +} + +// TableName returns the table name. +func (c *Argon2Configuration) TableName() string { + if c != nil && c.tableName != "" { + return c.tableName + } + return "auth_password_argon2_conf" +} + +// SetTableName sets the table name. +func (c *Argon2Configuration) SetTableName(n string) { + c.tableName = n +} + +func (c *Argon2Configuration) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{c.PrivateId}, + "resource-type": []string{"password argon2 conf"}, + "op-type": []string{op.String()}, + } + if c.PasswordMethodId != "" { + metadata["password-method-id"] = []string{c.PasswordMethodId} + } + return metadata +} + +func (c *Argon2Configuration) whereDup() (string, []interface{}) { + var where []string + var args []interface{} + + where, args = append(where, "password_method_id = ?"), append(args, c.PasswordMethodId) + where, args = append(where, "iterations = ?"), append(args, c.Iterations) + where, args = append(where, "memory = ?"), append(args, c.Memory) + where, args = append(where, "threads = ?"), append(args, c.Threads) + where, args = append(where, "key_length = ?"), append(args, c.KeyLength) + where, args = append(where, "salt_length = ?"), append(args, c.SaltLength) + + return strings.Join(where, " and "), args +} diff --git a/internal/auth/password/argon2_test.go b/internal/auth/password/argon2_test.go new file mode 100644 index 0000000000..2b7589711f --- /dev/null +++ b/internal/auth/password/argon2_test.go @@ -0,0 +1,270 @@ +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 TestArgon2Configuration_New(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + + authMethods := testAuthMethods(t, conn, 1) + authMethod := authMethods[0] + authMethodId := authMethod.GetPublicId() + ctx := context.Background() + + // There should already be a configuration when an authMethod is created. + t.Run("default-configuration", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var confs []*Argon2Configuration + err := rw.SearchWhere(ctx, &confs, "password_method_id = ?", []interface{}{authMethodId}) + require.NoError(err) + require.Equal(1, len(confs)) + got := confs[0] + want := &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + PrivateId: got.GetPrivateId(), + CreateTime: got.GetCreateTime(), + PasswordMethodId: authMethodId, + Iterations: 3, + Memory: 64 * 1024, + Threads: 1, + SaltLength: 32, + KeyLength: 32, + }, + } + assert.Equal(want, got) + }) + t.Run("no-duplicate-configurations", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got := NewArgon2Configuration() + require.NotNil(got) + var err error + got.PrivateId, err = newArgon2ConfigurationId() + require.NoError(err) + got.PasswordMethodId = authMethodId + err = rw.Create(ctx, got) + assert.Error(err) + }) + t.Run("multiple-configurations", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + var confs []*Argon2Configuration + err := rw.SearchWhere(ctx, &confs, "password_method_id = ?", []interface{}{authMethodId}) + require.NoError(err) + assert.Equal(1, len(confs)) + + c1 := NewArgon2Configuration() + require.NotNil(c1) + c1.PrivateId, err = newArgon2ConfigurationId() + require.NoError(err) + c1.PasswordMethodId = authMethodId + c1.Iterations = c1.Iterations + 1 + c1.Threads = c1.Threads + 1 + err = rw.Create(ctx, c1) + assert.NoError(err) + + c2 := NewArgon2Configuration() + require.NotNil(c2) + c2.PrivateId, err = newArgon2ConfigurationId() + require.NoError(err) + c2.PasswordMethodId = authMethodId + c2.Memory = 32 * 1024 + c2.SaltLength = 16 + c2.KeyLength = 16 + err = rw.Create(ctx, c2) + assert.NoError(err) + + confs = nil + err = rw.SearchWhere(ctx, &confs, "password_method_id = ?", []interface{}{authMethodId}) + require.NoError(err) + assert.Equal(3, len(confs)) + }) +} + +func TestArgon2Configuration_Readonly(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + + rw := db.New(conn) + + changeIterations := func() func(*Argon2Configuration) (*Argon2Configuration, []string) { + return func(c *Argon2Configuration) (*Argon2Configuration, []string) { + c.Iterations = c.Iterations + 1 + return c, []string{"Iterations"} + } + } + changeThreads := func() func(*Argon2Configuration) (*Argon2Configuration, []string) { + return func(c *Argon2Configuration) (*Argon2Configuration, []string) { + c.Threads = c.Threads + 1 + return c, []string{"Threads"} + } + } + changeMemory := func() func(*Argon2Configuration) (*Argon2Configuration, []string) { + return func(c *Argon2Configuration) (*Argon2Configuration, []string) { + c.Memory = c.Memory + 1 + return c, []string{"Memory"} + } + } + + authMethods := testAuthMethods(t, conn, 1) + authMethod := authMethods[0] + authMethodId := authMethod.GetPublicId() + + var tests = []struct { + name string + chgFn func(*Argon2Configuration) (*Argon2Configuration, []string) + }{ + { + name: "iterations", + chgFn: changeIterations(), + }, + { + name: "threads", + chgFn: changeThreads(), + }, + { + name: "Memory", + chgFn: changeMemory(), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var confs []*Argon2Configuration + err := rw.SearchWhere(context.Background(), &confs, "password_method_id = ?", []interface{}{authMethodId}) + require.NoError(err) + assert.Greater(len(confs), 0) + orig := confs[0] + changed, masks := tt.chgFn(orig) + + require.NotEmpty(changed.GetPrivateId()) + + count, err := rw.Update(context.Background(), changed, masks, nil) + assert.Error(err) + assert.Equal(0, count) + }) + } + +} + +func TestArgon2Configuration_Validate(t *testing.T) { + var tests = []struct { + name string + in *Argon2Configuration + want error + }{ + { + name: "nil-configuration", + in: nil, + want: ErrInvalidConfiguration, + }, + { + name: "nil-embedded-config", + in: &Argon2Configuration{}, + want: ErrInvalidConfiguration, + }, + { + name: "valid-default", + in: NewArgon2Configuration(), + }, + { + name: "valid-changes", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + Iterations: 3 * 2, + Memory: 32 * 1024, + Threads: 10, + SaltLength: 16, + KeyLength: 16, + }, + }, + }, + { + name: "invalid-iterations", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + Iterations: 0, + Memory: 1, + Threads: 1, + SaltLength: 1, + KeyLength: 1, + }, + }, + want: ErrInvalidConfiguration, + }, + { + name: "invalid-memory", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + Iterations: 1, + Memory: 0, + Threads: 1, + SaltLength: 1, + KeyLength: 1, + }, + }, + want: ErrInvalidConfiguration, + }, + { + name: "invalid-threads", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + Iterations: 1, + Memory: 1, + Threads: 0, + SaltLength: 1, + KeyLength: 1, + }, + }, + want: ErrInvalidConfiguration, + }, + { + name: "invalid-salt-length", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + Iterations: 1, + Memory: 1, + Threads: 1, + SaltLength: 0, + KeyLength: 1, + }, + }, + want: ErrInvalidConfiguration, + }, + { + name: "invalid-key-length", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + Iterations: 1, + Memory: 1, + Threads: 1, + SaltLength: 1, + KeyLength: 0, + }, + }, + want: ErrInvalidConfiguration, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got := tt.in.validate() + if tt.want == nil { + assert.NoErrorf(got, "valid argon2 configuration: %+v", tt.in) + return + } + require.Error(got) + assert.Truef(errors.Is(got, tt.want), "want err: %q got: %q", tt.want, got) + }) + } +} diff --git a/internal/auth/password/authmethod_test.go b/internal/auth/password/authmethod_test.go index f47c879da9..477b8c3f4f 100644 --- a/internal/auth/password/authmethod_test.go +++ b/internal/auth/password/authmethod_test.go @@ -27,9 +27,17 @@ func testAuthMethods(t *testing.T, conn *gorm.DB, count int) []*AuthMethod { require.NotEmpty(id) cat.PublicId = id + conf := NewArgon2Configuration() + require.NotNil(conf) + conf.PrivateId, err = newArgon2ConfigurationId() + require.NoError(err) + conf.PasswordMethodId = cat.PublicId + cat.PasswordConfId = conf.PrivateId + ctx := context.Background() _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, iw db.Writer) error { + require.NoError(iw.Create(ctx, conf)) return iw.Create(ctx, cat) }, ) @@ -123,14 +131,20 @@ func TestAuthMethod_New(t *testing.T) { tt.want.PublicId = id got.PublicId = id - conn.LogMode(true) + conf := NewArgon2Configuration() + require.NotNil(conf) + conf.PrivateId, err = newArgon2ConfigurationId() + require.NoError(err) + conf.PasswordMethodId = got.PublicId + got.PasswordConfId = conf.PrivateId + ctx := context.Background() _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, iw db.Writer) error { + require.NoError(iw.Create(ctx, conf)) return iw.Create(ctx, got) }, ) - conn.LogMode(false) assert.NoError(err2) }) } diff --git a/internal/auth/password/errors.go b/internal/auth/password/errors.go new file mode 100644 index 0000000000..67494c40f9 --- /dev/null +++ b/internal/auth/password/errors.go @@ -0,0 +1,13 @@ +package password + +import "errors" + +var ( + // ErrUnsupportedConfiguration results from attempting to perform an + // operation that sets a password configuration to an unsupported type. + ErrUnsupportedConfiguration = errors.New("unsupported configuration") + + // ErrInvalidConfiguration results from attempting to perform an + // operation that sets a password configuration with invalid settings. + ErrInvalidConfiguration = errors.New("invalid configuration") +) diff --git a/internal/auth/password/options.go b/internal/auth/password/options.go index 412bd8786b..0ebb777dd5 100644 --- a/internal/auth/password/options.go +++ b/internal/auth/password/options.go @@ -17,10 +17,13 @@ type options struct { withName string withDescription string withLimit int + withConfig Configuration } func getDefaultOptions() options { - return options{} + return options{ + withConfig: NewArgon2Configuration(), + } } // WithDescription provides an optional description. @@ -45,3 +48,10 @@ func WithLimit(l int) Option { o.withLimit = l } } + +// WithConfiguration provides an optional configuration. +func WithConfiguration(config Configuration) Option { + return func(o *options) { + o.withConfig = config + } +} diff --git a/internal/auth/password/options_test.go b/internal/auth/password/options_test.go index ce0f191fa3..4d9ccabc2e 100644 --- a/internal/auth/password/options_test.go +++ b/internal/auth/password/options_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_GetOpts(t *testing.T) { @@ -26,4 +27,15 @@ func Test_GetOpts(t *testing.T) { testOpts.withLimit = 5 assert.Equal(t, opts, testOpts) }) + t.Run("WithConfiguration", func(t *testing.T) { + conf := NewArgon2Configuration() + conf.KeyLength = conf.KeyLength * 2 + opts := getOpts(WithConfiguration(conf)) + testOpts := getDefaultOptions() + c, ok := testOpts.withConfig.(*Argon2Configuration) + require.True(t, ok, "need an Argon2Configuration") + c.KeyLength = c.KeyLength * 2 + testOpts.withConfig = c + assert.Equal(t, opts, testOpts) + }) } diff --git a/internal/auth/password/private_ids.go b/internal/auth/password/private_ids.go new file mode 100644 index 0000000000..9696cda38b --- /dev/null +++ b/internal/auth/password/private_ids.go @@ -0,0 +1,20 @@ +package password + +import ( + "fmt" + + "github.com/hashicorp/watchtower/internal/db" +) + +// Prefixes for private ids for types in the password package. +const ( + argon2ConfigurationPrefix = "arg2conf" +) + +func newArgon2ConfigurationId() (string, error) { + id, err := db.NewPrivateId(argon2ConfigurationPrefix) + if err != nil { + return "", fmt.Errorf("new password argon2 configuration id: %w", err) + } + return id, err +} diff --git a/internal/auth/password/private_ids_test.go b/internal/auth/password/private_ids_test.go new file mode 100644 index 0000000000..24d6472df9 --- /dev/null +++ b/internal/auth/password/private_ids_test.go @@ -0,0 +1,17 @@ +package password + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_PrivateIds(t *testing.T) { + t.Run("argon2Config", func(t *testing.T) { + id, err := newArgon2ConfigurationId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, argon2ConfigurationPrefix+"_")) + }) +} diff --git a/internal/auth/password/repository_authmethod.go b/internal/auth/password/repository_authmethod.go index 104280eb5f..40c1d34063 100644 --- a/internal/auth/password/repository_authmethod.go +++ b/internal/auth/password/repository_authmethod.go @@ -13,6 +13,9 @@ import ( // contain a valid ScopeId. m must not contain a PublicId. The PublicId is // generated and assigned by this method. // +// WithConfiguration is the only valid option. All other options are +// ignored. +// // 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) { @@ -36,9 +39,29 @@ func (r *Repository) CreateAuthMethod(ctx context.Context, m *AuthMethod, opt .. } m.PublicId = id + opts := getOpts(opt...) + c, ok := opts.withConfig.(*Argon2Configuration) + if !ok { + return nil, fmt.Errorf("create: password auth method: unknown configuration: %w", ErrUnsupportedConfiguration) + } + if err := c.validate(); err != nil { + return nil, fmt.Errorf("create: password auth method: %w", err) + } + + c.PrivateId, err = newArgon2ConfigurationId() + if err != nil { + return nil, fmt.Errorf("create: password auth method: %w", err) + } + m.PasswordConfId, c.PasswordMethodId = c.PrivateId, m.PublicId + var newAuthMethod *AuthMethod + var newArgon2Conf *Argon2Configuration _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error { + newArgon2Conf = c.clone() + if err := w.Create(ctx, newArgon2Conf, db.WithOplog(r.wrapper, c.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil { + return err + } newAuthMethod = m.clone() return w.Create(ctx, newAuthMethod, db.WithOplog(r.wrapper, m.oplog(oplog.OpType_OP_TYPE_CREATE))) }, diff --git a/internal/auth/password/repository_authmethod_test.go b/internal/auth/password/repository_authmethod_test.go index 352f5bbd73..a598b35bc2 100644 --- a/internal/auth/password/repository_authmethod_test.go +++ b/internal/auth/password/repository_authmethod_test.go @@ -96,6 +96,30 @@ func TestRepository_CreateAuthMethod(t *testing.T) { }, }, }, + { + name: "invalid-with-config-nil-embedded-config", + in: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + ScopeId: org.PublicId, + }, + }, + opts: []Option{ + WithConfiguration(&Argon2Configuration{}), + }, + wantIsErr: ErrInvalidConfiguration, + }, + { + name: "invalid-with-config-unknown-config-type", + in: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + ScopeId: org.PublicId, + }, + }, + opts: []Option{ + WithConfiguration(tconf(0)), + }, + wantIsErr: ErrUnsupportedConfiguration, + }, } for _, tt := range tests { diff --git a/internal/auth/password/repository_configuration.go b/internal/auth/password/repository_configuration.go new file mode 100644 index 0000000000..7a7aba21fc --- /dev/null +++ b/internal/auth/password/repository_configuration.go @@ -0,0 +1,134 @@ +package password + +import ( + "context" + "fmt" + + "github.com/hashicorp/watchtower/internal/auth/password/store" + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" +) + +// A Configuration is an interface holding one of the configuration types +// for a specific key derivation function. Argon2Configuration is currently +// the only configuration type. +type Configuration interface { + AuthMethodId() string + validate() error +} + +// GetConfiguration returns the current configuration for authMethodId. +func (r *Repository) GetConfiguration(ctx context.Context, authMethodId string) (Configuration, error) { + if authMethodId == "" { + return nil, fmt.Errorf("get password configuration: no auth method id: %w", db.ErrInvalidParameter) + } + cc, err := r.currentConfig(ctx, authMethodId) + if err != nil { + return nil, fmt.Errorf("get password configuration: %w", err) + } + return cc.argon2(), nil +} + +// SetConfiguration sets the configuration for c.AuthMethodId to c and +// returns a new Configuration. c is not changed. c must contain a valid +// AuthMethodId. c.PrivateId is ignored. +// +// If c contains new settings for c.AuthMethodId, SetConfiguration inserts +// c into the repository and updates AuthMethod to use the new +// configuration. If c contains settings equal to the current configuration +// for c.AuthMethodId, SetConfiguration ignores c. If c contains settings +// equal to a previous configuration for c.AuthMethodId, SetConfiguration +// updates AuthMethod to use the previous configuration. +func (r *Repository) SetConfiguration(ctx context.Context, c Configuration) (Configuration, error) { + if c == nil { + return nil, fmt.Errorf("set password configuration: %w", db.ErrNilParameter) + } + if c.AuthMethodId() == "" { + return nil, fmt.Errorf("set password configuration: no auth method id: %w", db.ErrInvalidParameter) + } + if err := c.validate(); err != nil { + return nil, fmt.Errorf("set password configuration: %w", err) + } + + switch v := c.(type) { + case *Argon2Configuration: + out, err := r.setArgon2Conf(ctx, v) + if err != nil { + return nil, fmt.Errorf("set password configuration: %w", err) + } + return out, nil + default: + return nil, fmt.Errorf("set password configuration: %w", ErrUnsupportedConfiguration) + } +} + +func (r *Repository) setArgon2Conf(ctx context.Context, c *Argon2Configuration) (*Argon2Configuration, error) { + c = c.clone() + + id, err := newArgon2ConfigurationId() + if err != nil { + return nil, err + } + c.PrivateId = id + + a := &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: c.PasswordMethodId, + }, + } + + newArgon2Conf := &Argon2Configuration{Argon2Configuration: &store.Argon2Configuration{}} + + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(rr db.Reader, w db.Writer) error { + where, args := c.whereDup() + if err := rr.LookupWhere(ctx, newArgon2Conf, where, args...); err != nil { + if err != db.ErrRecordNotFound { + return err + } + newArgon2Conf = c.clone() + if err := w.Create(ctx, newArgon2Conf, db.WithOplog(r.wrapper, c.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil { + return err + } + } + + a.PasswordConfId = newArgon2Conf.PrivateId + rowsUpdated, err := w.Update(ctx, a, []string{"PasswordConfId"}, nil, db.WithOplog(r.wrapper, a.oplog(oplog.OpType_OP_TYPE_UPDATE))) + if err == nil && rowsUpdated > 1 { + return db.ErrMultipleRecords + } + return err + }, + ) + if err != nil { + return nil, err + } + return newArgon2Conf, nil +} + +type currentConfig struct { + ConfType string + MinUserNameLength int + MinPasswordLength int + + *Argon2Configuration +} + +func (c *currentConfig) TableName() string { + return "auth_password_current_conf" +} + +func (r *Repository) currentConfig(ctx context.Context, authMethodId string) (*currentConfig, error) { + var cc currentConfig + if err := r.reader.LookupWhere(ctx, &cc, "password_method_id = ?", authMethodId); err != nil { + return nil, err + } + return &cc, nil +} + +func (c *currentConfig) argon2() *Argon2Configuration { + if c.ConfType != "argon2" { + return nil + } + return c.Argon2Configuration +} diff --git a/internal/auth/password/repository_configuration_test.go b/internal/auth/password/repository_configuration_test.go new file mode 100644 index 0000000000..ea22545954 --- /dev/null +++ b/internal/auth/password/repository_configuration_test.go @@ -0,0 +1,309 @@ +package password + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/hashicorp/watchtower/internal/auth/password/store" + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_GetSetConfiguration(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + assert.NoError(t, err) + require.NotNil(t, repo) + + authMethods := testAuthMethods(t, conn, 1) + authMethod := authMethods[0] + authMethodId := authMethod.GetPublicId() + ctx := context.Background() + + // The order of these tests are important. Some tests have a dependency + // on prior tests. + + var original string // original configuration ID + + t.Run("has-default-configuration", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := repo.GetConfiguration(ctx, authMethodId) + assert.NoError(err) + require.NotNil(got) + + conf, ok := got.(*Argon2Configuration) + require.True(ok, "want *Argon2Configuration") + + require.NotEmpty(conf.PrivateId, "default configuration PrivateId") + original = conf.PrivateId + + want := NewArgon2Configuration() + want.PrivateId = original + want.CreateTime = conf.CreateTime + want.PasswordMethodId = authMethodId + require.Equal(want, got) + }) + t.Run("change-configuration", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NotEmpty(original, "Original ID") + + current, err := repo.GetConfiguration(ctx, authMethodId) + assert.NoError(err) + require.NotNil(current) + + currentConf, ok := current.(*Argon2Configuration) + require.True(ok, "want *Argon2Configuration") + assert.Equal(original, currentConf.PrivateId) + + newConf := NewArgon2Configuration() + assert.Empty(newConf.PrivateId) + newConf.PasswordMethodId = currentConf.PasswordMethodId + newConf.Memory = currentConf.Memory * 2 + + updated, err := repo.SetConfiguration(ctx, newConf) + assert.NoError(err) + require.NotNil(updated) + assert.NotSame(newConf, updated) + + updatedConf, ok := updated.(*Argon2Configuration) + require.True(ok, "want *Argon2Configuration") + + assert.NotSame(newConf, updatedConf) + assert.NotEmpty(updatedConf.PrivateId, "updatedConf.PrivateId") + assert.NotEqual(original, updatedConf.PrivateId) + + current2, err := repo.GetConfiguration(ctx, authMethodId) + assert.NoError(err) + require.NotNil(current2) + + current2Conf, ok := current2.(*Argon2Configuration) + require.True(ok, "want *Argon2Configuration") + assert.Equal(updatedConf.PrivateId, current2Conf.PrivateId) + assert.Equal(newConf.Memory, current2Conf.Memory, "changed setting") + }) + t.Run("change-to-old-configuration", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NotEmpty(original, "Original ID") + + newConf := NewArgon2Configuration() + newConf.PasswordMethodId = authMethodId + assert.Empty(newConf.PrivateId) + + updated, err := repo.SetConfiguration(ctx, newConf) + assert.NoError(err) + require.NotNil(updated) + assert.NotSame(newConf, updated) + + updatedConf, ok := updated.(*Argon2Configuration) + require.True(ok, "want *Argon2Configuration") + + assert.NotSame(newConf, updatedConf) + assert.NotEmpty(updatedConf.PrivateId, "updatedConf.PrivateId") + assert.Equal(original, updatedConf.PrivateId) + + current, err := repo.GetConfiguration(ctx, authMethodId) + assert.NoError(err) + require.NotNil(current) + + currentConf, ok := current.(*Argon2Configuration) + require.True(ok, "want *Argon2Configuration") + assert.Equal(updatedConf.PrivateId, currentConf.PrivateId) + }) +} + +func TestRepository_GetConfiguration(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + assert.NoError(t, err) + require.NotNil(t, repo) + + authMethods := testAuthMethods(t, conn, 1) + authMethod := authMethods[0] + authMethodId := authMethod.GetPublicId() + ctx := context.Background() + + var tests = []struct { + name string + authMethodId string + want *Argon2Configuration + wantIsErr error + }{ + { + name: "invalid-no-authMethodId", + authMethodId: "", + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "invalid-authMethodId", + authMethodId: "abcdefghijk", + wantIsErr: db.ErrRecordNotFound, + }, + { + name: "valid-authMethodId", + authMethodId: authMethodId, + want: NewArgon2Configuration(), + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := repo.GetConfiguration(ctx, tt.authMethodId) + if tt.wantIsErr != nil { + assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got, "returned configuration") + return + } + require.NoError(err) + gotConf, ok := got.(*Argon2Configuration) + require.True(ok, "want *Argon2Configuration") + + tt.want.PasswordMethodId = tt.authMethodId + + assert.Equal(tt.authMethodId, gotConf.AuthMethodId(), "authMethodId") + + assert.Equal(tt.want.PasswordMethodId, gotConf.PasswordMethodId) + assert.Equal(tt.want.Iterations, gotConf.Iterations) + assert.Equal(tt.want.Memory, gotConf.Memory) + assert.Equal(tt.want.Threads, gotConf.Threads) + assert.Equal(tt.want.SaltLength, gotConf.SaltLength) + assert.Equal(tt.want.KeyLength, gotConf.KeyLength) + }) + } +} + +type tconf int +func (t tconf) AuthMethodId() string { return "abcdefghijk" } +func (t tconf) validate() error { return nil } +var _ Configuration = tconf(0) + +func TestRepository_SetConfiguration(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + assert.NoError(t, err) + require.NotNil(t, repo) + + authMethods := testAuthMethods(t, conn, 1) + authMethod := authMethods[0] + authMethodId := authMethod.GetPublicId() + + var tests = []struct { + name string + in Configuration + want *Argon2Configuration + wantUnknownErr bool + wantIsErr error + }{ + { + name: "invalid-nil-config", + wantIsErr: db.ErrNilParameter, + }, + { + name: "nil-embedded-config", + in: &Argon2Configuration{}, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "invalid-no-authMethodId", + in: NewArgon2Configuration(), + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "unknown-configuration-type", + in: tconf(0), + wantIsErr: ErrUnsupportedConfiguration, + }, + { + name: "invalid-unknown-authMethodId", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + PasswordMethodId: "abcdefghijk", + Iterations: 3 * 2, + Memory: 64 * 1024, + Threads: 1, + SaltLength: 32, + KeyLength: 32, + }, + }, + wantUnknownErr: true, + }, + { + name: "invalid-config-setting", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + PasswordMethodId: authMethodId, + Iterations: 0, + Memory: 64 * 1024, + Threads: 1, + SaltLength: 32, + KeyLength: 32, + }, + }, + wantIsErr: ErrInvalidConfiguration, + }, + { + name: "valid", + in: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + PasswordMethodId: authMethodId, + Iterations: 3 * 2, + Memory: 64 * 1024, + Threads: 1, + SaltLength: 32, + KeyLength: 32, + }, + }, + want: &Argon2Configuration{ + Argon2Configuration: &store.Argon2Configuration{ + PasswordMethodId: authMethodId, + Iterations: 3 * 2, + Memory: 64 * 1024, + Threads: 1, + SaltLength: 32, + KeyLength: 32, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := repo.SetConfiguration(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, "returned configuration") + return + } + if tt.wantUnknownErr { + assert.Error(err) + return + } + require.NoError(err) + + assert.NotSame(tt.in, got) + + gotConf, ok := got.(*Argon2Configuration) + require.True(ok, "want *Argon2Configuration") + + assert.Equal(tt.want.PasswordMethodId, gotConf.PasswordMethodId) + assert.Equal(tt.want.Iterations, gotConf.Iterations) + assert.Equal(tt.want.Memory, gotConf.Memory) + assert.Equal(tt.want.Threads, gotConf.Threads) + assert.Equal(tt.want.SaltLength, gotConf.SaltLength) + assert.Equal(tt.want.KeyLength, gotConf.KeyLength) + + assert.NoError(db.TestVerifyOplog(t, rw, gotConf.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } +} diff --git a/internal/auth/password/store/argon2.pb.go b/internal/auth/password/store/argon2.pb.go new file mode 100644 index 0000000000..b38df32d8c --- /dev/null +++ b/internal/auth/password/store/argon2.pb.go @@ -0,0 +1,264 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.12.3 +// source: controller/storage/auth/password/store/v1/argon2.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 + +// Argon2Configuration is a configuration for using the argon2id key +// derivation function. It is owned by an AuthMethod. +// +// Iterations, Memory, and Threads are the cost parameters. The cost +// parameters should be increased as memory latency and CPU parallelism +// increases. +// +// For a detailed specification of Argon2 see: +// https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf +type Argon2Configuration struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + PrivateId string `protobuf:"bytes,1,opt,name=private_id,json=privateId,proto3" json:"private_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"` + // @inject_tag: `gorm:"not_null"` + PasswordMethodId string `protobuf:"bytes,3,opt,name=password_method_id,json=passwordMethodId,proto3" json:"password_method_id,omitempty" gorm:"not_null"` + // Iterations is the time parameter in the Argon2 specification. It + // specifies the number of passes over the memory. Must be > 0. + // @inject_tag: `gorm:"default:null"` + Iterations uint32 `protobuf:"varint,4,opt,name=iterations,proto3" json:"iterations,omitempty" gorm:"default:null"` + // Memory is the memory parameter in the Argon2 specification. It + // specifies the size of the memory in KiB. For example Memory=32*1024 + // sets the memory cost to ~32 MB. Must be > 0. + // @inject_tag: `gorm:"default:null"` + Memory uint32 `protobuf:"varint,5,opt,name=memory,proto3" json:"memory,omitempty" gorm:"default:null"` + // Threads is the threads parameter in the Argon2 specification. It can + // be adjusted to the number of available CPUs. Must be > 0. + // @inject_tag: `gorm:"default:null"` + Threads uint32 `protobuf:"varint,6,opt,name=threads,proto3" json:"threads,omitempty" gorm:"default:null"` + // SaltLength is in bytes. Must be >= 16. + // @inject_tag: `gorm:"default:null"` + SaltLength uint32 `protobuf:"varint,7,opt,name=salt_length,json=saltLength,proto3" json:"salt_length,omitempty" gorm:"default:null"` + // KeyLength is in bytes. Must be >= 16. + // @inject_tag: `gorm:"default:null"` + KeyLength uint32 `protobuf:"varint,8,opt,name=key_length,json=keyLength,proto3" json:"key_length,omitempty" gorm:"default:null"` +} + +func (x *Argon2Configuration) Reset() { + *x = Argon2Configuration{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Argon2Configuration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Argon2Configuration) ProtoMessage() {} + +func (x *Argon2Configuration) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_password_store_v1_argon2_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 Argon2Configuration.ProtoReflect.Descriptor instead. +func (*Argon2Configuration) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_password_store_v1_argon2_proto_rawDescGZIP(), []int{0} +} + +func (x *Argon2Configuration) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *Argon2Configuration) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Argon2Configuration) GetPasswordMethodId() string { + if x != nil { + return x.PasswordMethodId + } + return "" +} + +func (x *Argon2Configuration) GetIterations() uint32 { + if x != nil { + return x.Iterations + } + return 0 +} + +func (x *Argon2Configuration) GetMemory() uint32 { + if x != nil { + return x.Memory + } + return 0 +} + +func (x *Argon2Configuration) GetThreads() uint32 { + if x != nil { + return x.Threads + } + return 0 +} + +func (x *Argon2Configuration) GetSaltLength() uint32 { + if x != nil { + return x.SaltLength + } + return 0 +} + +func (x *Argon2Configuration) GetKeyLength() uint32 { + if x != nil { + return x.KeyLength + } + return 0 +} + +var File_controller_storage_auth_password_store_v1_argon2_proto protoreflect.FileDescriptor + +var file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc = []byte{ + 0x0a, 0x36, 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, 0x61, 0x72, 0x67, 0x6f, + 0x6e, 0x32, 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, 0xc1, 0x02, 0x0a, 0x13, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 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, 0x2c, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x69, 0x74, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x18, + 0x0a, 0x07, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x07, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x61, 0x6c, 0x74, + 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, + 0x61, 0x6c, 0x74, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x6b, 0x65, 0x79, + 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x6b, + 0x65, 0x79, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 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_argon2_proto_rawDescOnce sync.Once + file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData = file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc +) + +func file_controller_storage_auth_password_store_v1_argon2_proto_rawDescGZIP() []byte { + file_controller_storage_auth_password_store_v1_argon2_proto_rawDescOnce.Do(func() { + file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData) + }) + return file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData +} + +var file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_auth_password_store_v1_argon2_proto_goTypes = []interface{}{ + (*Argon2Configuration)(nil), // 0: controller.storage.auth.password.store.v1.Argon2Configuration + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_auth_password_store_v1_argon2_proto_depIdxs = []int32{ + 1, // 0: controller.storage.auth.password.store.v1.Argon2Configuration.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_controller_storage_auth_password_store_v1_argon2_proto_init() } +func file_controller_storage_auth_password_store_v1_argon2_proto_init() { + if File_controller_storage_auth_password_store_v1_argon2_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Argon2Configuration); 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_argon2_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_auth_password_store_v1_argon2_proto_goTypes, + DependencyIndexes: file_controller_storage_auth_password_store_v1_argon2_proto_depIdxs, + MessageInfos: file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes, + }.Build() + File_controller_storage_auth_password_store_v1_argon2_proto = out.File + file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc = nil + file_controller_storage_auth_password_store_v1_argon2_proto_goTypes = nil + file_controller_storage_auth_password_store_v1_argon2_proto_depIdxs = nil +} diff --git a/internal/auth/password/store/password.pb.go b/internal/auth/password/store/password.pb.go index ae39a56612..031211d441 100644 --- a/internal/auth/password/store/password.pb.go +++ b/internal/auth/password/store/password.pb.go @@ -50,6 +50,8 @@ type AuthMethod struct { // 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"` + PasswordConfId string `protobuf:"bytes,7,opt,name=password_conf_id,json=passwordConfId,proto3" json:"password_conf_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"` @@ -130,6 +132,13 @@ func (x *AuthMethod) GetScopeId() string { return "" } +func (x *AuthMethod) GetPasswordConfId() string { + if x != nil { + return x.PasswordConfId + } + return "" +} + func (x *AuthMethod) GetMinUserNameLength() uint32 { if x != nil { return x.MinUserNameLength @@ -272,7 +281,7 @@ var file_controller_storage_auth_password_store_v1_password_proto_rawDesc = []by 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, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9f, 0x03, 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, @@ -289,40 +298,42 @@ var file_controller_storage_auth_password_store_v1_password_proto_rawDesc = []by 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, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x28, 0x0a, + 0x10, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x69, + 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x43, 0x6f, 0x6e, 0x66, 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 ( diff --git a/internal/db/migrations/postgres.gen.go b/internal/db/migrations/postgres.gen.go index 5d75ec9563..d65781dd97 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -1660,7 +1660,7 @@ begin; ┌────────────────┐ ┌──────────────────────┐ ┌────────────────────────────┐ │ auth_method │ │ auth_password_method │ │ auth_password_conf │ ├────────────────┤ ├──────────────────────┤ ├────────────────────────────┤ - │ public_id (pk) │ │ public_id (pk,fk) │ ╱│ public_id (pk,fk) │ + │ public_id (pk) │ │ public_id (pk,fk) │ ╱│ private_id (pk,fk) │ │ scope_id (fk) │┼┼─────────────○┼│ scope_id (fk) │┼┼─────────○─│ password_method_id (fk) │ │ │ │ ... │ ╲│ │ └────────────────┘ └──────────────────────┘ └────────────────────────────┘ @@ -1674,7 +1674,7 @@ begin; ┌──────────────────────────┐ ┌──────────────────────────┐ ┌───────────────────────────────┐ │ auth_account │ │ auth_password_account │ │ auth_password_credential │ ├──────────────────────────┤ ├──────────────────────────┤ ├───────────────────────────────┤ - │ public_id (pk) │ │ public_id (pk,fk2) │ │ public_id (pk) │ + │ public_id (pk) │ │ public_id (pk,fk2) │ │ private_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) │ @@ -1714,6 +1714,7 @@ begin; create table auth_password_method ( public_id wt_public_id primary key, scope_id wt_scope_id not null, + password_conf_id wt_private_id not null, -- FK to auth_password_conf added below name text, description text, create_time wt_timestamp, @@ -1769,15 +1770,23 @@ begin; for each row execute procedure insert_auth_account_subtype(); create table auth_password_conf ( - public_id wt_public_id primary key, + private_id wt_private_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) + unique(password_method_id, private_id) ); + alter table auth_password_method + add constraint current_conf_fkey + foreign key (public_id, password_conf_id) + references auth_password_conf (password_method_id, private_id) + on delete cascade + on update cascade + deferrable initially deferred; + -- insert_auth_password_conf_subtype() is a trigger function for subtypes of -- auth_password_conf create or replace function @@ -1786,20 +1795,20 @@ begin; as $$ begin insert into auth_password_conf - (public_id, password_method_id) + (private_id, password_method_id) values - (new.public_id, new.password_method_id); + (new.private_id, new.password_method_id); return new; end; $$ language plpgsql; create table auth_password_credential ( - public_id wt_public_id primary key, + private_id wt_private_id primary key, password_account_id wt_public_id not null unique, - password_conf_id wt_public_id not null, + password_conf_id wt_private_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) + references auth_password_conf (password_method_id, private_id) on delete cascade on update cascade, foreign key (password_method_id, password_account_id) @@ -1823,16 +1832,16 @@ begin; 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) + (private_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); + (new.private_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 @@ -1870,13 +1879,151 @@ begin; insert on auth_password_account for each row execute procedure default_create_time(); - insert into oplog_ticket (name, version) + -- The tickets for oplog are the subtypes not the base types because no updates + -- are done to any values in the base types. + insert into oplog_ticket + (name, version) values ('auth_password_method', 1), ('auth_password_account', 1); commit; +`), + }, + "migrations/13_auth_password_argon.down.sql": { + name: "13_auth_password_argon.down.sql", + bytes: []byte(` +begin; + + drop table auth_password_argon2_conf; + +commit; + +`), + }, + "migrations/13_auth_password_argon.up.sql": { + name: "13_auth_password_argon.up.sql", + bytes: []byte(` +begin; + + create table auth_password_argon2_conf ( + private_id wt_private_id primary key + references auth_password_conf (private_id) + on delete cascade + on update cascade, + password_method_id wt_public_id not null, + create_time wt_timestamp, + iterations int not null default 3 + check(iterations > 0), + memory int not null default 65536 + check(memory > 0), + threads int not null default 1 + check(threads > 0), + -- salt_length unit is bytes + salt_length int not null default 32 + -- minimum of 16 bytes (128 bits) + check(salt_length >= 16), + -- key_length unit is bytes + key_length int not null default 32 + -- minimum of 16 bytes (128 bits) + check(key_length >= 16), + unique(password_method_id, iterations, memory, threads, salt_length, key_length), + unique (password_method_id, private_id), + foreign key (password_method_id, private_id) + references auth_password_conf (password_method_id, private_id) + on delete cascade + on update cascade + deferrable initially deferred + ); + create or replace function + read_only_auth_password_argon2_conf() + returns trigger + as $$ + begin + raise exception 'auth_password_argon2_conf is read-only'; + end; + $$ language plpgsql; + + create trigger + read_only_auth_password_argon2_conf + before + update on auth_password_argon2_conf + for each row execute procedure read_only_auth_password_argon2_conf(); + + create trigger + insert_auth_password_conf_subtype + before insert on auth_password_argon2_conf + for each row execute procedure insert_auth_password_conf_subtype(); + + -- + -- triggers for time columns + -- + create trigger + immutable_create_time + before + update on auth_password_argon2_conf + for each row execute procedure immutable_create_time_func(); + + create trigger + default_create_time_column + before + insert on auth_password_argon2_conf + for each row execute procedure default_create_time(); + + -- The tickets for oplog are the subtypes not the base types because no updates + -- are done to any values in the base types. + insert into oplog_ticket + (name, version) + values + ('auth_password_argon2_conf', 1), + ('auth_password_argon2_cred', 1); + +commit; + +`), + }, + "migrations/14_auth_password_views.down.sql": { + name: "14_auth_password_views.down.sql", + bytes: []byte(` +begin; + + drop view auth_password_current_conf; + drop view auth_password_conf_union; + +commit; + +`), + }, + "migrations/14_auth_password_views.up.sql": { + name: "14_auth_password_views.up.sql", + bytes: []byte(` +begin; + + -- auth_password_conf_union is a union of the configuration settings + -- of all supported key derivation functions. + -- It will be updated as new key derivation functions are supported. + create or replace view auth_password_conf_union as + -- Do not change the order of the columns when adding new configurations. + -- Union with new tables appending new columns as needed. + select c.password_method_id, c.private_id as password_conf_id, c.private_id, + 'argon2' as conf_type, + c.iterations, c.memory, c.threads, c.salt_length, c.key_length + from auth_password_argon2_conf c; + + -- auth_password_current_conf provides a view of the current password + -- configuration for each password auth method. + -- The view will be updated as new key derivation functions are supported + -- but the query to create the view should not need to be updated. + create or replace view auth_password_current_conf as + -- Rerun this query whenever auth_password_conf_union is updated. + select pm.min_user_name_length, pm.min_password_length, c.* + from auth_password_method pm + inner join auth_password_conf_union c + on pm.password_conf_id = c.password_conf_id; + +commit; + `), }, } diff --git a/internal/db/migrations/postgres/12_auth_password.up.sql b/internal/db/migrations/postgres/12_auth_password.up.sql index 3b93a5bb2e..1c8ce5eb07 100644 --- a/internal/db/migrations/postgres/12_auth_password.up.sql +++ b/internal/db/migrations/postgres/12_auth_password.up.sql @@ -5,7 +5,7 @@ begin; ┌────────────────┐ ┌──────────────────────┐ ┌────────────────────────────┐ │ auth_method │ │ auth_password_method │ │ auth_password_conf │ ├────────────────┤ ├──────────────────────┤ ├────────────────────────────┤ - │ public_id (pk) │ │ public_id (pk,fk) │ ╱│ public_id (pk,fk) │ + │ public_id (pk) │ │ public_id (pk,fk) │ ╱│ private_id (pk,fk) │ │ scope_id (fk) │┼┼─────────────○┼│ scope_id (fk) │┼┼─────────○─│ password_method_id (fk) │ │ │ │ ... │ ╲│ │ └────────────────┘ └──────────────────────┘ └────────────────────────────┘ @@ -19,7 +19,7 @@ begin; ┌──────────────────────────┐ ┌──────────────────────────┐ ┌───────────────────────────────┐ │ auth_account │ │ auth_password_account │ │ auth_password_credential │ ├──────────────────────────┤ ├──────────────────────────┤ ├───────────────────────────────┤ - │ public_id (pk) │ │ public_id (pk,fk2) │ │ public_id (pk) │ + │ public_id (pk) │ │ public_id (pk,fk2) │ │ private_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) │ @@ -59,6 +59,7 @@ begin; create table auth_password_method ( public_id wt_public_id primary key, scope_id wt_scope_id not null, + password_conf_id wt_private_id not null, -- FK to auth_password_conf added below name text, description text, create_time wt_timestamp, @@ -114,15 +115,23 @@ begin; for each row execute procedure insert_auth_account_subtype(); create table auth_password_conf ( - public_id wt_public_id primary key, + private_id wt_private_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) + unique(password_method_id, private_id) ); + alter table auth_password_method + add constraint current_conf_fkey + foreign key (public_id, password_conf_id) + references auth_password_conf (password_method_id, private_id) + on delete cascade + on update cascade + deferrable initially deferred; + -- insert_auth_password_conf_subtype() is a trigger function for subtypes of -- auth_password_conf create or replace function @@ -131,20 +140,20 @@ begin; as $$ begin insert into auth_password_conf - (public_id, password_method_id) + (private_id, password_method_id) values - (new.public_id, new.password_method_id); + (new.private_id, new.password_method_id); return new; end; $$ language plpgsql; create table auth_password_credential ( - public_id wt_public_id primary key, + private_id wt_private_id primary key, password_account_id wt_public_id not null unique, - password_conf_id wt_public_id not null, + password_conf_id wt_private_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) + references auth_password_conf (password_method_id, private_id) on delete cascade on update cascade, foreign key (password_method_id, password_account_id) @@ -168,16 +177,16 @@ begin; 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) + (private_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); + (new.private_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 @@ -215,7 +224,10 @@ begin; insert on auth_password_account for each row execute procedure default_create_time(); - insert into oplog_ticket (name, version) + -- The tickets for oplog are the subtypes not the base types because no updates + -- are done to any values in the base types. + insert into oplog_ticket + (name, version) values ('auth_password_method', 1), ('auth_password_account', 1); diff --git a/internal/db/migrations/postgres/13_auth_password_argon.down.sql b/internal/db/migrations/postgres/13_auth_password_argon.down.sql new file mode 100644 index 0000000000..ba4f8bb75d --- /dev/null +++ b/internal/db/migrations/postgres/13_auth_password_argon.down.sql @@ -0,0 +1,5 @@ +begin; + + drop table auth_password_argon2_conf; + +commit; diff --git a/internal/db/migrations/postgres/13_auth_password_argon.up.sql b/internal/db/migrations/postgres/13_auth_password_argon.up.sql new file mode 100644 index 0000000000..f47f8e6fd4 --- /dev/null +++ b/internal/db/migrations/postgres/13_auth_password_argon.up.sql @@ -0,0 +1,75 @@ +begin; + + create table auth_password_argon2_conf ( + private_id wt_private_id primary key + references auth_password_conf (private_id) + on delete cascade + on update cascade, + password_method_id wt_public_id not null, + create_time wt_timestamp, + iterations int not null default 3 + check(iterations > 0), + memory int not null default 65536 + check(memory > 0), + threads int not null default 1 + check(threads > 0), + -- salt_length unit is bytes + salt_length int not null default 32 + -- minimum of 16 bytes (128 bits) + check(salt_length >= 16), + -- key_length unit is bytes + key_length int not null default 32 + -- minimum of 16 bytes (128 bits) + check(key_length >= 16), + unique(password_method_id, iterations, memory, threads, salt_length, key_length), + unique (password_method_id, private_id), + foreign key (password_method_id, private_id) + references auth_password_conf (password_method_id, private_id) + on delete cascade + on update cascade + deferrable initially deferred + ); + create or replace function + read_only_auth_password_argon2_conf() + returns trigger + as $$ + begin + raise exception 'auth_password_argon2_conf is read-only'; + end; + $$ language plpgsql; + + create trigger + read_only_auth_password_argon2_conf + before + update on auth_password_argon2_conf + for each row execute procedure read_only_auth_password_argon2_conf(); + + create trigger + insert_auth_password_conf_subtype + before insert on auth_password_argon2_conf + for each row execute procedure insert_auth_password_conf_subtype(); + + -- + -- triggers for time columns + -- + create trigger + immutable_create_time + before + update on auth_password_argon2_conf + for each row execute procedure immutable_create_time_func(); + + create trigger + default_create_time_column + before + insert on auth_password_argon2_conf + for each row execute procedure default_create_time(); + + -- The tickets for oplog are the subtypes not the base types because no updates + -- are done to any values in the base types. + insert into oplog_ticket + (name, version) + values + ('auth_password_argon2_conf', 1), + ('auth_password_argon2_cred', 1); + +commit; diff --git a/internal/db/migrations/postgres/14_auth_password_views.down.sql b/internal/db/migrations/postgres/14_auth_password_views.down.sql new file mode 100644 index 0000000000..303ccd0bda --- /dev/null +++ b/internal/db/migrations/postgres/14_auth_password_views.down.sql @@ -0,0 +1,6 @@ +begin; + + drop view auth_password_current_conf; + drop view auth_password_conf_union; + +commit; diff --git a/internal/db/migrations/postgres/14_auth_password_views.up.sql b/internal/db/migrations/postgres/14_auth_password_views.up.sql new file mode 100644 index 0000000000..6806f71927 --- /dev/null +++ b/internal/db/migrations/postgres/14_auth_password_views.up.sql @@ -0,0 +1,25 @@ +begin; + + -- auth_password_conf_union is a union of the configuration settings + -- of all supported key derivation functions. + -- It will be updated as new key derivation functions are supported. + create or replace view auth_password_conf_union as + -- Do not change the order of the columns when adding new configurations. + -- Union with new tables appending new columns as needed. + select c.password_method_id, c.private_id as password_conf_id, c.private_id, + 'argon2' as conf_type, + c.iterations, c.memory, c.threads, c.salt_length, c.key_length + from auth_password_argon2_conf c; + + -- auth_password_current_conf provides a view of the current password + -- configuration for each password auth method. + -- The view will be updated as new key derivation functions are supported + -- but the query to create the view should not need to be updated. + create or replace view auth_password_current_conf as + -- Rerun this query whenever auth_password_conf_union is updated. + select pm.min_user_name_length, pm.min_password_length, c.* + from auth_password_method pm + inner join auth_password_conf_union c + on pm.password_conf_id = c.password_conf_id; + +commit; diff --git a/internal/proto/local/controller/storage/auth/password/store/v1/argon2.proto b/internal/proto/local/controller/storage/auth/password/store/v1/argon2.proto new file mode 100644 index 0000000000..b99418c013 --- /dev/null +++ b/internal/proto/local/controller/storage/auth/password/store/v1/argon2.proto @@ -0,0 +1,52 @@ +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"; + +// Argon2Configuration is a configuration for using the argon2id key +// derivation function. It is owned by an AuthMethod. +// +// Iterations, Memory, and Threads are the cost parameters. The cost +// parameters should be increased as memory latency and CPU parallelism +// increases. +// +// For a detailed specification of Argon2 see: +// https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf +message Argon2Configuration { + // @inject_tag: `gorm:"primary_key"` + string private_id = 1; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // @inject_tag: `gorm:"not_null"` + string password_method_id = 3; + + // Iterations is the time parameter in the Argon2 specification. It + // specifies the number of passes over the memory. Must be > 0. + // @inject_tag: `gorm:"default:null"` + uint32 iterations = 4; + + // Memory is the memory parameter in the Argon2 specification. It + // specifies the size of the memory in KiB. For example Memory=32*1024 + // sets the memory cost to ~32 MB. Must be > 0. + // @inject_tag: `gorm:"default:null"` + uint32 memory = 5; + + // Threads is the threads parameter in the Argon2 specification. It can + // be adjusted to the number of available CPUs. Must be > 0. + // @inject_tag: `gorm:"default:null"` + uint32 threads = 6; + + // SaltLength is in bytes. Must be >= 16. + // @inject_tag: `gorm:"default:null"` + uint32 salt_length = 7; + + // KeyLength is in bytes. Must be >= 16. + // @inject_tag: `gorm:"default:null"` + uint32 key_length = 8; +} diff --git a/internal/proto/local/controller/storage/auth/password/store/v1/password.proto b/internal/proto/local/controller/storage/auth/password/store/v1/password.proto index 1659c296e2..d5feee186a 100644 --- a/internal/proto/local/controller/storage/auth/password/store/v1/password.proto +++ b/internal/proto/local/controller/storage/auth/password/store/v1/password.proto @@ -30,6 +30,9 @@ message AuthMethod { // @inject_tag: `gorm:"not_null"` string scope_id = 6; + // @inject_tag: `gorm:"not_null"` + string password_conf_id = 7; + // @inject_tag: `gorm:"default:null"` uint32 min_user_name_length = 8;