From 4363a4084e34ccb994bba310c33ad80d70a6c2f6 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Tue, 1 Jun 2021 14:18:10 -0500 Subject: [PATCH] Add membership setting function (#1259) --- internal/auth/oidc/managed_group.go | 8 +- .../auth/oidc/managed_group_member_account.go | 75 +++++ internal/auth/oidc/options.go | 11 + internal/auth/oidc/options_test.go | 9 + internal/auth/oidc/query.go | 12 +- .../oidc/repository_managed_group_members.go | 216 ++++++++++++++ .../repository_managed_group_members_test.go | 281 ++++++++++++++++++ .../oidc/repository_managed_group_test.go | 42 +-- internal/auth/oidc/service_callback.go | 2 + internal/auth/oidc/store/oidc.pb.go | 160 ++++++++-- internal/errors/code.go | 5 + internal/errors/code_test.go | 5 + internal/errors/info.go | 4 + .../storage/auth/oidc/store/v1/oidc.proto | 16 + 14 files changed, 783 insertions(+), 63 deletions(-) create mode 100644 internal/auth/oidc/managed_group_member_account.go create mode 100644 internal/auth/oidc/repository_managed_group_members.go create mode 100644 internal/auth/oidc/repository_managed_group_members_test.go diff --git a/internal/auth/oidc/managed_group.go b/internal/auth/oidc/managed_group.go index a50479c89..ff74c5733 100644 --- a/internal/auth/oidc/managed_group.go +++ b/internal/auth/oidc/managed_group.go @@ -82,14 +82,14 @@ func (mg *ManagedGroup) SetTableName(n string) { } // oplog will create oplog metadata for the ManagedGroup. -func (c *ManagedGroup) oplog(op oplog.OpType, authMethodScopeId string) oplog.Metadata { +func (mg *ManagedGroup) oplog(op oplog.OpType, authMethodScopeId string) oplog.Metadata { metadata := oplog.Metadata{ - "resource-public-id": []string{c.GetPublicId()}, + "resource-public-id": []string{mg.GetPublicId()}, "resource-type": []string{"oidc managed group"}, "op-type": []string{op.String()}, } - if c.AuthMethodId != "" { - metadata["auth-method-id"] = []string{c.AuthMethodId} + if mg.AuthMethodId != "" { + metadata["auth-method-id"] = []string{mg.AuthMethodId} } if authMethodScopeId != "" { metadata["scope-id"] = []string{authMethodScopeId} diff --git a/internal/auth/oidc/managed_group_member_account.go b/internal/auth/oidc/managed_group_member_account.go new file mode 100644 index 000000000..0f4719d3a --- /dev/null +++ b/internal/auth/oidc/managed_group_member_account.go @@ -0,0 +1,75 @@ +package oidc + +import ( + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +// defaultManagedGroupMemberAccountTableName defines the default table name for a Managed Group +const defaultManagedGroupMemberAccountTableName = "auth_oidc_managed_group_member_account" + +// ManagedGroupMemberAccount contains a mapping between a managed group and a +// member account +type ManagedGroupMemberAccount struct { + *store.ManagedGroupMemberAccount + tableName string +} + +// NewManagedGroupMemberAccount creates a new in memory +// ManagedGroupMemberAccount assigned to a managed group within an OIDC +// AuthMethod. Supported options are withName and withDescription. +func NewManagedGroupMemberAccount(managedGroupId string, memberId string, opt ...Option) (*ManagedGroupMemberAccount, error) { + const op = "oidc.NewManagedGroupMemberAccount" + mg := &ManagedGroupMemberAccount{ + ManagedGroupMemberAccount: &store.ManagedGroupMemberAccount{ + ManagedGroupId: managedGroupId, + MemberId: memberId, + }, + } + if err := mg.validate(op); err != nil { + return nil, err // intentionally not wrapped. + } + + return mg, nil +} + +// validate the ManagedGroupMemberAccount. On success, it will return nil. +func (mg *ManagedGroupMemberAccount) validate(caller errors.Op) error { + if mg.ManagedGroupId == "" { + return errors.New(errors.InvalidParameter, caller, "missing managed group id") + } + if mg.MemberId == "" { + return errors.New(errors.InvalidParameter, caller, "missing member id") + } + + return nil +} + +// AllocManagedGroupMemberAccount makes an empty one in memory +func AllocManagedGroupMemberAccount() *ManagedGroupMemberAccount { + return &ManagedGroupMemberAccount{ + ManagedGroupMemberAccount: &store.ManagedGroupMemberAccount{}, + } +} + +// Clone a ManagedGroupMemberAccount. +func (mg *ManagedGroupMemberAccount) Clone() *ManagedGroupMemberAccount { + cp := proto.Clone(mg.ManagedGroupMemberAccount) + return &ManagedGroupMemberAccount{ + ManagedGroupMemberAccount: cp.(*store.ManagedGroupMemberAccount), + } +} + +// TableName returns the table name. +func (mg *ManagedGroupMemberAccount) TableName() string { + if mg.tableName != "" { + return mg.tableName + } + return defaultManagedGroupMemberAccountTableName +} + +// SetTableName sets the table name. +func (mg *ManagedGroupMemberAccount) SetTableName(n string) { + mg.tableName = n +} diff --git a/internal/auth/oidc/options.go b/internal/auth/oidc/options.go index 87dd578e7..7c2cfb484 100644 --- a/internal/auth/oidc/options.go +++ b/internal/auth/oidc/options.go @@ -3,6 +3,8 @@ package oidc import ( "crypto/x509" "net/url" + + "github.com/hashicorp/boundary/internal/db" ) // getOpts - iterate the inbound Options and return a struct. @@ -42,6 +44,7 @@ type options struct { withIssuer *url.URL withOperationalState AuthMethodState withAccountClaimMap map[string]AccountToClaim + withReader db.Reader } func getDefaultOptions() options { @@ -218,3 +221,11 @@ func WithAccountClaimMap(acm map[string]AccountToClaim) Option { o.withAccountClaimMap = acm } } + +// WithReader provides an option for specifying a reader to use for the +// operation. +func WithReader(reader db.Reader) Option { + return func(o *options) { + o.withReader = reader + } +} diff --git a/internal/auth/oidc/options_test.go b/internal/auth/oidc/options_test.go index 5ea8fbe1c..5dd89eb62 100644 --- a/internal/auth/oidc/options_test.go +++ b/internal/auth/oidc/options_test.go @@ -5,6 +5,7 @@ import ( "net/url" "testing" + "github.com/hashicorp/boundary/internal/db" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -194,4 +195,12 @@ func Test_GetOpts(t *testing.T) { testOpts.withAccountClaimMap = acm assert.Equal(opts, testOpts) }) + t.Run("WithReader", func(t *testing.T) { + assert := assert.New(t) + testOpts := getDefaultOptions() + assert.Nil(testOpts.withReader) + var r db.Reader + opts := getOpts(WithReader(r)) + assert.Equal(r, opts.withReader) + }) } diff --git a/internal/auth/oidc/query.go b/internal/auth/oidc/query.go index 725b6e583..845717c7c 100644 --- a/internal/auth/oidc/query.go +++ b/internal/auth/oidc/query.go @@ -2,14 +2,14 @@ package oidc const ( acctUpsertQuery = ` - insert into auth_oidc_account + insert into auth_oidc_account (%s) - values + values (%s) - on conflict on constraint + on conflict on constraint auth_oidc_account_auth_method_id_issuer_subject_uq - do update set + do update set %s - returning public_id, version - ` + returning public_id, version + ` ) diff --git a/internal/auth/oidc/repository_managed_group_members.go b/internal/auth/oidc/repository_managed_group_members.go new file mode 100644 index 000000000..bdf9b3a93 --- /dev/null +++ b/internal/auth/oidc/repository_managed_group_members.go @@ -0,0 +1,216 @@ +package oidc + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// SetManagedGroupMemberships will set the managed groups for the given account +// ID. If mgs is empty, the set of groups the account belongs to will be +// cleared. It returns the set of managed group IDs. +// +// mgs contains the set of managed groups that matched. It must contain the +// group's version as this is used to ensure consistency between when the filter +// attached to the managed group was run and the point at which we are adding +// the account to the group. +func (r *Repository) SetManagedGroupMemberships(ctx context.Context, am *AuthMethod, acct *Account, mgs []*ManagedGroup, _ ...Option) ([]*ManagedGroupMemberAccount, int, error) { + const op = "oidc.(Repository).SetManagedGroupMemberships" + if am == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing auth method") + } + if am.AuthMethod == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing auth method store") + } + if am.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing auth method id") + } + if am.ScopeId == "" { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing auth method scope id") + } + if acct == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing account") + } + if acct.Account == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing account store") + } + if acct.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing account id") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + newMgPublicIds := make(map[string]bool, len(mgs)) + mgsToUpdate := make([]*ManagedGroup, 0, len(mgs)) + for _, mg := range mgs { + if mg.Version == 0 { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, fmt.Sprintf("missing version for managed group %s", mg.PublicId)) + } + if newMgPublicIds[mg.PublicId] { + // We've already seen this -- could be a duplicate in the incoming + // MGs. We don't want to add it again because the version won't be + // correct, and it's unnecessary. + continue + } + newMgPublicIds[mg.PublicId] = true + mgToUpdate := AllocManagedGroup() + mgToUpdate.PublicId = mg.PublicId + mgToUpdate.AuthMethodId = am.PublicId + mgToUpdate.Version = mg.Version + 1 + mgsToUpdate = append(mgsToUpdate, mgToUpdate) + } + + ticketMg := AllocManagedGroup() + var totalRowsAffected int + var currentMemberships []*ManagedGroupMemberAccount + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + // We need a ticket, which won't be redeemed until all the other + // writes are successful. We can't just use a single ticket because + // we need to write oplog entries for deletes and adds. + mgTicket, err := w.GetTicket(ticketMg) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to get ticket for oidc managed groups")) + } + + msgs := make([]*oplog.Message, 0, len(mgs)+5) + metadata := oplog.Metadata{ + "op-type": []string{oplog.OpType_OP_TYPE_UPDATE.String()}, + "scope-id": []string{am.ScopeId}, + "auth-method-id": []string{am.PublicId}, + "account-id": []string{acct.PublicId}, + } + + // Ensure that none of the filters have changed or will change + // during this operation + for _, mgToUpdate := range mgsToUpdate { + var mgOplogMsg oplog.Message + // mgToUpdate will have come in with an incremented version + // already, but WithVersion needs the current version + prevVersion := mgToUpdate.Version - 1 + rowsUpdated, err := w.Update(ctx, mgToUpdate, []string{"Version"}, nil, db.NewOplogMsg(&mgOplogMsg), db.WithVersion(&prevVersion)) + if err != nil { + return errors.Wrap(err, op) + } + if rowsUpdated != 1 { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("updated oidc managed group and %d rows updated", rowsUpdated)) + } + msgs = append(msgs, &mgOplogMsg) + } + + currentMemberships, err = r.ListManagedGroupMembershipsByMember(ctx, acct.PublicId, WithReader(reader)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to retrieve current managed group memberships before deletion")) + } + + // Figure out which ones to delete and which ones we already have + toDelete := make([]interface{}, 0, len(mgs)) + for _, currMg := range currentMemberships { + currMgId := currMg.ManagedGroupId + if newMgPublicIds[currMgId] { + // We're slated to add it in, but it's already in there, so + // take it out of the new list + delete(newMgPublicIds, currMgId) + } else { + // It's not currently matching a filter, so needs to be deleted + delMg := AllocManagedGroupMemberAccount() + delMg.ManagedGroupId = currMgId + delMg.MemberId = acct.PublicId + toDelete = append(toDelete, delMg) + } + } + + // At this point, anything in toDelete should be deleted, and + // anything left in newMgPublicIds should be added. However, if we + // had no managed group to update, because none were passed in, but + // also none to delete, we return at this point. Nothing will have + // changed and nothing will be changed either. + if len(mgs) == 0 && len(toDelete) == 0 { + return errors.New(errors.GracefullyAborted, op, "nothing to do") + } + + // Start with deletion + if len(toDelete) > 0 { + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) + deleteOplogMsgs := make([]*oplog.Message, 0, len(toDelete)) + rowsDeleted, err := w.DeleteItems(ctx, toDelete, db.NewOplogMsgs(&deleteOplogMsgs)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to delete managed group member accounts")) + } + if rowsDeleted != len(toDelete) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("managed group member accounts deleted %d did not match request for %d", rowsDeleted, len(toDelete))) + } + totalRowsAffected += rowsDeleted + msgs = append(msgs, deleteOplogMsgs...) + } + + // Now do insertion + if len(newMgPublicIds) > 0 { + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) + addOplogMsgs := make([]*oplog.Message, 0, len(newMgPublicIds)) + toAdd := make([]interface{}, 0, len(newMgPublicIds)) + for mgId := range newMgPublicIds { + newMg := AllocManagedGroupMemberAccount() + newMg.ManagedGroupId = mgId + newMg.MemberId = acct.PublicId + toAdd = append(toAdd, newMg) + } + if err := w.CreateItems(ctx, toAdd, db.NewOplogMsgs(&addOplogMsgs)); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to add managed group member accounts")) + } + totalRowsAffected += len(toAdd) + msgs = append(msgs, addOplogMsgs...) + } + + if len(msgs) > 0 { + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, mgTicket, metadata, msgs); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to write oplog")) + } + } + + currentMemberships, err = r.ListManagedGroupMembershipsByMember(ctx, acct.PublicId, WithReader(reader)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to retrieve current managed group memberships after set")) + } + return nil + }) + if err != nil && !errors.Match(errors.T(errors.GracefullyAborted), err) { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + return currentMemberships, totalRowsAffected, nil +} + +// ListManagedGroupMembershipsByMember lists managed group memberships via the +// member (account) ID and supports WithLimit option. +func (r *Repository) ListManagedGroupMembershipsByMember(ctx context.Context, withAcctId string, opt ...Option) ([]*ManagedGroupMemberAccount, error) { + const op = "oidc.(Repository).ListManagedGroupMembershipsByMember" + if withAcctId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing account 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 + } + reader := r.reader + if opts.withReader != nil { + reader = opts.withReader + } + var mgs []*ManagedGroupMemberAccount + err := reader.SearchWhere(ctx, &mgs, "member_id = ?", []interface{}{withAcctId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(err, op) + } + return mgs, nil +} diff --git a/internal/auth/oidc/repository_managed_group_members_test.go b/internal/auth/oidc/repository_managed_group_members_test.go new file mode 100644 index 000000000..0062bf5ba --- /dev/null +++ b/internal/auth/oidc/repository_managed_group_members_test.go @@ -0,0 +1,281 @@ +package oidc_test + +import ( + "context" + "math/rand" + "strings" + "testing" + + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/controller" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testFakeManagedGroupFilter = `"/foo" == "bar"` + +func Test_ManagedGroupMemberships(t *testing.T) { + // This tests both managed group membership functions (set/list) as list is + // always called as a return from set and we are validating the values that + // come back against what we expect. + + // This test can be run in parallel; the subtests *cannot*. + t.Parallel() + + // Note: using a test controller here for ease of setup as we need a working + // dev OIDC auth method and associated accounts. This test is not making API + // calls! It's accessing the repo directly via the test controller's + // exposure of the underlying DB primitives. + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + conn := tc.DbConn() + rw := db.New(conn) + + kmsCache := tc.Kms() + iamRepo := tc.IamRepo() + org, _ := iam.TestScopes(t, iamRepo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + repo, err := oidc.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + require.NotNil(t, repo) + + mgs := make([]*oidc.ManagedGroup, 0, 10) + + for i := 0; i < 100; i++ { + mg := oidc.AllocManagedGroup() + mg.AuthMethodId = authMethod.PublicId + mg.Filter = testFakeManagedGroupFilter + got, err := repo.CreateManagedGroup(context.Background(), org.GetPublicId(), mg) + require.NoError(t, err) + mgs = append(mgs, got) + } + + // Fetch valid OIDC accounts. One will be "static" where we will simply + // ensure modifying the groups for the other doesn't affect it; the other + // will be used for testing. + var accts []*oidc.Account + err = rw.SearchWhere(ctx, &accts, "", nil, db.WithLimit(2)) + require.NoError(t, err) + require.Len(t, accts, 2) + staticAccountId := accts[0].PublicId + staticMembershipCount := 20 + accountId := accts[1].PublicId + + account := oidc.AllocAccount() + account.PublicId = accountId + + tests := []struct { + name string + // If true, we will auto populate necesary values into the function + validPrereqs bool + + // Else these can be used for testing + authMethod *oidc.AuthMethod + authMethodId string + account *oidc.Account + accountId string + authMethodScopeId string + wantPreseededMgsCount int + wantMgsCount int + specificMgs []*oidc.ManagedGroup + + wantErr errors.Code + wantErrContains string + }{ + { + name: "nil auth method", + wantErr: errors.InvalidParameter, + wantErrContains: "missing auth method", + }, + { + name: "missing auth method store", + authMethod: &oidc.AuthMethod{}, + wantErr: errors.InvalidParameter, + wantErrContains: "missing auth method store", + }, + { + name: "missing auth method id", + authMethod: &oidc.AuthMethod{AuthMethod: &store.AuthMethod{}}, + wantErr: errors.InvalidParameter, + wantErrContains: "missing auth method id", + }, + { + name: "missing auth method scope id", + authMethod: &oidc.AuthMethod{AuthMethod: &store.AuthMethod{PublicId: authMethod.PublicId}}, + wantErr: errors.InvalidParameter, + wantErrContains: "missing auth method scope id", + }, + { + name: "missing account", + authMethod: authMethod, + wantErr: errors.InvalidParameter, + wantErrContains: "missing account", + }, + { + name: "missing account store", + authMethod: authMethod, + account: &oidc.Account{}, + wantErr: errors.InvalidParameter, + wantErrContains: "missing account store", + }, + { + name: "missing account id", + authMethod: authMethod, + account: &oidc.Account{Account: &store.Account{}}, + wantErr: errors.InvalidParameter, + wantErrContains: "missing account id", + }, + { + name: "valid fixed, static", + validPrereqs: true, + accountId: staticAccountId, + specificMgs: mgs[0:staticMembershipCount], + }, + { + name: "valid fixed", + validPrereqs: true, + specificMgs: mgs[0:20], + }, + { + name: "valid fixed, same values", + validPrereqs: true, + specificMgs: mgs[0:20], + }, + { + name: "valid fixed, new values", + validPrereqs: true, + specificMgs: mgs[20:40], + }, + { + name: "valid none", + validPrereqs: true, + wantMgsCount: 0, + }, + { + name: "valid none, second test, testing gracefully aborting", + validPrereqs: true, + wantMgsCount: 0, + }, + { + name: "valid fixed, prep for random", + validPrereqs: true, + specificMgs: mgs[20:50], + }, + { + name: "valid random", + validPrereqs: true, + wantMgsCount: 30, + }, + { + name: "valid random, second test", + validPrereqs: true, + wantMgsCount: 20, + }, + { + name: "valid with duplicates", + validPrereqs: true, + specificMgs: append(mgs[0:20], mgs[0:20]...), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // We are intentionally carrying things over between tests to be + // more realistic but that means we need correct versions, so update + // them first. + currMgs, err := repo.ListManagedGroups(ctx, authMethod.PublicId) + require.NoError(err) + require.Len(currMgs, 100) + currVersionMap := make(map[string]uint32, len(currMgs)) + for _, currMg := range currMgs { + currVersionMap[currMg.PublicId] = currMg.Version + } + for _, mg := range mgs { + mg.Version = currVersionMap[mg.PublicId] + } + + var mgsToTest []*oidc.ManagedGroup + var finalMgs map[string]*oidc.ManagedGroup + // If we know the inputs are sane, create the test data + if tt.validPrereqs { + tt.authMethod = authMethod + tt.account = oidc.AllocAccount() + tt.account.PublicId = accountId + if tt.accountId != "" { + // This is for the test where we initially populate the + // static account + tt.account.PublicId = tt.accountId + } + mgsToTest = tt.specificMgs + if mgsToTest == nil { + // Select at random + mgsToTest = make([]*oidc.ManagedGroup, tt.wantMgsCount) + for i := 0; i < tt.wantMgsCount; i++ { + mg := mgs[rand.Int()%len(mgs)] + mgsToTest[i] = mg + } + } + finalMgs = make(map[string]*oidc.ManagedGroup) + for _, v := range mgsToTest { + finalMgs[v.PublicId] = v + } + } + + memberships, _, err := repo.SetManagedGroupMemberships(ctx, tt.authMethod, tt.account, mgsToTest) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "Unexpected error %s", err) + if tt.wantErrContains != "" { + assert.True(strings.Contains(err.Error(), tt.wantErrContains)) + } + return + } + + require.NoError(err) + assert.Len(memberships, len(finalMgs)) + + // Ensure the same set was found; all memberships found should have + // been in the finalMgs map, and when they are all removed there + // should be nothing left. + for _, mship := range memberships { + assert.Contains(finalMgs, mship.ManagedGroupId) + delete(finalMgs, mship.ManagedGroupId) + } + assert.Len(finalMgs, 0) + + // Now check that the static account still has the same memberships + memberships, err = repo.ListManagedGroupMembershipsByMember(ctx, staticAccountId) + require.NoError(err) + assert.Len(memberships, staticMembershipCount) + finalMgs = make(map[string]*oidc.ManagedGroup, staticMembershipCount) + for _, mg := range mgs[0:staticMembershipCount] { + finalMgs[mg.PublicId] = mg + } + for _, mship := range memberships { + assert.Contains(finalMgs, mship.ManagedGroupId) + delete(finalMgs, mship.ManagedGroupId) + } + assert.Len(finalMgs, 0) + }) + } +} diff --git a/internal/auth/oidc/repository_managed_group_test.go b/internal/auth/oidc/repository_managed_group_test.go index 811752fec..bc7dc7f36 100644 --- a/internal/auth/oidc/repository_managed_group_test.go +++ b/internal/auth/oidc/repository_managed_group_test.go @@ -19,7 +19,7 @@ import ( "github.com/stretchr/testify/require" ) -const testFakeFilter = `"/foo" == "bar"` +const testFakeManagedGroupFilter = `"/foo" == "bar"` func TestRepository_CreateManagedGroup(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") @@ -91,7 +91,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, PublicId: "mgoidc_OOOOOOOOOO", }, }, @@ -103,7 +103,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, }, }, wantIsErr: errors.InvalidParameter, @@ -115,13 +115,13 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, }, }, want: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, }, }, }, @@ -131,14 +131,14 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, Name: "test-name-repo", }, }, want: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, Name: "test-name-repo", }, }, @@ -149,7 +149,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, Description: ("test-description-repo"), Name: "myname", }, @@ -157,7 +157,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { want: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, Description: ("test-description-repo"), Name: "myname", }, @@ -169,7 +169,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, Description: ("test-description-repo"), Name: "myname", }, @@ -231,7 +231,7 @@ func TestRepository_LookupManagedGroup(t *testing.T) { WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) - mg := TestManagedGroup(t, conn, authMethod, testFakeFilter) + mg := TestManagedGroup(t, conn, authMethod, testFakeManagedGroupFilter) newMgId, err := newManagedGroupId() require.NoError(t, err) @@ -296,7 +296,7 @@ func TestRepository_DeleteManagedGroup(t *testing.T) { WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) - mg := TestManagedGroup(t, conn, authMethod, testFakeFilter) + mg := TestManagedGroup(t, conn, authMethod, testFakeManagedGroupFilter) newMgId, err := newManagedGroupId() require.NoError(t, err) tests := []struct { @@ -387,18 +387,18 @@ func TestRepository_ListManagedGroups(t *testing.T) { 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), + TestManagedGroup(t, conn, authMethod1, testFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod1, testFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod1, testFakeManagedGroupFilter), } 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), + TestManagedGroup(t, conn, authMethod2, testFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod2, testFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod2, testFakeManagedGroupFilter), } sort.Slice(mgs2, func(i, j int) bool { return strings.Compare(mgs2[i].PublicId, mgs2[j].PublicId) < 0 @@ -789,7 +789,7 @@ func TestRepository_UpdateManagedGroup(t *testing.T) { version: 1, orig: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, }, }, chgFn: changeFilter(`"/zip" == "zap"`), @@ -865,7 +865,7 @@ func TestRepository_UpdateManagedGroup(t *testing.T) { version: 1, orig: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ - Filter: testFakeFilter, + Filter: testFakeManagedGroupFilter, }, }, masks: []string{FilterField}, @@ -923,7 +923,7 @@ func TestRepository_UpdateManagedGroup(t *testing.T) { assert.NoError(err) require.NotNil(repo) - orig := TestManagedGroup(t, conn, am, testFakeFilter, WithName(tt.orig.GetName()), WithDescription(tt.orig.GetDescription())) + orig := TestManagedGroup(t, conn, am, testFakeManagedGroupFilter, WithName(tt.orig.GetName()), WithDescription(tt.orig.GetDescription())) tt.orig.AuthMethodId = am.PublicId if tt.chgFn != nil { diff --git a/internal/auth/oidc/service_callback.go b/internal/auth/oidc/service_callback.go index 074c3bf3c..fc9b66827 100644 --- a/internal/auth/oidc/service_callback.go +++ b/internal/auth/oidc/service_callback.go @@ -203,6 +203,8 @@ func Callback( return "", errors.Wrap(err, op) } + // Now we need to check filters and assign managed groups by filter. + // wow, we're getting close. we just need to create a pending token for this // successful authentication process, so it can be retrieved by the polling client // that initialed the authentication attempt. diff --git a/internal/auth/oidc/store/oidc.pb.go b/internal/auth/oidc/store/oidc.pb.go index 787296c02..99b9bf055 100644 --- a/internal/auth/oidc/store/oidc.pb.go +++ b/internal/auth/oidc/store/oidc.pb.go @@ -948,6 +948,77 @@ func (x *ManagedGroup) GetFilter() string { return "" } +// ManagedGroupMemberAccount contains a mapping between a managed group and a +// member account. +type ManagedGroupMemberAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // managed_group_id is the fk to the oidc managed group public id + // @inject_tag: `gorm:"primary_key"` + ManagedGroupId string `protobuf:"bytes,20,opt,name=managed_group_id,json=managedGroupId,proto3" json:"managed_group_id,omitempty" gorm:"primary_key"` + // member_id is the fk to the oidc account public id + // @inject_tag: `gorm:"primary_key"` + MemberId string `protobuf:"bytes,30,opt,name=member_id,json=memberId,proto3" json:"member_id,omitempty" gorm:"primary_key"` +} + +func (x *ManagedGroupMemberAccount) Reset() { + *x = ManagedGroupMemberAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ManagedGroupMemberAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ManagedGroupMemberAccount) ProtoMessage() {} + +func (x *ManagedGroupMemberAccount) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[8] + 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 ManagedGroupMemberAccount.ProtoReflect.Descriptor instead. +func (*ManagedGroupMemberAccount) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{8} +} + +func (x *ManagedGroupMemberAccount) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *ManagedGroupMemberAccount) GetManagedGroupId() string { + if x != nil { + return x.ManagedGroupId + } + return "" +} + +func (x *ManagedGroupMemberAccount) GetMemberId() string { + if x != nil { + return x.MemberId + } + 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{ @@ -1159,11 +1230,22 @@ var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ 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, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xaf, 0x01, 0x0a, 0x19, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x0a, 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, 0x28, 0x0a, 0x10, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x5f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x49, 0x64, 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 ( @@ -1178,35 +1260,37 @@ 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, 8) +var file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes = make([]protoimpl.MessageInfo, 9) 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 - (*SigningAlg)(nil), // 2: controller.storage.auth.oidc.store.v1.SigningAlg - (*AudClaim)(nil), // 3: controller.storage.auth.oidc.store.v1.AudClaim - (*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 - (*ManagedGroup)(nil), // 7: controller.storage.auth.oidc.store.v1.ManagedGroup - (*timestamp.Timestamp)(nil), // 8: controller.storage.timestamp.v1.Timestamp + (*AuthMethod)(nil), // 0: controller.storage.auth.oidc.store.v1.AuthMethod + (*Account)(nil), // 1: controller.storage.auth.oidc.store.v1.Account + (*SigningAlg)(nil), // 2: controller.storage.auth.oidc.store.v1.SigningAlg + (*AudClaim)(nil), // 3: controller.storage.auth.oidc.store.v1.AudClaim + (*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 + (*ManagedGroup)(nil), // 7: controller.storage.auth.oidc.store.v1.ManagedGroup + (*ManagedGroupMemberAccount)(nil), // 8: controller.storage.auth.oidc.store.v1.ManagedGroupMemberAccount + (*timestamp.Timestamp)(nil), // 9: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_auth_oidc_store_v1_oidc_proto_depIdxs = []int32{ - 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 + 9, // 0: controller.storage.auth.oidc.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 1: controller.storage.auth.oidc.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 2: controller.storage.auth.oidc.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 3: controller.storage.auth.oidc.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 4: controller.storage.auth.oidc.store.v1.SigningAlg.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 5: controller.storage.auth.oidc.store.v1.AudClaim.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 6: controller.storage.auth.oidc.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 7: controller.storage.auth.oidc.store.v1.ClaimsScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 8: controller.storage.auth.oidc.store.v1.AccountClaimMap.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 9: controller.storage.auth.oidc.store.v1.ManagedGroup.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 10: controller.storage.auth.oidc.store.v1.ManagedGroup.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // 11: controller.storage.auth.oidc.store.v1.ManagedGroupMemberAccount.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_controller_storage_auth_oidc_store_v1_oidc_proto_init() } @@ -1311,6 +1395,18 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_init() { return nil } } + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ManagedGroupMemberAccount); 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{ @@ -1318,7 +1414,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: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/errors/code.go b/internal/errors/code.go index 89d515f88..b33fbc679 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -79,6 +79,11 @@ const ( MaxRetries Code = 1103 // MaxRetries represent that a db Tx hit max retires allowed Exception Code = 1104 // Exception represent that an underlying db exception was raised VersionMismatch Code = 1105 // VersionMismatch represents the update version and the db version for an entry do not match. + // GracefullyAborted means we intended to abort a transaction but the + // enclosing function should not treat it as an error; we aborted for + // reasons related to the state of the DDL and/or inputs (such as we're + // already in the right state and don't want to end up writing oplogs). + GracefullyAborted Code = 1106 // Migration setup errors are codes 2000-2999 MigrationIntegrity Code = 2000 // MigrationIntegrity represents an error with the generated migration related code diff --git a/internal/errors/code_test.go b/internal/errors/code_test.go index adb33dc62..3198f0a48 100644 --- a/internal/errors/code_test.go +++ b/internal/errors/code_test.go @@ -252,6 +252,11 @@ func TestCode_Both_String_Info(t *testing.T) { c: OidcProviderCallbackError, want: OidcProviderCallbackError, }, + { + name: "GracefullyAborted", + c: GracefullyAborted, + want: GracefullyAborted, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/errors/info.go b/internal/errors/info.go index 5b9351208..e96f141e9 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -196,4 +196,8 @@ var errorCodeInfo = map[Code]Info{ Message: "oidc provider callback error", Kind: External, }, + GracefullyAborted: { + Message: "purposefully aborted without error", + Kind: Other, + }, } 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 2b83307a4..0a771f1fe 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 @@ -285,4 +285,20 @@ message ManagedGroup { // filter is a go-bexpr filter // @inject_tag: `gorm:"not_null"` string filter = 80 [(custom_options.v1.mask_mapping) = { this: "Filter" that: "filter" }]; +} + +// ManagedGroupMemberAccount contains a mapping between a managed group and a +// member account. +message ManagedGroupMemberAccount { + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 10; + + // managed_group_id is the fk to the oidc managed group public id + // @inject_tag: `gorm:"primary_key"` + string managed_group_id = 20; + + // member_id is the fk to the oidc account public id + // @inject_tag: `gorm:"primary_key"` + string member_id = 30; } \ No newline at end of file