From 7da892a05499bf3173bcfaf45a70ab51b672a703 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Tue, 1 Jun 2021 14:12:34 -0500 Subject: [PATCH] Managed Groups OIDC Repo (#1254) --- internal/auth/additional_verification_test.go | 5 +- internal/auth/oidc/ids.go | 11 + internal/auth/oidc/managed_group.go | 98 ++ internal/auth/oidc/managed_group_test.go | 32 + internal/auth/oidc/repository_account_test.go | 2 +- .../oidc/repository_auth_method_update.go | 1 + .../auth/oidc/repository_managed_group.go | 251 +++++ .../oidc/repository_managed_group_test.go | 972 ++++++++++++++++++ internal/auth/oidc/store/oidc.pb.go | 206 +++- internal/auth/oidc/testing.go | 18 + internal/auth/subtype.go | 15 +- internal/cmd/base/dev.go | 10 +- internal/cmd/base/option.go | 29 +- internal/cmd/commands/server/server_test.go | 3 +- .../storage/auth/oidc/store/v1/oidc.proto | 34 + internal/servers/controller/testing.go | 13 +- internal/tests/cluster/unix_listener_test.go | 5 +- 17 files changed, 1660 insertions(+), 45 deletions(-) create mode 100644 internal/auth/oidc/managed_group.go create mode 100644 internal/auth/oidc/managed_group_test.go create mode 100644 internal/auth/oidc/repository_managed_group.go create mode 100644 internal/auth/oidc/repository_managed_group_test.go diff --git a/internal/auth/additional_verification_test.go b/internal/auth/additional_verification_test.go index afc6c8cce..eae6f4c24 100644 --- a/internal/auth/additional_verification_test.go +++ b/internal/auth/additional_verification_test.go @@ -116,7 +116,10 @@ func TestFetchActionSetForId(t *testing.T) { func TestRecursiveListingDifferentOutputFields(t *testing.T) { require, assert := require.New(t), assert.New(t) - tc := controller.NewTestController(t, nil) + tc := controller.NewTestController(t, &controller.TestControllerOpts{ + // Disable this to avoid having to deal with sorting them in the test + DisableOidcAuthMethodCreation: true, + }) defer tc.Shutdown() conn := tc.DbConn() diff --git a/internal/auth/oidc/ids.go b/internal/auth/oidc/ids.go index 05ef04464..20cd10b02 100644 --- a/internal/auth/oidc/ids.go +++ b/internal/auth/oidc/ids.go @@ -10,6 +10,8 @@ const ( AuthMethodPrefix = "amoidc" // AccountPrefix defines the prefix for Account public ids. AccountPrefix = "acctoidc" + // ManagedGroupPrefix defines the prefix for ManagedGroup public ids. + ManagedGroupPrefix = "mgoidc" ) func newAuthMethodId() (string, error) { @@ -38,3 +40,12 @@ func newAccountId(authMethodId, issuer, sub string) (string, error) { } return id, nil } + +func newManagedGroupId() (string, error) { + const op = "oidc.newManagedGroupId" + id, err := db.NewPublicId(ManagedGroupPrefix) + if err != nil { + return "", errors.Wrap(err, op) + } + return id, nil +} diff --git a/internal/auth/oidc/managed_group.go b/internal/auth/oidc/managed_group.go new file mode 100644 index 000000000..a50479c89 --- /dev/null +++ b/internal/auth/oidc/managed_group.go @@ -0,0 +1,98 @@ +package oidc + +import ( + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/go-bexpr" + "google.golang.org/protobuf/proto" +) + +// defaultManagedGroupTableName defines the default table name for a Managed Group +const defaultManagedGroupTableName = "auth_oidc_managed_group" + +// ManagedGroup contains an OIDC managed group. It is assigned to an OIDC AuthMethod +// and updates/deletes to that AuthMethod are cascaded to its Managed Groups. +type ManagedGroup struct { + *store.ManagedGroup + tableName string +} + +// NewManagedGroup creates a new in memory ManagedGroup assigned to OIDC +// AuthMethod. Supported options are withName and withDescription. +func NewManagedGroup(authMethodId string, filter string, opt ...Option) (*ManagedGroup, error) { + const op = "oidc.NewManagedGroup" + opts := getOpts(opt...) + mg := &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethodId, + Name: opts.withName, + Description: opts.withDescription, + Filter: filter, + }, + } + if err := mg.validate(op); err != nil { + return nil, err // intentionally not wrapped. + } + + return mg, nil +} + +// validate the Managed Group. On success, it will return nil. +func (mg *ManagedGroup) validate(caller errors.Op) error { + if mg.AuthMethodId == "" { + return errors.New(errors.InvalidParameter, caller, "missing auth method id") + } + if mg.Filter == "" { + return errors.New(errors.InvalidParameter, caller, "missing filter") + } + if _, err := bexpr.CreateEvaluator(mg.Filter); err != nil { + return errors.New(errors.InvalidParameter, caller, "error evaluating filter expression", errors.WithWrap(err)) + } + + return nil +} + +// AllocManagedGroup makes an empty one in memory +func AllocManagedGroup() *ManagedGroup { + return &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + } +} + +// Clone a ManagedGroup. +func (mg *ManagedGroup) Clone() *ManagedGroup { + cp := proto.Clone(mg.ManagedGroup) + return &ManagedGroup{ + ManagedGroup: cp.(*store.ManagedGroup), + } +} + +// TableName returns the table name. +func (mg *ManagedGroup) TableName() string { + if mg.tableName != "" { + return mg.tableName + } + return defaultManagedGroupTableName +} + +// SetTableName sets the table name. +func (mg *ManagedGroup) SetTableName(n string) { + mg.tableName = n +} + +// oplog will create oplog metadata for the ManagedGroup. +func (c *ManagedGroup) oplog(op oplog.OpType, authMethodScopeId string) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{c.GetPublicId()}, + "resource-type": []string{"oidc managed group"}, + "op-type": []string{op.String()}, + } + if c.AuthMethodId != "" { + metadata["auth-method-id"] = []string{c.AuthMethodId} + } + if authMethodScopeId != "" { + metadata["scope-id"] = []string{authMethodScopeId} + } + return metadata +} diff --git a/internal/auth/oidc/managed_group_test.go b/internal/auth/oidc/managed_group_test.go new file mode 100644 index 000000000..35f518c1c --- /dev/null +++ b/internal/auth/oidc/managed_group_test.go @@ -0,0 +1,32 @@ +package oidc + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" +) + +func Test_ManagedGroups_RepoValidate(t *testing.T) { + t.Parallel() + assert := assert.New(t) + const op = "test" + mg := AllocManagedGroup() + t.Run("empty auth method", func(t *testing.T) { + assert.Contains(mg.validate(op).Error(), errors.New(errors.InvalidParameter, op, "missing auth method id").Error()) + }) + t.Run("empty filter", func(t *testing.T) { + mg.AuthMethodId = "amoidc_1234567890" + assert.Contains(mg.validate(op).Error(), errors.New(errors.InvalidParameter, op, "missing filter").Error()) + }) + t.Run("bad filter", func(t *testing.T) { + mg.AuthMethodId = "amoidc_1234567890" + mg.Filter = "foobar" + assert.Contains(mg.validate(op).Error(), errors.New(errors.InvalidParameter, op, "error evaluating filter expression").Error()) + }) + t.Run("valid", func(t *testing.T) { + mg.AuthMethodId = "amoidc_1234567890" + mg.Filter = testFakeFilter + assert.NoError(mg.validate(op)) + }) +} diff --git a/internal/auth/oidc/repository_account_test.go b/internal/auth/oidc/repository_account_test.go index 8cda9fed3..f2121b70b 100644 --- a/internal/auth/oidc/repository_account_test.go +++ b/internal/auth/oidc/repository_account_test.go @@ -67,7 +67,7 @@ func TestRepository_CreateAccount(t *testing.T) { wantErrMsg: "oidc.(Repository).CreateAccount: missing embedded Account: parameter violation: error #100", }, { - name: "invalid-no-scope-id", + name: "invalid-no-auth-method-id", in: &Account{ Account: &store.Account{}, }, diff --git a/internal/auth/oidc/repository_auth_method_update.go b/internal/auth/oidc/repository_auth_method_update.go index e6af61ef1..3012c7b9a 100644 --- a/internal/auth/oidc/repository_auth_method_update.go +++ b/internal/auth/oidc/repository_auth_method_update.go @@ -21,6 +21,7 @@ const ( VersionField = "Version" NameField = "Name" DescriptionField = "Description" + FilterField = "Filter" IssuerField = "Issuer" ClientIdField = "ClientId" ClientSecretField = "ClientSecret" diff --git a/internal/auth/oidc/repository_managed_group.go b/internal/auth/oidc/repository_managed_group.go new file mode 100644 index 000000000..e5193586d --- /dev/null +++ b/internal/auth/oidc/repository_managed_group.go @@ -0,0 +1,251 @@ +package oidc + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + dbcommon "github.com/hashicorp/boundary/internal/db/common" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// CreateManagedGroup inserts an ManagedGroup, mg, into the repository and +// returns a new ManagedGroup containing its PublicId. mg is not changed. mg +// must contain a valid AuthMethodId. mg must not contain a PublicId. The +// PublicId is generated and assigned by this method. +// +// Both mg.Name and mg.Description are optional. If mg.Name is set, it must be +// unique within mg.AuthMethodId. +func (r *Repository) CreateManagedGroup(ctx context.Context, scopeId string, mg *ManagedGroup, opt ...Option) (*ManagedGroup, error) { + const op = "oidc.(Repository).CreateManagedGroup" + if mg == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing ManagedGroup") + } + if mg.ManagedGroup == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing embedded ManagedGroup") + } + if mg.AuthMethodId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method id") + } + if mg.Filter == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing filter") + } + if mg.PublicId != "" { + return nil, errors.New(errors.InvalidParameter, op, "public id must be empty") + } + if scopeId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing scope id") + } + + mg = mg.Clone() + + id, err := newManagedGroupId() + if err != nil { + return nil, errors.Wrap(err, op) + } + mg.PublicId = id + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get oplog wrapper"), errors.WithCode(errors.Encrypt)) + } + + var newManagedGroup *ManagedGroup + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newManagedGroup = mg.Clone() + if err := w.Create(ctx, newManagedGroup, db.WithOplog(oplogWrapper, mg.oplog(oplog.OpType_OP_TYPE_CREATE, scopeId))); err != nil { + return errors.Wrap(err, op) + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.New(errors.NotUnique, op, fmt.Sprintf( + "in auth method %s: name %q already exists", + mg.AuthMethodId, mg.Name)) + } + return nil, errors.Wrap(err, op, errors.WithMsg(mg.AuthMethodId)) + } + return newManagedGroup, nil +} + +// LookupManagedGroup will look up a managed group in the repository. If the managed group is not +// found, it will return nil, nil. All options are ignored. +func (r *Repository) LookupManagedGroup(ctx context.Context, withPublicId string, opt ...Option) (*ManagedGroup, error) { + const op = "oidc.(Repository).LookupManagedGroup" + if withPublicId == "" { + return nil, errors.New(errors.InvalidPublicId, op, "missing public id") + } + a := AllocManagedGroup() + a.PublicId = withPublicId + if err := r.reader.LookupByPublicId(ctx, a); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId))) + } + return a, nil +} + +// ListManagedGroups in an auth method and supports WithLimit option. +func (r *Repository) ListManagedGroups(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*ManagedGroup, error) { + const op = "oidc.(Repository).ListManagedGroups" + if withAuthMethodId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method id") + } + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var mgs []*ManagedGroup + err := r.reader.SearchWhere(ctx, &mgs, "auth_method_id = ?", []interface{}{withAuthMethodId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(err, op) + } + return mgs, nil +} + +// DeleteManagedGroup deletes the managed group for the provided id from the +// repository returning a count of the number of records deleted. All options +// are ignored. +func (r *Repository) DeleteManagedGroup(ctx context.Context, scopeId, withPublicId string, opt ...Option) (int, error) { + const op = "oidc.(Repository).DeleteManagedGroup" + if withPublicId == "" { + return db.NoRowsAffected, errors.New(errors.InvalidPublicId, op, "missing public id") + } + if scopeId == "" { + return db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing scope id") + } + mg := AllocManagedGroup() + mg.PublicId = withPublicId + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + metadata := mg.oplog(oplog.OpType_OP_TYPE_DELETE, scopeId) + dMg := mg.Clone() + rowsDeleted, err = w.Delete(ctx, dMg, db.WithOplog(oplogWrapper, metadata)) + if err != nil { + return errors.Wrap(err, op) + } + if rowsDeleted > 1 { + return errors.New(errors.MultipleRecords, op, "more than 1 resource would have been deleted") + } + return nil + }, + ) + + if err != nil { + return db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg(withPublicId)) + } + + return rowsDeleted, nil +} + +// UpdateManagedGroup updates the repository entry for mg.PublicId with the +// values in mg for the fields listed in fieldMaskPaths. It returns a new +// ManagedGroup containing the updated values and a count of the number of +// records updated. mg is not changed. +// +// mg must contain a valid PublicId. Only mg.Name, mg.Description, and mg.Filter +// can be updated. If mg.Name is set to a non-empty string, it must be unique +// within mg.AuthMethodId. +// +// An attribute of a will be set to NULL in the database if the attribute in a +// is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdateManagedGroup(ctx context.Context, scopeId string, mg *ManagedGroup, version uint32, fieldMaskPaths []string, opt ...Option) (*ManagedGroup, int, error) { + const op = "oidc.(Repository).UpdateManagedGroup" + if mg == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing ManagedGroup") + } + if mg.ManagedGroup == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing embedded ManagedGroup") + } + if mg.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(errors.InvalidPublicId, op, "missing public id") + } + if version == 0 { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing version") + } + if scopeId == "" { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing scope id") + } + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(NameField, f): + case strings.EqualFold(DescriptionField, f): + case strings.EqualFold(FilterField, f): + default: + return nil, db.NoRowsAffected, errors.New(errors.InvalidFieldMask, op, f) + } + } + var dbMask, nullFields []string + dbMask, nullFields = dbcommon.BuildUpdatePaths( + map[string]interface{}{ + NameField: mg.Name, + DescriptionField: mg.Description, + FilterField: mg.Filter, + }, + fieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(errors.EmptyFieldMask, op, "missing field mask") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op, errors.WithCode(errors.Encrypt), + errors.WithMsg(("unable to get oplog wrapper"))) + } + + mg = mg.Clone() + + metadata := mg.oplog(oplog.OpType_OP_TYPE_UPDATE, scopeId) + + // TODO/FIXME: if the filter is updated, remove all account/mg associations + + var rowsUpdated int + var returnedManagedGroup *ManagedGroup + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedManagedGroup = mg.Clone() + var err error + rowsUpdated, err = w.Update(ctx, returnedManagedGroup, dbMask, nullFields, db.WithOplog(oplogWrapper, metadata), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(err, op) + } + if rowsUpdated > 1 { + return errors.New(errors.MultipleRecords, op, "more than 1 resource would have been updated") + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, db.NoRowsAffected, errors.New(errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", mg.Name, mg.PublicId)) + } + return nil, db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg(mg.PublicId)) + } + + return returnedManagedGroup, rowsUpdated, nil +} diff --git a/internal/auth/oidc/repository_managed_group_test.go b/internal/auth/oidc/repository_managed_group_test.go new file mode 100644 index 000000000..811752fec --- /dev/null +++ b/internal/auth/oidc/repository_managed_group_test.go @@ -0,0 +1,972 @@ +package oidc + +import ( + "context" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testFakeFilter = `"/foo" == "bar"` + +func TestRepository_CreateManagedGroup(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.GetPublicId(), ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + tests := []struct { + name string + scopeId string + in *ManagedGroup + opts []Option + want *ManagedGroup + wantIsErr errors.Code + wantErrMsg string + wantErrContains string + }{ + { + name: "nil-ManagedGroup", + scopeId: org.GetPublicId(), + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateManagedGroup: missing ManagedGroup: parameter violation: error #100", + }, + { + name: "nil-embedded-ManagedGroup", + scopeId: org.GetPublicId(), + in: &ManagedGroup{}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateManagedGroup: missing embedded ManagedGroup: parameter violation: error #100", + }, + { + name: "invalid-no-auth-method-id", + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + }, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateManagedGroup: missing auth method id: parameter violation: error #100", + }, + { + name: "invalid-no-filter", + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + }, + }, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateManagedGroup: missing filter: parameter violation: error #100", + }, + { + name: "invalid-public-id-set", + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + PublicId: "mgoidc_OOOOOOOOOO", + }, + }, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateManagedGroup: public id must be empty: parameter violation: error #100", + }, + { + name: "no-scope", + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + }, + }, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateManagedGroup: missing scope id: parameter violation: error #100", + }, + { + name: "valid-no-options", + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + }, + }, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + }, + }, + }, + { + name: "valid-with-name", + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + Name: "test-name-repo", + }, + }, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + Name: "test-name-repo", + }, + }, + }, + { + name: "valid-with-description", + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + Description: ("test-description-repo"), + Name: "myname", + }, + }, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + Description: ("test-description-repo"), + Name: "myname", + }, + }, + }, + { + name: "duplicate-name", + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethod.PublicId, + Filter: testFakeFilter, + Description: ("test-description-repo"), + Name: "myname", + }, + }, + wantIsErr: errors.NotUnique, + wantErrContains: `name "myname" already exists`, + }, + } + + 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, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.CreateManagedGroup(context.Background(), tt.scopeId, tt.in, tt.opts...) + if tt.wantIsErr != 0 { + require.Error(err) + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err) + if tt.wantErrContains != "" { + assert.True(strings.Contains(err.Error(), tt.wantErrContains)) + } else { + assert.Equal(tt.wantErrMsg, err.Error()) + } + return + } + require.NoError(err) + assert.Empty(tt.in.PublicId) + require.NotNil(got) + assertPublicId(t, ManagedGroupPrefix, 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) + + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } +} + +func TestRepository_LookupManagedGroup(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + mg := TestManagedGroup(t, conn, authMethod, testFakeFilter) + + newMgId, err := newManagedGroupId() + require.NoError(t, err) + tests := []struct { + name string + in string + want *ManagedGroup + wantIsErr errors.Code + wantErrMsg string + }{ + { + name: "With no public id", + wantIsErr: errors.InvalidPublicId, + wantErrMsg: "oidc.(Repository).LookupManagedGroup: missing public id: parameter violation: error #102", + }, + { + name: "With non existing mg id", + in: newMgId, + }, + { + name: "With existing mg id", + in: mg.GetPublicId(), + want: mg, + }, + } + + 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, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.LookupManagedGroup(context.Background(), tt.in) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err) + assert.Equal(tt.wantErrMsg, err.Error()) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +} + +func TestRepository_DeleteManagedGroup(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + mg := TestManagedGroup(t, conn, authMethod, testFakeFilter) + newMgId, err := newManagedGroupId() + require.NoError(t, err) + tests := []struct { + name string + scopeId string + in string + want int + wantIsErr errors.Code + wantErrMsg string + }{ + { + name: "With no scope id", + scopeId: "", + in: mg.GetPublicId(), + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).DeleteManagedGroup: missing scope id: parameter violation: error #100", + }, + { + name: "With no public id", + scopeId: org.GetPublicId(), + wantIsErr: errors.InvalidPublicId, + wantErrMsg: "oidc.(Repository).DeleteManagedGroup: missing public id: parameter violation: error #102", + }, + { + name: "With non existing managed group id", + scopeId: org.GetPublicId(), + in: newMgId, + want: 0, + }, + { + name: "With existing managed group id", + scopeId: org.GetPublicId(), + in: mg.GetPublicId(), + want: 1, + }, + } + + 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, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.DeleteManagedGroup(context.Background(), tt.scopeId, tt.in) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err) + assert.Equal(tt.wantErrMsg, err.Error()) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +} + +func TestRepository_ListManagedGroups(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + authMethod1 := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice1.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + authMethod2 := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice2.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + authMethod3 := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice3.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + mgs1 := []*ManagedGroup{ + TestManagedGroup(t, conn, authMethod1, testFakeFilter), + TestManagedGroup(t, conn, authMethod1, testFakeFilter), + TestManagedGroup(t, conn, authMethod1, testFakeFilter), + } + sort.Slice(mgs1, func(i, j int) bool { + return strings.Compare(mgs1[i].PublicId, mgs1[j].PublicId) < 0 + }) + + mgs2 := []*ManagedGroup{ + TestManagedGroup(t, conn, authMethod2, testFakeFilter), + TestManagedGroup(t, conn, authMethod2, testFakeFilter), + TestManagedGroup(t, conn, authMethod2, testFakeFilter), + } + sort.Slice(mgs2, func(i, j int) bool { + return strings.Compare(mgs2[i].PublicId, mgs2[j].PublicId) < 0 + }) + + tests := []struct { + name string + in string + opts []Option + want []*ManagedGroup + wantIsErr errors.Code + wantErrMsg string + }{ + { + name: "With no auth method id", + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).ListManagedGroups: missing auth method id: parameter violation: error #100", + }, + { + name: "With no managed groups", + in: authMethod3.GetPublicId(), + want: []*ManagedGroup{}, + }, + { + name: "With first auth method id", + in: authMethod1.GetPublicId(), + want: mgs1, + }, + { + name: "With first auth method id", + in: authMethod2.GetPublicId(), + want: mgs2, + }, + } + + 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, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListManagedGroups(context.Background(), tt.in, tt.opts...) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err) + assert.Equal(tt.wantErrMsg, err.Error()) + return + } + require.NoError(err) + + sort.Slice(got, func(i, j int) bool { + return strings.Compare(got[i].PublicId, got[j].PublicId) < 0 + }) + + assert.EqualValues(tt.want, got) + }) + } +} + +func TestRepository_ListManagedGroups_Limits(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + am := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice1.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + mgCount := 10 + for i := 0; i < mgCount; i++ { + TestManagedGroup(t, conn, am, fmt.Sprintf(`"/foo/%d" == "bar"`, i)) + } + + tests := []struct { + name string + repoOpts []Option + listOpts []Option + wantLen int + }{ + { + name: "With no limits", + wantLen: mgCount, + }, + { + name: "With repo limit", + repoOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "With negative repo limit", + repoOpts: []Option{WithLimit(-1)}, + wantLen: mgCount, + }, + { + name: "With List limit", + listOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "With negative List limit", + listOpts: []Option{WithLimit(-1)}, + wantLen: mgCount, + }, + { + name: "With repo smaller than list limit", + repoOpts: []Option{WithLimit(2)}, + listOpts: []Option{WithLimit(6)}, + wantLen: 6, + }, + { + name: "With repo larger than list limit", + repoOpts: []Option{WithLimit(6)}, + listOpts: []Option{WithLimit(2)}, + wantLen: 2, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache, tt.repoOpts...) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListManagedGroups(context.Background(), am.GetPublicId(), tt.listOpts...) + require.NoError(err) + assert.Len(got, tt.wantLen) + }) + } +} + +func TestRepository_UpdateManagedGroup(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + changeName := func(s string) func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.Name = s + return mg + } + } + + changeDescription := func(s string) func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.Description = s + return mg + } + } + + changeFilter := func(s string) func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.Filter = s + return mg + } + } + + makeNil := func() func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + return nil + } + } + + makeEmbeddedNil := func() func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + return &ManagedGroup{} + } + } + + deletePublicId := func() func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.PublicId = "" + return mg + } + } + + nonExistentPublicId := func() func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.PublicId = "abcd_OOOOOOOOOO" + return mg + } + } + + combine := func(fns ...func(mg *ManagedGroup) *ManagedGroup) func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + for _, fn := range fns { + mg = fn(mg) + } + return mg + } + } + + tests := []struct { + name string + scopeId string + version uint32 + orig *ManagedGroup + chgFn func(*ManagedGroup) *ManagedGroup + masks []string + want *ManagedGroup + wantCount int + wantIsErr errors.Code + wantErrMsg string + wantErrContains string + }{ + { + name: "nil-ManagedGroup", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + }, + chgFn: makeNil(), + masks: []string{NameField, DescriptionField}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: missing ManagedGroup: parameter violation: error #100", + }, + { + name: "nil-embedded-ManagedGroup", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + }, + chgFn: makeEmbeddedNil(), + masks: []string{NameField, DescriptionField}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: missing embedded ManagedGroup: parameter violation: error #100", + }, + { + name: "no-scope-id", + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "no-scope-id-test-name-repo", + }, + }, + chgFn: changeName("no-scope-id-test-update-name-repo"), + masks: []string{NameField}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: missing scope id: parameter violation: error #100", + }, + { + name: "missing-version", + scopeId: org.GetPublicId(), + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "missing-version-test-name-repo", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{NameField}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: missing version: parameter violation: error #100", + }, + { + name: "no-public-id", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + }, + chgFn: deletePublicId(), + masks: []string{NameField, DescriptionField}, + wantIsErr: errors.InvalidPublicId, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: missing public id: parameter violation: error #102", + }, + { + name: "updating-non-existent-ManagedGroup", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "updating-non-existent-ManagedGroup-test-name-repo", + }, + }, + chgFn: combine(nonExistentPublicId(), changeName("updating-non-existent-ManagedGroup-test-update-name-repo")), + masks: []string{NameField}, + wantIsErr: errors.RecordNotFound, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: abcd_OOOOOOOOOO: db.DoTx: oidc.(Repository).UpdateManagedGroup: db.Update: db.lookupAfterWrite: db.LookupById: record not found, search issue: error #1100", + }, + { + name: "empty-field-mask", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "empty-field-mask-test-name-repo", + }, + }, + chgFn: changeName("empty-field-mask-test-update-name-repo"), + wantIsErr: errors.EmptyFieldMask, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: missing field mask: parameter violation: error #104", + }, + { + name: "read-only-fields-in-field-mask", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "read-only-fields-in-field-mask-test-name-repo", + }, + }, + chgFn: changeName("read-only-fields-in-field-mask-test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "AuthMethodId"}, + wantIsErr: errors.InvalidFieldMask, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: PublicId: parameter violation: error #103", + }, + { + name: "unknown-field-in-field-mask", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "unknown-field-in-field-mask-test-name-repo", + }, + }, + chgFn: changeName("unknown-field-in-field-mask-test-update-name-repo"), + masks: []string{"Bilbo"}, + wantIsErr: errors.InvalidFieldMask, + wantErrMsg: "oidc.(Repository).UpdateManagedGroup: Bilbo: parameter violation: error #103", + }, + { + name: "change-name", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "change-name-test-name-repo", + }, + }, + chgFn: changeName("change-name-test-update-name-repo"), + masks: []string{NameField}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "change-name-test-update-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-description", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Description: "test-description-repo", + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{DescriptionField}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-filter", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Filter: testFakeFilter, + }, + }, + chgFn: changeFilter(`"/zip" == "zap"`), + masks: []string{FilterField}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Filter: `"/zip" == "zap"`, + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "change-name-and-description-test-name-repo", + Description: "test-description-repo", + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("change-name-and-description-test-update-name-repo")), + masks: []string{NameField, DescriptionField}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "change-name-and-description-test-update-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "delete-name-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{NameField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "delete-description-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{DescriptionField}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "delete-description-test-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-filter", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Filter: testFakeFilter, + }, + }, + masks: []string{FilterField}, + chgFn: combine(changeFilter("")), + wantIsErr: errors.NotNull, + wantErrContains: "oidc.(Repository).UpdateManagedGroup: db.Update: filter must not be empty: not null constraint violated: integrity violation: error #1001", + }, + { + name: "do-not-delete-name", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "do-not-delete-name-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{DescriptionField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "do-not-delete-name-test-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "do-not-delete-description-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{NameField}, + chgFn: combine(changeDescription(""), changeName("do-not-delete-description-test-update-name-repo")), + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "do-not-delete-description-test-update-name-repo", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + } + + 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, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + orig := TestManagedGroup(t, conn, am, testFakeFilter, WithName(tt.orig.GetName()), WithDescription(tt.orig.GetDescription())) + + tt.orig.AuthMethodId = am.PublicId + if tt.chgFn != nil { + orig = tt.chgFn(orig) + } + got, gotCount, err := repo.UpdateManagedGroup(context.Background(), tt.scopeId, orig, tt.version, tt.masks) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + if tt.wantErrContains != "" { + assert.True(strings.Contains(err.Error(), tt.wantErrContains)) + } else { + assert.Equal(tt.wantErrMsg, err.Error()) + } + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + assert.NoError(err) + assert.Empty(tt.orig.PublicId) + if tt.wantCount == 0 { + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + require.NotNil(got) + assertPublicId(t, ManagedGroupPrefix, got.PublicId) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.NotSame(tt.orig, got) + assert.Equal(tt.orig.AuthMethodId, got.AuthMethodId) + dbassert := dbassert.New(t, conn.DB()) + if tt.want.Name == "" { + dbassert.IsNull(got, "name") + return + } + assert.Equal(tt.want.Name, got.Name) + if tt.want.Description == "" { + dbassert.IsNull(got, "description") + return + } + assert.Equal(tt.want.Description, got.Description) + if tt.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } +} diff --git a/internal/auth/oidc/store/oidc.pb.go b/internal/auth/oidc/store/oidc.pb.go index b588273d6..787296c02 100644 --- a/internal/auth/oidc/store/oidc.pb.go +++ b/internal/auth/oidc/store/oidc.pb.go @@ -829,6 +829,125 @@ func (x *AccountClaimMap) GetCreateTime() *timestamp.Timestamp { return nil } +// ManagedGroup entries provide an OIDC auth method implementation of managed +// groups. +type ManagedGroup struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,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,20,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,30,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 auth_method_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,40,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,50,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,60,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // auth_method_id is the fk to the account's auth method. + // @inject_tag: `gorm:"not_null"` + AuthMethodId string `protobuf:"bytes,70,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty" gorm:"not_null"` + // filter is a go-bexpr filter + // @inject_tag: `gorm:"not_null"` + Filter string `protobuf:"bytes,80,opt,name=filter,proto3" json:"filter,omitempty" gorm:"not_null"` +} + +func (x *ManagedGroup) Reset() { + *x = ManagedGroup{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ManagedGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ManagedGroup) ProtoMessage() {} + +func (x *ManagedGroup) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[7] + 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 ManagedGroup.ProtoReflect.Descriptor instead. +func (*ManagedGroup) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{7} +} + +func (x *ManagedGroup) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *ManagedGroup) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *ManagedGroup) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *ManagedGroup) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ManagedGroup) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ManagedGroup) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ManagedGroup) GetAuthMethodId() string { + if x != nil { + return x.AuthMethodId + } + return "" +} + +func (x *ManagedGroup) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + var File_controller_storage_auth_oidc_store_v1_oidc_proto protoreflect.FileDescriptor var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ @@ -1012,11 +1131,39 @@ var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ 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, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, - 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, - 0x74, 0x68, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x6d, 0x65, 0x22, 0xbf, 0x03, 0x0a, 0x0c, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, + 0x64, 0x18, 0x0a, 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, 0x14, 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, 0x1e, 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, 0x24, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x3c, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, + 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, + 0x46, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x0c, 0x41, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x42, 0x14, 0xc2, 0xdd, 0x29, 0x10, 0x0a, 0x06, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1031,7 +1178,7 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP() []byte return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescData } -var file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_controller_storage_auth_oidc_store_v1_oidc_proto_goTypes = []interface{}{ (*AuthMethod)(nil), // 0: controller.storage.auth.oidc.store.v1.AuthMethod (*Account)(nil), // 1: controller.storage.auth.oidc.store.v1.Account @@ -1040,23 +1187,26 @@ var file_controller_storage_auth_oidc_store_v1_oidc_proto_goTypes = []interface{ (*Certificate)(nil), // 4: controller.storage.auth.oidc.store.v1.Certificate (*ClaimsScope)(nil), // 5: controller.storage.auth.oidc.store.v1.ClaimsScope (*AccountClaimMap)(nil), // 6: controller.storage.auth.oidc.store.v1.AccountClaimMap - (*timestamp.Timestamp)(nil), // 7: controller.storage.timestamp.v1.Timestamp + (*ManagedGroup)(nil), // 7: controller.storage.auth.oidc.store.v1.ManagedGroup + (*timestamp.Timestamp)(nil), // 8: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_auth_oidc_store_v1_oidc_proto_depIdxs = []int32{ - 7, // 0: controller.storage.auth.oidc.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 1: controller.storage.auth.oidc.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 2: controller.storage.auth.oidc.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 3: controller.storage.auth.oidc.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 4: controller.storage.auth.oidc.store.v1.SigningAlg.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 5: controller.storage.auth.oidc.store.v1.AudClaim.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 6: controller.storage.auth.oidc.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 7: controller.storage.auth.oidc.store.v1.ClaimsScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // 8: controller.storage.auth.oidc.store.v1.AccountClaimMap.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 8, // 0: controller.storage.auth.oidc.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 1: controller.storage.auth.oidc.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 2: controller.storage.auth.oidc.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 3: controller.storage.auth.oidc.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 4: controller.storage.auth.oidc.store.v1.SigningAlg.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 5: controller.storage.auth.oidc.store.v1.AudClaim.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 6: controller.storage.auth.oidc.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 7: controller.storage.auth.oidc.store.v1.ClaimsScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 8: controller.storage.auth.oidc.store.v1.AccountClaimMap.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 9: controller.storage.auth.oidc.store.v1.ManagedGroup.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // 10: controller.storage.auth.oidc.store.v1.ManagedGroup.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_controller_storage_auth_oidc_store_v1_oidc_proto_init() } @@ -1149,6 +1299,18 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_init() { return nil } } + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ManagedGroup); 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{ @@ -1156,7 +1318,7 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/auth/oidc/testing.go b/internal/auth/oidc/testing.go index 62622083a..48699f145 100644 --- a/internal/auth/oidc/testing.go +++ b/internal/auth/oidc/testing.go @@ -176,6 +176,24 @@ func TestAccount(t *testing.T, conn *gorm.DB, am *AuthMethod, subject string, op return a } +// TestManagedGroup creates a test oidc managed group. +func TestManagedGroup(t *testing.T, conn *gorm.DB, am *AuthMethod, filter string, opt ...Option) *ManagedGroup { + t.Helper() + require := require.New(t) + rw := db.New(conn) + ctx := context.Background() + + mg, err := NewManagedGroup(am.PublicId, filter, opt...) + require.NoError(err) + + id, err := newManagedGroupId() + require.NoError(err) + mg.PublicId = id + + require.NoError(rw.Create(ctx, mg)) + return mg +} + // TestConvertToUrls will convert URL string representations to a slice of // *url.URL func TestConvertToUrls(t *testing.T, urls ...string) []*url.URL { diff --git a/internal/auth/subtype.go b/internal/auth/subtype.go index 927ae2d96..5fb193098 100644 --- a/internal/auth/subtype.go +++ b/internal/auth/subtype.go @@ -58,6 +58,18 @@ var ( _ Account = (*password.Account)(nil) ) +type ManagedGroup interface { + GetPublicId() string + GetCreateTime() *timestamp.Timestamp + GetUpdateTime() *timestamp.Timestamp + GetName() string + GetDescription() string + GetAuthMethodId() string + GetVersion() uint32 +} + +var _ ManagedGroup = (*oidc.ManagedGroup)(nil) + // SubtypeFromType converts a string to a Subtype. // returns UnknownSubtype if no Subtype with that name is found. func SubtypeFromType(t string) Subtype { @@ -79,7 +91,8 @@ func SubtypeFromId(id string) Subtype { strings.HasPrefix(strings.TrimSpace(id), password.AccountPrefix): return PasswordSubtype case strings.HasPrefix(strings.TrimSpace(id), oidc.AuthMethodPrefix), - strings.HasPrefix(strings.TrimSpace(id), oidc.AccountPrefix): + strings.HasPrefix(strings.TrimSpace(id), oidc.AccountPrefix), + strings.HasPrefix(strings.TrimSpace(id), oidc.ManagedGroupPrefix): return OidcSubtype } return UnknownSubtype diff --git a/internal/cmd/base/dev.go b/internal/cmd/base/dev.go index 5195c7045..5d5a9dc0c 100644 --- a/internal/cmd/base/dev.go +++ b/internal/cmd/base/dev.go @@ -119,8 +119,10 @@ func (b *Server) CreateDevDatabase(ctx context.Context, opt ...Option) error { return err } - if err := b.CreateDevOidcAuthMethod(ctx); err != nil { - return err + if !opts.withSkipOidcAuthMethodCreation { + if err := b.CreateDevOidcAuthMethod(ctx); err != nil { + return err + } } if opts.withSkipScopesCreation { @@ -225,7 +227,7 @@ func (b *Server) CreateDevOidcAuthMethod(ctx context.Context) error { // re-use it. This is a sort of hacky way to get around the chicken and egg // of the auth method needing to know the discovery URL and the test // provider needing to know the callback URL. - l, err := net.Listen("tcp", fmt.Sprintf("%s:0", b.DevOidcSetup.hostAddr)) + l, err := net.Listen("tcp", net.JoinHostPort(b.DevOidcSetup.hostAddr, "0")) if err != nil { return fmt.Errorf("error finding port for oidc test provider: %w", err) } @@ -233,7 +235,7 @@ func (b *Server) CreateDevOidcAuthMethod(ctx context.Context) error { if err := l.Close(); err != nil { return fmt.Errorf("error closing initial test port: %w", err) } - b.DevOidcSetup.callbackUrl, err = url.Parse(fmt.Sprintf("http://%s:%s", b.DevOidcSetup.hostAddr, b.DevOidcSetup.callbackPort)) + b.DevOidcSetup.callbackUrl, err = url.Parse(fmt.Sprintf("http://%s", net.JoinHostPort(b.DevOidcSetup.hostAddr, b.DevOidcSetup.callbackPort))) if err != nil { return fmt.Errorf("error parsing oidc test provider callback url: %w", err) } diff --git a/internal/cmd/base/option.go b/internal/cmd/base/option.go index 817880371..5fadf9220 100644 --- a/internal/cmd/base/option.go +++ b/internal/cmd/base/option.go @@ -14,15 +14,16 @@ type Option func(*Options) // Options - how Options are represented. type Options struct { - withNoTokenScope bool - withNoTokenValue bool - withSkipDatabaseDestruction bool - withSkipAuthMethodCreation bool - withSkipScopesCreation bool - withSkipHostResourcesCreation bool - withSkipTargetCreation bool - withContainerImage string - withDialect string + withNoTokenScope bool + withNoTokenValue bool + withSkipDatabaseDestruction bool + withSkipAuthMethodCreation bool + withSkipOidcAuthMethodCreation bool + withSkipScopesCreation bool + withSkipHostResourcesCreation bool + withSkipTargetCreation bool + withContainerImage string + withDialect string } func getDefaultOptions() Options { @@ -57,7 +58,7 @@ func WithNoTokenValue() Option { } } -// WithSkipAuthMethodCreation tells the command not to instantiate an auth +// WithSkipAuthMethodCreation tells the command not to instantiate any auth // method on first run. func WithSkipAuthMethodCreation() Option { return func(o *Options) { @@ -65,6 +66,14 @@ func WithSkipAuthMethodCreation() Option { } } +// WithSkipOidcAuthMethodCreation tells the command not to instantiate an OIDC auth +// method on first run, useful in some tests. +func WithSkipOidcAuthMethodCreation() Option { + return func(o *Options) { + o.withSkipOidcAuthMethodCreation = true + } +} + // WithSkipScopesCreation tells the command not to instantiate scopes on first // run. func WithSkipScopesCreation() Option { diff --git a/internal/cmd/commands/server/server_test.go b/internal/cmd/commands/server/server_test.go index 91372adad..0f6f23a51 100644 --- a/internal/cmd/commands/server/server_test.go +++ b/internal/cmd/commands/server/server_test.go @@ -36,8 +36,7 @@ func testServerCommand(t *testing.T, controllerKey string) *Command { require.NoError(err) require.NoError(cmd.SetupKMSes(cmd.UI, parsedKmsConfig)) - opts := base.WithContainerImage("postgres") - err = cmd.CreateDevDatabase(cmd.Context, opts) + err = cmd.CreateDevDatabase(cmd.Context, base.WithContainerImage("postgres"), base.WithSkipOidcAuthMethodCreation()) if err != nil { if cmd.DevDatabaseCleanupFunc != nil { require.NoError(cmd.DevDatabaseCleanupFunc()) diff --git a/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto b/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto index d6234991c..2b83307a4 100644 --- a/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto +++ b/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto @@ -251,4 +251,38 @@ message AccountClaimMap { // The create_time is set by the database. // @inject_tag: `gorm:"default:current_timestamp"` timestamp.v1.Timestamp create_time = 40; +} + +// ManagedGroup entries provide an OIDC auth method implementation of managed +// groups. +message ManagedGroup { + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 20; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 30; + + // name is optional. If set, it must be unique within auth_method_id. + // @inject_tag: `gorm:"default:null"` + string name = 40 [(custom_options.v1.mask_mapping) = { this: "Name" that: "name" }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 50 [(custom_options.v1.mask_mapping) = { this: "Description" that: "description" }]; + + // @inject_tag: `gorm:"default:null"` + uint32 version = 60; + + // auth_method_id is the fk to the account's auth method. + // @inject_tag: `gorm:"not_null"` + string auth_method_id = 70 [(custom_options.v1.mask_mapping) = { this: "AuthMethodId" that: "auth_method_id" }]; + + // filter is a go-bexpr filter + // @inject_tag: `gorm:"not_null"` + string filter = 80 [(custom_options.v1.mask_mapping) = { this: "Filter" that: "filter" }]; } \ No newline at end of file diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index 10326bb4f..9e82686dc 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -304,6 +304,10 @@ type TestControllerOpts struct { // method automatically. DisableAuthMethodCreation bool + // DisableOidcAuthMethodCreation can be set true to disable the built-in + // OIDC listener. Useful for e.g. unix listener tests. + DisableOidcAuthMethodCreation bool + // DisableScopesCreation can be set true to disable creating scopes // automatically. DisableScopesCreation bool @@ -510,8 +514,10 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { if _, _, err := tc.b.CreateInitialPasswordAuthMethod(ctx); err != nil { t.Fatal(err) } - if err := tc.b.CreateDevOidcAuthMethod(ctx); err != nil { - t.Fatal(err) + if !opts.DisableOidcAuthMethodCreation { + if err := tc.b.CreateDevOidcAuthMethod(ctx); err != nil { + t.Fatal(err) + } } if !opts.DisableScopesCreation { if _, _, err := tc.b.CreateInitialScopes(ctx); err != nil { @@ -536,6 +542,9 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { if opts.DisableAuthMethodCreation { createOpts = append(createOpts, base.WithSkipAuthMethodCreation()) } + if opts.DisableOidcAuthMethodCreation { + createOpts = append(createOpts, base.WithSkipOidcAuthMethodCreation()) + } if err := tc.b.CreateDevDatabase(ctx, createOpts...); err != nil { t.Fatal(err) } diff --git a/internal/tests/cluster/unix_listener_test.go b/internal/tests/cluster/unix_listener_test.go index 92d3101ed..d3e5473ce 100644 --- a/internal/tests/cluster/unix_listener_test.go +++ b/internal/tests/cluster/unix_listener_test.go @@ -47,8 +47,9 @@ func TestUnixListener(t *testing.T) { } c1 := controller.NewTestController(t, &controller.TestControllerOpts{ - Config: conf, - Logger: logger.Named("c1"), + Config: conf, + Logger: logger.Named("c1"), + DisableOidcAuthMethodCreation: true, }) defer c1.Shutdown()