Managed group list pagination (#4208)

* wip

* managed groups pagination

* address PR comments

* missed one change
This commit is contained in:
Michael Milton authored and Johan Brandhorst-Satzkorn committed 2024-01-05 07:43:21 -08:00
1 parent f49ba4662c
commit 52b1c1b4c7
31 files changed
+4059 -278

No files matched your search

+11
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"github.com/hashicorp/boundary/internal/auth/ldap/store"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/resource"
@@ -110,3 +111,13 @@ func (mg *ManagedGroup) oplog(ctx context.Context, opType oplog.OpType, authMeth
}
return metadata, nil
}
type deletedManagedGroup struct {
PublicId string `gorm:"primary_key"`
DeleteTime *timestamp.Timestamp
}
// TableName returns the tablename to override the default gorm table name
func (s *deletedManagedGroup) TableName() string {
return "auth_ldap_managed_group_deleted"
}
+3
View File
@@ -6,5 +6,8 @@ package ldap
const (
estimateCountAccounts = `
select sum(reltuples::bigint) as estimate from pg_class where oid in ('auth_ldap_account'::regclass)
`
estimateCountManagedGroups = `
select sum(reltuples::bigint) as estimate from pg_class where oid in ('auth_ldap_managed_group'::regclass)
`
)
+130 -9
View File
@@ -5,10 +5,13 @@ package ldap
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
@@ -97,27 +100,103 @@ func (r *Repository) LookupManagedGroup(ctx context.Context, withPublicId string
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) {
// ListManagedGroups returns a slice of managed groups in the auth method.
// Supported options:
// - WithLimit which overrides the limit set in the Repository object
// - WithStartPageAfterItem which sets where to start listing from
func (r *Repository) ListManagedGroups(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*ManagedGroup, time.Time, error) {
const op = "ldap.(Repository).ListManagedGroups"
if withAuthMethodId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
}
opts, err := getOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
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 = ?", []any{withAuthMethodId}, db.WithLimit(limit))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
var args []any
whereClause := "auth_method_id = @auth_method_id"
args = append(args, sql.Named("auth_method_id", withAuthMethodId))
if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(create_time, public_id) < (@last_item_create_time, @last_item_id) and %s", whereClause)
args = append(args,
sql.Named("last_item_create_time", opts.withStartPageAfterItem.GetCreateTime()),
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
return mgs, nil
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("create_time desc, public_id desc")}
return r.queryManagedGroups(ctx, whereClause, args, dbOpts...)
}
// ListManagedGroupsRefresh returns a slice of managed groups in the auth method.
// Supported options:
// - WithLimit which overrides the limit set in the Repository object
// - WithStartPageAfterItem which sets where to start listing from
func (r *Repository) ListManagedGroupsRefresh(ctx context.Context, withAuthMethodId string, updatedAfter time.Time, opt ...Option) ([]*ManagedGroup, time.Time, error) {
const op = "ldap.(Repository).ListManagedGroupsRefresh"
switch {
case withAuthMethodId == "":
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
case updatedAfter.IsZero():
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updated after time")
}
opts, err := getOpts(opt...)
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
limit := r.defaultLimit
if opts.withLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
}
var args []any
whereClause := "update_time > @updated_after_time and auth_method_id = @auth_method_id"
args = append(args,
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
sql.Named("auth_method_id", withAuthMethodId),
)
if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(update_time, public_id) < (@last_item_update_time, @last_item_id) and %s", whereClause)
args = append(args,
sql.Named("last_item_update_time", opts.withStartPageAfterItem.GetUpdateTime()),
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("update_time desc, public_id desc")}
return r.queryManagedGroups(ctx, whereClause, args, dbOpts...)
}
func (r *Repository) queryManagedGroups(ctx context.Context, whereClause string, args []any, opt ...db.Option) ([]*ManagedGroup, time.Time, error) {
const op = "ldap.(Repository).queryManagedGroups"
var mgs []*ManagedGroup
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error {
var inMgs []*ManagedGroup
if err := rd.SearchWhere(ctx, &inMgs, whereClause, args, opt...); err != nil {
return errors.Wrap(ctx, err, op)
}
mgs = inMgs
var err error
transactionTimestamp, err = rd.Now(ctx)
return err
}); err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
return mgs, transactionTimestamp, nil
}
// DeleteManagedGroup deletes the managed group for the provided id from the
@@ -262,3 +341,45 @@ func (r *Repository) UpdateManagedGroup(ctx context.Context, scopeId string, mg
return returnedManagedGroup, rowsUpdated, nil
}
// listDeletedManagedGroupIds lists the public IDs of any managed groups deleted since the timestamp provided,
// and the timestamp of the transaction within which the managed groups were listed.
func (r *Repository) listDeletedManagedGroupIds(ctx context.Context, since time.Time) ([]string, time.Time, error) {
const op = "ldap.(Repository).listDeletedManagedGroupIds"
var deletedManagedGroups []*deletedManagedGroup
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, _ db.Writer) error {
if err := r.SearchWhere(ctx, &deletedManagedGroups, "delete_time >= ?", []any{since}); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted managed groups"))
}
var err error
transactionTimestamp, err = r.Now(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to get transaction timestamp"))
}
return nil
}); err != nil {
return nil, time.Time{}, err
}
var managedGroupIds []string
for _, a := range deletedManagedGroups {
managedGroupIds = append(managedGroupIds, a.PublicId)
}
return managedGroupIds, transactionTimestamp, nil
}
// estimatedManagedGroupCount returns an estimate of the total number of managed groups.
func (r *Repository) estimatedManagedGroupCount(ctx context.Context) (int, error) {
const op = "ldap.(Repository).estimatedManagedGroupCount"
rows, err := r.reader.Query(ctx, estimateCountManagedGroups, nil)
if err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query ldap managed group counts"))
}
var count int
for rows.Next() {
if err := r.reader.ScanRows(ctx, rows, &count); err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query ldap managed group counts"))
}
}
return count, nil
}
@@ -8,6 +8,7 @@ import (
"encoding/json"
"math/rand"
"testing"
"time"
"github.com/hashicorp/boundary/internal/auth/ldap"
"github.com/hashicorp/boundary/internal/auth/oidc/store"
@@ -124,8 +125,10 @@ func Test_ManagedGroupMemberships(t *testing.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(testCtx, testAuthMethod.PublicId)
currMgs, ttime, err := repo.ListManagedGroups(testCtx, testAuthMethod.PublicId)
require.NoError(err)
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
require.Len(currMgs, 101)
currVersionMap := make(map[string]uint32, len(currMgs))
for _, currMg := range currMgs {
@@ -6,23 +6,27 @@ package ldap
import (
"context"
"fmt"
"sort"
"slices"
"strings"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/ldap/store"
"github.com/hashicorp/boundary/internal/db"
dbassert "github.com/hashicorp/boundary/internal/db/assert"
"github.com/hashicorp/boundary/internal/db/timestamp"
"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"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestRepository_CreateManagedGroup(t *testing.T) {
@@ -593,7 +597,11 @@ func TestRepository_DeleteManagedGroup(t *testing.T) {
}
func TestRepository_ListManagedGroups(t *testing.T) {
t.Parallel()
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testRootWrapper := db.TestWrapper(t)
@@ -614,18 +622,24 @@ func TestRepository_ListManagedGroups(t *testing.T) {
TestManagedGroup(t, testConn, authMethod1, testGrpNames),
TestManagedGroup(t, testConn, authMethod1, testGrpNames),
}
sort.Slice(mgs1, func(i, j int) bool {
return strings.Compare(mgs1[i].PublicId, mgs1[j].PublicId) < 0
})
mgs2 := []*ManagedGroup{
TestManagedGroup(t, testConn, authMethod2, testGrpNames),
TestManagedGroup(t, testConn, authMethod2, testGrpNames),
TestManagedGroup(t, testConn, authMethod2, testGrpNames),
}
sort.Slice(mgs2, func(i, j int) bool {
return strings.Compare(mgs2[i].PublicId, mgs2[j].PublicId) < 0
})
slices.Reverse(mgs1)
slices.Reverse(mgs2)
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
ManagedGroup{},
store.ManagedGroup{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
}
testRepo, err := NewRepository(testCtx, testRw, testRw, testKms)
assert.NoError(t, err)
@@ -688,7 +702,7 @@ func TestRepository_ListManagedGroups(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := tc.repo.ListManagedGroups(tc.ctx, tc.in, tc.opts...)
got, ttime, err := tc.repo.ListManagedGroups(tc.ctx, tc.in, tc.opts...)
if tc.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err)
@@ -698,17 +712,43 @@ func TestRepository_ListManagedGroups(t *testing.T) {
return
}
require.NoError(err)
sort.Slice(got, func(i, j int) bool {
return strings.Compare(got[i].PublicId, got[j].PublicId) < 0
})
// Transaction timestamp should be within ~10 seconds of now
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
require.Empty(cmp.Diff(got, tc.want, cmpOpts...))
assert.EqualValues(tc.want, got)
})
}
t.Run("validation", func(t *testing.T) {
t.Parallel()
t.Run("missing auth method id", func(t *testing.T) {
t.Parallel()
_, _, err := testRepo.ListManagedGroups(testCtx, "", WithLimit(testCtx, 1))
require.ErrorContains(t, err, "missing auth method id")
})
})
t.Run("success-without-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := testRepo.ListManagedGroups(testCtx, authMethod1.PublicId, WithLimit(testCtx, 10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1, cmpOpts...))
})
t.Run("success-with-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := testRepo.ListManagedGroups(testCtx, authMethod1.PublicId, WithStartPageAfterItem(testCtx, mgs1[0]), WithLimit(testCtx, 10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1[1:], cmpOpts...))
})
}
func TestRepository_ListManagedGroups_Limits(t *testing.T) {
t.Parallel()
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testRootWrapper := db.TestWrapper(t)
@@ -776,9 +816,11 @@ func TestRepository_ListManagedGroups_Limits(t *testing.T) {
repo, err := NewRepository(testCtx, testRw, testRw, testKms, tc.repoOpts...)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListManagedGroups(context.Background(), am.GetPublicId(), tc.listOpts...)
got, ttime, err := repo.ListManagedGroups(context.Background(), am.GetPublicId(), tc.listOpts...)
require.NoError(err)
assert.Len(got, tc.wantLen)
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
})
}
}
@@ -1225,3 +1267,226 @@ func TestRepository_UpdateManagedGroup(t *testing.T) {
})
}
}
func TestRepository_ListManagedGroupsRefresh(t *testing.T) {
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
testConn, _ := db.TestSetup(t, "postgres")
testRw := db.New(testConn)
testRootWrapper := db.TestWrapper(t)
ctx := context.Background()
testKms := kms.TestKms(t, testConn, testRootWrapper)
iamRepo := iam.TestRepo(t, testConn, testRootWrapper)
org, _ := iam.TestScopes(t, iamRepo)
orgDbWrapper, err := testKms.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
authMethod1 := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1"})
authMethod2 := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap2"})
mgs1 := []*ManagedGroup{
TestManagedGroup(t, testConn, authMethod1, testGrpNames),
TestManagedGroup(t, testConn, authMethod1, testGrpNames),
TestManagedGroup(t, testConn, authMethod1, testGrpNames),
}
mgs2 := []*ManagedGroup{
TestManagedGroup(t, testConn, authMethod2, testGrpNames),
TestManagedGroup(t, testConn, authMethod2, testGrpNames),
TestManagedGroup(t, testConn, authMethod2, testGrpNames),
}
slices.Reverse(mgs1)
slices.Reverse(mgs2)
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
ManagedGroup{},
store.ManagedGroup{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
cmpopts.SortSlices(func(i, j string) bool { return i < j }),
}
repo, err := NewRepository(ctx, testRw, testRw, testKms)
require.NotNil(t, repo)
assert.NoError(t, err)
t.Run("validation", func(t *testing.T) {
t.Parallel()
t.Run("missing updated after", func(t *testing.T) {
t.Parallel()
_, _, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, time.Time{}, WithLimit(ctx, 1))
require.ErrorContains(t, err, "missing updated after time")
})
t.Run("missing auth method id", func(t *testing.T) {
t.Parallel()
_, _, err := repo.ListManagedGroupsRefresh(ctx, "", fiveDaysAgo, WithLimit(ctx, 1))
require.ErrorContains(t, err, "missing auth method id")
})
})
t.Run("success-without-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, fiveDaysAgo, WithLimit(ctx, 10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1, cmpOpts...))
})
t.Run("success-with-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, fiveDaysAgo, WithStartPageAfterItem(ctx, mgs1[0]), WithLimit(ctx, 10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1[1:], cmpOpts...))
})
t.Run("success-without-after-item-recent-updated-after", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, mgs1[len(mgs1)-1].GetUpdateTime().AsTime(), WithLimit(ctx, 10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1[:len(mgs1)-1], cmpOpts...))
})
t.Run("success-with-after-item-recent-updated-after", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, mgs1[len(mgs1)-1].GetUpdateTime().AsTime(), WithStartPageAfterItem(ctx, mgs1[0]), WithLimit(ctx, 10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1[1:len(mgs1)-1], cmpOpts...))
})
}
func TestRepository_estimatedCountManagedGroups(t *testing.T) {
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
testWrapper := db.TestWrapper(t)
ctx := context.Background()
testKms := kms.TestKms(t, conn, testWrapper)
iamRepo := iam.TestRepo(t, conn, testWrapper)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
repo, err := NewRepository(ctx, rw, rw, testKms)
assert.NoError(t, err)
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
// Check total entries at start, expect 0
numItems, err := repo.estimatedManagedGroupCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
// create managed group and check count, expect 1
authMethod1 := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
mg := TestManagedGroup(t, conn, authMethod1, testGrpNames)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
numItems, err = repo.estimatedManagedGroupCount(ctx)
require.NoError(t, err)
assert.Equal(t, 1, numItems)
// Delete managed group and check count, expect 0 again
_, err = repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg.GetPublicId())
require.NoError(t, err)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
numItems, err = repo.estimatedManagedGroupCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
}
func TestRepository_listDeletedIdsManagedGroups(t *testing.T) {
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
testWrapper := db.TestWrapper(t)
ctx := context.Background()
testKms := kms.TestKms(t, conn, testWrapper)
iamRepo := iam.TestRepo(t, conn, testWrapper)
org, _ := iam.TestScopes(t, iamRepo)
databaseWrapper, err := testKms.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
repo, err := NewRepository(ctx, rw, rw, testKms)
assert.NoError(t, err)
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
// Check total entries at start, expect 0
numItems, err := repo.estimatedManagedGroupCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
// create managed group and check count, expect 1
authMethod1 := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"})
mg := TestManagedGroup(t, conn, authMethod1, testGrpNames)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
deletedIds, ttime, err := repo.listDeletedManagedGroupIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(t, err)
require.Empty(t, deletedIds)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
// Delete mg and check count, expect 1 entry
_, err = repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg.GetPublicId())
require.NoError(t, err)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
deletedIds, ttime, err = repo.listDeletedManagedGroupIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(t, err)
assert.Empty(
t,
cmp.Diff(
[]string{mg.GetPublicId()},
deletedIds,
cmpopts.SortSlices(func(i, j string) bool { return i < j }),
),
)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
// Try again with the time set to now, expect no entries
deletedIds, ttime, err = repo.listDeletedManagedGroupIds(ctx, time.Now())
require.NoError(t, err)
require.Empty(t, deletedIds)
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
}
@@ -0,0 +1,62 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap
import (
"context"
"time"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/pagination"
)
// ListManagedGroups lists up to page size ldap managed groups, filtering out entries that
// do not pass the filter item function. It will automatically request
// more managed groups from the database, at page size chunks, to fill the page.
// It returns a new list token used to continue pagination or refresh items.
// Managed groups are ordered by create time descending (most recently created first).
func ListManagedGroups(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[auth.ManagedGroup],
repo *Repository,
authMethodId string,
) (*pagination.ListResponse[auth.ManagedGroup], error) {
const op = "ldap.ListManagedGroups"
switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case filterItemFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case authMethodId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method ID")
}
listItemsFn := func(ctx context.Context, lastPageItem auth.ManagedGroup, limit int) ([]auth.ManagedGroup, time.Time, error) {
opts := []Option{
WithLimit(ctx, limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(ctx, lastPageItem))
}
ldapManagedGroups, listTime, err := repo.ListManagedGroups(ctx, authMethodId, opts...)
if err != nil {
return nil, time.Time{}, err
}
var managedGroups []auth.ManagedGroup
for _, managedGroup := range ldapManagedGroups {
managedGroups = append(managedGroups, managedGroup)
}
return managedGroups, listTime, nil
}
return pagination.List(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedManagedGroupCount)
}
@@ -0,0 +1,626 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap_test
import (
"context"
"slices"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/ldap"
"github.com/hashicorp/boundary/internal/auth/ldap/store"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
var testGrpNames = []string{"test-admin", "test-users"}
func TestService_ListManagedGroups(t *testing.T) {
// Set database read timeout to avoid duplicates in response
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
wrapper := db.TestWrapper(t)
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
rw := db.New(conn)
testKms := kms.TestKms(t, conn, wrapper)
iamRepo := iam.TestRepo(t, conn, wrapper)
org, _ := iam.TestScopes(t, iamRepo)
orgDbWrapper, err := testKms.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
repo, err := ldap.NewRepository(context.Background(), rw, rw, testKms)
require.NoError(t, err)
authMethod := ldap.TestAuthMethod(t, conn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1"})
ldapMgs := []*ldap.ManagedGroup{
ldap.TestManagedGroup(t, conn, authMethod, testGrpNames),
ldap.TestManagedGroup(t, conn, authMethod, testGrpNames),
ldap.TestManagedGroup(t, conn, authMethod, testGrpNames),
ldap.TestManagedGroup(t, conn, authMethod, testGrpNames),
ldap.TestManagedGroup(t, conn, authMethod, testGrpNames),
}
var mgs []auth.ManagedGroup
for _, mg := range ldapMgs {
mgs = append(mgs, mg)
}
// since we sort by create time descending, we need to reverse the slice
slices.Reverse(mgs)
// Run analyze to update host estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
ldap.ManagedGroup{},
store.ManagedGroup{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
}
t.Run("ListManagedGroups validation", func(t *testing.T) {
t.Parallel()
t.Run("missing grants hash", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := ldap.ListManagedGroups(ctx, nil, 1, filterFunc, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := ldap.ListManagedGroups(ctx, []byte("some hash"), 0, filterFunc, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("negative page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := ldap.ListManagedGroups(ctx, []byte("some hash"), -1, filterFunc, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
_, err := ldap.ListManagedGroups(ctx, []byte("some hash"), 1, nil, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := ldap.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, nil, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing auth method ID", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := ldap.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, repo, "")
require.ErrorContains(t, err, "missing auth method ID")
})
})
t.Run("ListManagedGroupsPage validation", func(t *testing.T) {
t.Parallel()
t.Run("missing grants hash", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsPage(ctx, nil, 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("negative page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, nil, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err = ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing token")
})
t.Run("wrong token type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a pagination token component")
})
t.Run("nil repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing auth method ID", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, "")
require.ErrorContains(t, err, "missing auth method ID")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a managed group resource type")
})
})
t.Run("ListManagedGroupsRefresh validation", func(t *testing.T) {
t.Parallel()
t.Run("missing grants hash", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefresh(ctx, nil, 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 0, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("negative page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), -1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, nil, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err = ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, nil, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing token")
})
t.Run("nil repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, nil, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing auth method ID", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, "")
require.ErrorContains(t, err, "missing auth method ID")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a managed group resource type")
})
})
t.Run("ListManagedGroupsRefreshPage validation", func(t *testing.T) {
t.Parallel()
t.Run("missing grants hash", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefreshPage(ctx, nil, 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("negative page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, nil, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err = ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing token")
})
t.Run("wrong token type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a refresh token component")
})
t.Run("nil repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing credential store id", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, "")
require.ErrorContains(t, err, "missing auth method ID")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a managed group resource type")
})
})
t.Run("simple pagination", func(t *testing.T) {
filterFunc := func(context.Context, auth.ManagedGroup) (bool, error) {
return true, nil
}
resp, err := ldap.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.NotNil(t, resp.ListToken)
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp.CompleteListing)
require.Equal(t, resp.EstimatedItemCount, 5)
require.Empty(t, resp.DeletedIds)
require.Len(t, resp.Items, 1)
require.Empty(t, cmp.Diff(resp.Items[0], mgs[0], cmpOpts...))
resp2, err := ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp2.CompleteListing)
require.Equal(t, resp2.EstimatedItemCount, 5)
require.Empty(t, resp2.DeletedIds)
require.Len(t, resp2.Items, 1)
require.Empty(t, cmp.Diff(resp2.Items[0], mgs[1], cmpOpts...))
resp3, err := ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 5)
require.Empty(t, resp3.DeletedIds)
require.Len(t, resp3.Items, 1)
require.Empty(t, cmp.Diff(resp3.Items[0], mgs[2], cmpOpts...))
resp4, err := ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp4.CompleteListing)
require.Equal(t, resp4.EstimatedItemCount, 5)
require.Empty(t, resp4.DeletedIds)
require.Len(t, resp4.Items, 1)
require.Empty(t, cmp.Diff(resp4.Items[0], mgs[3], cmpOpts...))
resp5, err := ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp5.CompleteListing)
require.Equal(t, resp5.EstimatedItemCount, 5)
require.Empty(t, resp5.DeletedIds)
require.Len(t, resp5.Items, 1)
require.Empty(t, cmp.Diff(resp5.Items[0], mgs[4], cmpOpts...))
// Finished initial pagination phase, request refresh
// Expect no results.
resp6, err := ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp5.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp6.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp6.CompleteListing)
require.Equal(t, resp6.EstimatedItemCount, 5)
require.Empty(t, resp6.DeletedIds)
require.Empty(t, resp6.Items)
// Create some new mgs
mg1 := ldap.TestManagedGroup(t, conn, authMethod, testGrpNames)
mg2 := ldap.TestManagedGroup(t, conn, authMethod, testGrpNames)
t.Cleanup(func() {
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg1.GetPublicId())
require.NoError(t, err)
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg2.GetPublicId())
require.NoError(t, err)
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
})
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
// Refresh again, should get mg2
resp7, err := ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp6.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp7.CompleteListing)
require.Equal(t, resp7.EstimatedItemCount, 7)
require.Empty(t, resp7.DeletedIds)
require.Len(t, resp7.Items, 1)
require.Empty(t, cmp.Diff(resp7.Items[0], mg2, cmpOpts...))
// Refresh again, should get mg1
resp8, err := ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp7.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp8.CompleteListing)
require.Equal(t, resp8.EstimatedItemCount, 7)
require.Empty(t, resp8.DeletedIds)
require.Len(t, resp8.Items, 1)
require.Empty(t, cmp.Diff(resp8.Items[0], mg1, cmpOpts...))
// Refresh again, should get no results
resp9, err := ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp8.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp9.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp9.CompleteListing)
require.Equal(t, resp9.EstimatedItemCount, 7)
require.Empty(t, resp9.DeletedIds)
require.Empty(t, resp9.Items)
})
t.Run("simple pagination with aggressive filtering", func(t *testing.T) {
filterFunc := func(ctx context.Context, mg auth.ManagedGroup) (bool, error) {
return mg.GetPublicId() == mgs[1].GetPublicId() ||
mg.GetPublicId() == mgs[len(mgs)-1].GetPublicId(), nil
}
resp, err := ldap.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.NotNil(t, resp.ListToken)
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp.CompleteListing)
require.Equal(t, resp.EstimatedItemCount, 5)
require.Empty(t, resp.DeletedIds)
require.Len(t, resp.Items, 1)
require.Empty(t, cmp.Diff(resp.Items[0], mgs[1], cmpOpts...))
resp2, err := ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.NotNil(t, resp2.ListToken)
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp2.CompleteListing)
require.Equal(t, resp2.EstimatedItemCount, 5)
require.Empty(t, resp2.DeletedIds)
require.Len(t, resp2.Items, 1)
require.Empty(t, cmp.Diff(resp2.Items[0], mgs[len(mgs)-1], cmpOpts...))
// request a refresh, nothing should be returned
resp3, err := ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 5)
require.Empty(t, resp3.DeletedIds)
require.Empty(t, resp3.Items)
// Create some new mgs
mg1 := ldap.TestManagedGroup(t, conn, authMethod, testGrpNames)
mg2 := ldap.TestManagedGroup(t, conn, authMethod, testGrpNames)
mg3 := ldap.TestManagedGroup(t, conn, authMethod, testGrpNames)
t.Cleanup(func() {
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg1.GetPublicId())
require.NoError(t, err)
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg2.GetPublicId())
require.NoError(t, err)
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg3.GetPublicId())
require.NoError(t, err)
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
})
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
filterFunc = func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return mg.GetPublicId() == mg3.GetPublicId() ||
mg.GetPublicId() == mg1.GetPublicId(), nil
}
// Refresh again, should get mg3
resp4, err := ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp4.CompleteListing)
require.Equal(t, resp4.EstimatedItemCount, 8)
require.Empty(t, resp4.DeletedIds)
require.Len(t, resp4.Items, 1)
require.Empty(t, cmp.Diff(resp4.Items[0], mg3, cmpOpts...))
// Refresh again, should get mg1
resp5, err := ldap.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp5.CompleteListing)
require.Equal(t, resp5.EstimatedItemCount, 8)
require.Empty(t, resp5.DeletedIds)
require.Len(t, resp5.Items, 1)
require.Empty(t, cmp.Diff(resp5.Items[0], mg1, cmpOpts...))
})
t.Run("simple pagination with deletion", func(t *testing.T) {
filterFunc := func(context.Context, auth.ManagedGroup) (bool, error) {
return true, nil
}
deletedMGId := mgs[0].GetPublicId()
repo.DeleteManagedGroup(ctx, org.GetPublicId(), deletedMGId)
require.NoError(t, err)
mgs = mgs[1:]
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
resp, err := ldap.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.NotNil(t, resp.ListToken)
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp.CompleteListing)
require.Equal(t, resp.EstimatedItemCount, 4)
require.Empty(t, resp.DeletedIds)
require.Len(t, resp.Items, 1)
require.Empty(t, cmp.Diff(resp.Items[0], mgs[0], cmpOpts...))
// request remaining results
resp2, err := ldap.ListManagedGroupsPage(ctx, []byte("some hash"), 3, filterFunc, resp.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp2.CompleteListing)
require.Equal(t, resp2.EstimatedItemCount, 4)
require.Empty(t, resp2.DeletedIds)
require.Len(t, resp2.Items, 3)
require.Empty(t, cmp.Diff(resp2.Items, mgs[1:], cmpOpts...))
deletedMGId = mgs[0].GetPublicId()
repo.DeleteManagedGroup(ctx, org.GetPublicId(), deletedMGId)
require.NoError(t, err)
mgs = mgs[1:]
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
// request a refresh, nothing should be returned except the deleted id
resp3, err := ldap.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 3)
require.Contains(t, resp3.DeletedIds, deletedMGId)
require.Empty(t, resp3.Items)
})
}
@@ -0,0 +1,79 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap
import (
"context"
"time"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListManagedGroupsPage lists up to page size ldap managed groups, filtering out entries that
// do not pass the filter item function. It will automatically request
// more ldap managed groups from the database, at page size chunks, to fill the page.
// It will start its paging based on the information in the token.
// It returns a new list token used to continue pagination or refresh items.
// Managed groups are ordered by create time descending (most recently created first).
func ListManagedGroupsPage(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[auth.ManagedGroup],
tok *listtoken.Token,
repo *Repository,
authMethodId string,
) (*pagination.ListResponse[auth.ManagedGroup], error) {
const op = "ldap.ListManagedGroupsPage"
switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case filterItemFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
case tok == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case authMethodId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method ID")
case tok.ResourceType != resource.ManagedGroup:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a managed group resource type")
}
if _, ok := tok.Subtype.(*listtoken.PaginationToken); !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a pagination token component")
}
listItemsFn := func(ctx context.Context, lastPageItem auth.ManagedGroup, limit int) ([]auth.ManagedGroup, time.Time, error) {
opts := []Option{
WithLimit(ctx, limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(ctx, lastPageItem))
} else {
lastItem, err := tok.LastItem(ctx)
if err != nil {
return nil, time.Time{}, err
}
opts = append(opts, WithStartPageAfterItem(ctx, lastItem))
}
ldapManagedGroups, listTime, err := repo.ListManagedGroups(ctx, authMethodId, opts...)
if err != nil {
return nil, time.Time{}, err
}
var accts []auth.ManagedGroup
for _, acct := range ldapManagedGroups {
accts = append(accts, acct)
}
return accts, listTime, nil
}
return pagination.ListPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedManagedGroupCount, tok)
}
@@ -0,0 +1,81 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap
import (
"context"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListManagedGroupsRefresh lists ldap managed groups according to the page size
// and list token, filtering out entries that do not
// pass the filter item fn. It returns a new list token
// based on the old one, the grants hash, and the returned
// ldap managed groups.
func ListManagedGroupsRefresh(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[auth.ManagedGroup],
tok *listtoken.Token,
repo *Repository,
authMethodId string,
) (*pagination.ListResponse[auth.ManagedGroup], error) {
const op = "ldap.ListManagedGroupsRefresh"
switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case filterItemFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
case tok == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case authMethodId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method ID")
case tok.ResourceType != resource.ManagedGroup:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a managed group resource type")
}
rt, ok := tok.Subtype.(*listtoken.StartRefreshToken)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a start-refresh token component")
}
listItemsFn := func(ctx context.Context, lastPageItem auth.ManagedGroup, limit int) ([]auth.ManagedGroup, time.Time, error) {
opts := []Option{
WithLimit(ctx, limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(ctx, lastPageItem))
}
// Add the database read timeout to managed group for any creations missed due to concurrent
// transactions in the initial pagination phase.
ldapManagedGroups, listTime, err := repo.ListManagedGroupsRefresh(ctx, authMethodId, rt.PreviousPhaseUpperBound.Add(-globals.RefreshReadLookbackDuration), opts...)
if err != nil {
return nil, time.Time{}, err
}
var managedGroups []auth.ManagedGroup
for _, managedGroup := range ldapManagedGroups {
managedGroups = append(managedGroups, managedGroup)
}
return managedGroups, listTime, nil
}
listDeletedIdsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) {
// Add the database read timeout to managed group for any deletes missed due to concurrent
// transactions in the original list pagination phase.
return repo.listDeletedManagedGroupIds(ctx, since.Add(-globals.RefreshReadLookbackDuration))
}
return pagination.ListRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedManagedGroupCount, listDeletedIdsFn, tok)
}
@@ -0,0 +1,91 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap
import (
"context"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListManagedGroupsRefreshPage lists up to page size managed groups, filtering out entries that
// do not pass the filter item function. It will automatically request
// more managed groups from the database, at page size chunks, to fill the page.
// It will start its paging based on the information in the token.
// It returns a new list token used to continue pagination or refresh items.
// Managed groups are ordered by update time descending (most recently updated first).
// Managed groups may contain items that were already returned during the initial
// pagination phase. It also returns a list of any managed groups deleted since the
// last response.
func ListManagedGroupsRefreshPage(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[auth.ManagedGroup],
tok *listtoken.Token,
repo *Repository,
authMethodId string,
) (*pagination.ListResponse[auth.ManagedGroup], error) {
const op = "ldap.ListManagedGroupsRefreshPage"
switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case filterItemFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
case tok == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case authMethodId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method ID")
case tok.ResourceType != resource.ManagedGroup:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a managed group resource type")
}
rt, ok := tok.Subtype.(*listtoken.RefreshToken)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a refresh token component")
}
listItemsFn := func(ctx context.Context, lastPageItem auth.ManagedGroup, limit int) ([]auth.ManagedGroup, time.Time, error) {
opts := []Option{
WithLimit(ctx, limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(ctx, lastPageItem))
} else {
lastItem, err := tok.LastItem(ctx)
if err != nil {
return nil, time.Time{}, err
}
opts = append(opts, WithStartPageAfterItem(ctx, lastItem))
}
// Add the database read timeout to managed group for any creations missed due to concurrent
// transactions in the original list pagination phase.
ldapManagedGroups, listTime, err := repo.ListManagedGroupsRefresh(ctx, authMethodId, rt.PhaseLowerBound.Add(-globals.RefreshReadLookbackDuration), opts...)
if err != nil {
return nil, time.Time{}, err
}
var managedGroups []auth.ManagedGroup
for _, managedGroup := range ldapManagedGroups {
managedGroups = append(managedGroups, managedGroup)
}
return managedGroups, listTime, nil
}
listDeletedIdsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) {
// Add the database read timeout to managed group for any deletes missed due to concurrent
// transactions in the original list pagination phase.
return repo.listDeletedManagedGroupIds(ctx, since.Add(-globals.RefreshReadLookbackDuration))
}
return pagination.ListRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedManagedGroupCount, listDeletedIdsFn, tok)
}
+11
View File
@@ -7,6 +7,7 @@ import (
"context"
"github.com/hashicorp/boundary/internal/auth/oidc/store"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/resource"
@@ -107,3 +108,13 @@ func (mg *ManagedGroup) oplog(op oplog.OpType, authMethodScopeId string) oplog.M
}
return metadata
}
type deletedManagedGroup struct {
PublicId string `gorm:"primary_key"`
DeleteTime *timestamp.Timestamp
}
// TableName returns the tablename to override the default gorm table name
func (s *deletedManagedGroup) TableName() string {
return "auth_oidc_managed_group_deleted"
}
+31
View File
@@ -7,12 +7,34 @@ import (
"crypto/x509"
"net/url"
"testing"
"time"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeItem struct {
pagination.Item
publicId string
createTime time.Time
updateTime time.Time
}
func (p *fakeItem) GetPublicId() string {
return p.publicId
}
func (p *fakeItem) GetCreateTime() *timestamp.Timestamp {
return timestamp.New(p.createTime)
}
func (p *fakeItem) GetUpdateTime() *timestamp.Timestamp {
return timestamp.New(p.updateTime)
}
// Test_GetOpts provides unit tests for GetOpts and all the options
func Test_GetOpts(t *testing.T) {
t.Parallel()
@@ -206,4 +228,13 @@ func Test_GetOpts(t *testing.T) {
opts := getOpts(WithReader(r))
assert.Equal(r, opts.withReader)
})
t.Run("WithStartPageAfterItem", func(t *testing.T) {
assert := assert.New(t)
updateTime := time.Now()
createTime := time.Now()
opts := getOpts(WithStartPageAfterItem(&fakeItem{nil, "s_1", createTime, updateTime}))
assert.Equal(opts.withStartPageAfterItem.GetPublicId(), "s_1")
assert.Equal(opts.withStartPageAfterItem.GetUpdateTime(), timestamp.New(updateTime))
assert.Equal(opts.withStartPageAfterItem.GetCreateTime(), timestamp.New(createTime))
})
}
+3
View File
@@ -19,4 +19,7 @@ const (
estimateCountAccounts = `
select sum(reltuples::bigint) as estimate from pg_class where oid in ('auth_oidc_account'::regclass)
`
estimateCountManagedGroups = `
select sum(reltuples::bigint) as estimate from pg_class where oid in ('auth_oidc_managed_group'::regclass)
`
)
+126 -8
View File
@@ -5,10 +5,13 @@ package oidc
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
@@ -96,24 +99,97 @@ func (r *Repository) LookupManagedGroup(ctx context.Context, withPublicId string
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) {
// ListManagedGroups returns a slice of managed groups in an auth method
// Supported options:
// - WithLimit which overrides the limit set in the Repository object
// - WithStartPageAfterItem which sets where to start listing from
func (r *Repository) ListManagedGroups(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*ManagedGroup, time.Time, error) {
const op = "oidc.(Repository).ListManagedGroups"
if withAuthMethodId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
return nil, time.Time{}, errors.New(ctx, 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 = ?", []any{withAuthMethodId}, db.WithLimit(limit))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
var args []any
whereClause := "auth_method_id = @auth_method_id"
args = append(args, sql.Named("auth_method_id", withAuthMethodId))
if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(create_time, public_id) < (@last_item_create_time, @last_item_id) and %s", whereClause)
args = append(args,
sql.Named("last_item_create_time", opts.withStartPageAfterItem.GetCreateTime()),
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
return mgs, nil
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("create_time desc, public_id desc")}
return r.queryManagedGroups(ctx, whereClause, args, dbOpts...)
}
// ListManagedGroupsRefresh returns a slice of managed groups in the auth method.
// Supported options:
// - WithLimit which overrides the limit set in the Repository object
// - WithStartPageAfterItem which sets where to start listing from
func (r *Repository) ListManagedGroupsRefresh(ctx context.Context, withAuthMethodId string, updatedAfter time.Time, opt ...Option) ([]*ManagedGroup, time.Time, error) {
const op = "oidc.(Repository).ListManagedGroupsRefresh"
switch {
case withAuthMethodId == "":
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
case updatedAfter.IsZero():
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updated after time")
}
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 args []any
whereClause := "update_time > @updated_after_time and auth_method_id = @auth_method_id"
args = append(args,
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
sql.Named("auth_method_id", withAuthMethodId),
)
if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(update_time, public_id) < (@last_item_update_time, @last_item_id) and %s", whereClause)
args = append(args,
sql.Named("last_item_update_time", opts.withStartPageAfterItem.GetUpdateTime()),
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("update_time desc, public_id desc")}
return r.queryManagedGroups(ctx, whereClause, args, dbOpts...)
}
func (r *Repository) queryManagedGroups(ctx context.Context, whereClause string, args []any, opt ...db.Option) ([]*ManagedGroup, time.Time, error) {
const op = "oidc.(Repository).queryManagedGroups"
var mgs []*ManagedGroup
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error {
var inMgs []*ManagedGroup
if err := rd.SearchWhere(ctx, &inMgs, whereClause, args, opt...); err != nil {
return errors.Wrap(ctx, err, op)
}
mgs = inMgs
var err error
transactionTimestamp, err = rd.Now(ctx)
return err
}); err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
return mgs, transactionTimestamp, nil
}
// DeleteManagedGroup deletes the managed group for the provided id from the
@@ -252,3 +328,45 @@ func (r *Repository) UpdateManagedGroup(ctx context.Context, scopeId string, mg
return returnedManagedGroup, rowsUpdated, nil
}
// listDeletedManagedGroupIds lists the public IDs of any managed groups deleted since the timestamp provided,
// and the timestamp of the transaction within which the managed groups were listed.
func (r *Repository) listDeletedManagedGroupIds(ctx context.Context, since time.Time) ([]string, time.Time, error) {
const op = "oidc.(Repository).listDeletedManagedGroupIds"
var deletedManagedGroups []*deletedManagedGroup
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, _ db.Writer) error {
if err := r.SearchWhere(ctx, &deletedManagedGroups, "delete_time >= ?", []any{since}); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted managed groups"))
}
var err error
transactionTimestamp, err = r.Now(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to get transaction timestamp"))
}
return nil
}); err != nil {
return nil, time.Time{}, err
}
var accountIds []string
for _, a := range deletedManagedGroups {
accountIds = append(accountIds, a.PublicId)
}
return accountIds, transactionTimestamp, nil
}
// estimatedManagedGroupCount returns an estimate of the total number of managed groups.
func (r *Repository) estimatedManagedGroupCount(ctx context.Context) (int, error) {
const op = "oidc.(Repository).estimatedManagedGroupCount"
rows, err := r.reader.Query(ctx, estimateCountManagedGroups, nil)
if err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query ldap managed group counts"))
}
var count int
for rows.Next() {
if err := r.reader.ScanRows(ctx, rows, &count); err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query ldap managed group counts"))
}
}
return count, nil
}
@@ -8,6 +8,7 @@ import (
"math/rand"
"strings"
"testing"
"time"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/oidc/store"
@@ -205,8 +206,11 @@ func Test_ManagedGroupMemberships(t *testing.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)
currMgs, ttime, err := repo.ListManagedGroups(ctx, authMethod.PublicId)
require.NoError(err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
require.Len(currMgs, 100)
currVersionMap := make(map[string]uint32, len(currMgs))
for _, currMg := range currMgs {
@@ -6,21 +6,25 @@ package oidc
import (
"context"
"fmt"
"sort"
"slices"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"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/db/timestamp"
"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"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestRepository_CreateManagedGroup(t *testing.T) {
@@ -356,6 +360,12 @@ func TestRepository_DeleteManagedGroup(t *testing.T) {
}
func TestRepository_ListManagedGroups(t *testing.T) {
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
@@ -367,6 +377,11 @@ func TestRepository_ListManagedGroups(t *testing.T) {
databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
repo, err := NewRepository(ctx, rw, rw, kmsCache)
assert.NoError(t, err)
require.NotNil(t, repo)
authMethod1 := TestAuthMethod(
t, conn, databaseWrapper, org.PublicId, ActivePrivateState,
"alice-rp", "fido",
@@ -393,18 +408,24 @@ func TestRepository_ListManagedGroups(t *testing.T) {
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, 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
})
slices.Reverse(mgs1)
slices.Reverse(mgs2)
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
ManagedGroup{},
store.ManagedGroup{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
}
tests := []struct {
name string
@@ -439,25 +460,48 @@ func TestRepository_ListManagedGroups(t *testing.T) {
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(ctx, rw, rw, kmsCache)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListManagedGroups(context.Background(), tt.in, tt.opts...)
got, ttime, 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())
assert.Truef(t, errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err)
assert.Equal(t, tt.wantErrMsg, err.Error())
return
}
require.NoError(err)
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
sort.Slice(got, func(i, j int) bool {
return strings.Compare(got[i].PublicId, got[j].PublicId) < 0
})
assert.EqualValues(tt.want, got)
assert.EqualValues(t, tt.want, got)
})
}
t.Run("validation", func(t *testing.T) {
t.Parallel()
t.Run("missing auth method id", func(t *testing.T) {
t.Parallel()
_, _, err := repo.ListManagedGroups(ctx, "", WithLimit(1))
require.ErrorContains(t, err, "missing auth method id")
})
})
t.Run("success-without-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroups(ctx, authMethod1.PublicId, WithLimit(10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1, cmpOpts...))
})
t.Run("success-with-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroups(ctx, authMethod1.PublicId, WithStartPageAfterItem(mgs1[0]), WithLimit(10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1[1:], cmpOpts...))
})
}
func TestRepository_ListManagedGroups_Limits(t *testing.T) {
@@ -537,13 +581,132 @@ func TestRepository_ListManagedGroups_Limits(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw, kmsCache, tt.repoOpts...)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListManagedGroups(context.Background(), am.GetPublicId(), tt.listOpts...)
got, ttime, err := repo.ListManagedGroups(context.Background(), am.GetPublicId(), tt.listOpts...)
require.NoError(err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
assert.Len(got, tt.wantLen)
})
}
}
func TestRepository_ListManagedGroupsRefresh(t *testing.T) {
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
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]),
)
mgs1 := []*ManagedGroup{
TestManagedGroup(t, conn, authMethod1, TestFakeManagedGroupFilter),
TestManagedGroup(t, conn, authMethod1, TestFakeManagedGroupFilter),
TestManagedGroup(t, conn, authMethod1, TestFakeManagedGroupFilter),
}
mgs2 := []*ManagedGroup{
TestManagedGroup(t, conn, authMethod2, TestFakeManagedGroupFilter),
TestManagedGroup(t, conn, authMethod2, TestFakeManagedGroupFilter),
TestManagedGroup(t, conn, authMethod2, TestFakeManagedGroupFilter),
}
slices.Reverse(mgs1)
slices.Reverse(mgs2)
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
ManagedGroup{},
store.ManagedGroup{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
cmpopts.SortSlices(func(i, j string) bool { return i < j }),
}
repo, err := NewRepository(ctx, rw, rw, kmsCache)
require.NotNil(t, repo)
assert.NoError(t, err)
t.Run("validation", func(t *testing.T) {
t.Parallel()
t.Run("missing updated after", func(t *testing.T) {
t.Parallel()
_, _, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, time.Time{}, WithLimit(1))
require.ErrorContains(t, err, "missing updated after time")
})
t.Run("missing auth method id", func(t *testing.T) {
t.Parallel()
_, _, err := repo.ListManagedGroupsRefresh(ctx, "", fiveDaysAgo, WithLimit(1))
require.ErrorContains(t, err, "missing auth method id")
})
})
t.Run("success-without-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, fiveDaysAgo, WithLimit(10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1, cmpOpts...))
})
t.Run("success-with-after-item", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, fiveDaysAgo, WithStartPageAfterItem(mgs1[0]), WithLimit(10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1[1:], cmpOpts...))
})
t.Run("success-without-after-item-recent-updated-after", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, mgs1[len(mgs1)-1].GetUpdateTime().AsTime(), WithLimit(10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1[:len(mgs1)-1], cmpOpts...))
})
t.Run("success-with-after-item-recent-updated-after", func(t *testing.T) {
t.Parallel()
resp, ttime, err := repo.ListManagedGroupsRefresh(ctx, authMethod1.PublicId, mgs1[len(mgs1)-1].GetUpdateTime().AsTime(), WithStartPageAfterItem(mgs1[0]), WithLimit(10))
require.NoError(t, err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
require.Empty(t, cmp.Diff(resp, mgs1[1:len(mgs1)-1], cmpOpts...))
})
}
func TestRepository_UpdateManagedGroup(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
@@ -974,3 +1137,135 @@ func TestRepository_UpdateManagedGroup(t *testing.T) {
})
}
}
func TestRepository_estimatedCountManagedGroups(t *testing.T) {
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
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)
repo, err := NewRepository(ctx, rw, rw, kmsCache)
assert.NoError(t, err)
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
// Check total entries at start, expect 0
numItems, err := repo.estimatedManagedGroupCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
// create managed group and check count, expect 1
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]),
)
mg := TestManagedGroup(t, conn, authMethod1, TestFakeManagedGroupFilter)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
numItems, err = repo.estimatedManagedGroupCount(ctx)
require.NoError(t, err)
assert.Equal(t, 1, numItems)
// Delete managed group and check count, expect 0 again
_, err = repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg.GetPublicId())
require.NoError(t, err)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
numItems, err = repo.estimatedManagedGroupCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
}
func TestRepository_listDeletedIdsManagedGroups(t *testing.T) {
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
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)
repo, err := NewRepository(ctx, rw, rw, kmsCache)
assert.NoError(t, err)
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
// Check total entries at start, expect 0
numItems, err := repo.estimatedManagedGroupCount(ctx)
require.NoError(t, err)
assert.Equal(t, 0, numItems)
// create managed group and check count, expect 1
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]),
)
mg := TestManagedGroup(t, conn, authMethod1, TestFakeManagedGroupFilter)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
deletedIds, ttime, err := repo.listDeletedManagedGroupIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(t, err)
require.Empty(t, deletedIds)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
// Delete mg and check count, expect 1 entry
_, err = repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg.GetPublicId())
require.NoError(t, err)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
deletedIds, ttime, err = repo.listDeletedManagedGroupIds(ctx, time.Now().AddDate(-1, 0, 0))
require.NoError(t, err)
assert.Empty(
t,
cmp.Diff(
[]string{mg.GetPublicId()},
deletedIds,
cmpopts.SortSlices(func(i, j string) bool { return i < j }),
),
)
// Transaction timestamp should be within ~10 seconds of now
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
// Try again with the time set to now, expect no entries
deletedIds, ttime, err = repo.listDeletedManagedGroupIds(ctx, time.Now())
require.NoError(t, err)
require.Empty(t, deletedIds)
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
}
+1 -1
View File
@@ -193,7 +193,7 @@ func Callback(
}
// Get the set of all managed groups so we can filter
mgs, err := r.ListManagedGroups(ctx, am.GetPublicId())
mgs, _, err := r.ListManagedGroups(ctx, am.GetPublicId())
if err != nil {
return "", errors.Wrap(ctx, err, op)
}
+4 -1
View File
@@ -819,8 +819,11 @@ func Test_ManagedGroupFiltering(t *testing.T) {
tp.SetExpectedState(state)
// Set the filters on the MGs for this test. First we need to get the current versions.
currMgs, err := repo.ListManagedGroups(ctx, testAuthMethod.PublicId)
currMgs, ttime, err := repo.ListManagedGroups(ctx, testAuthMethod.PublicId)
require.NoError(err)
// Transaction timestamp should be within ~10 seconds of now
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
require.Len(currMgs, 2)
currVersionMap := map[string]uint32{
currMgs[0].PublicId: currMgs[0].Version,
@@ -0,0 +1,62 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oidc
import (
"context"
"time"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/pagination"
)
// ListManagedGroups lists up to page size oidc managed groups, filtering out entries that
// do not pass the filter item function. It will automatically request
// more managed groups from the database, at page size chunks, to fill the page.
// It returns a new list token used to continue pagination or refresh items.
// Managed groups are ordered by create time descending (most recently created first).
func ListManagedGroups(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[auth.ManagedGroup],
repo *Repository,
authMethodId string,
) (*pagination.ListResponse[auth.ManagedGroup], error) {
const op = "oidc.ListManagedGroups"
switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case filterItemFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case authMethodId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method ID")
}
listItemsFn := func(ctx context.Context, lastPageItem auth.ManagedGroup, limit int) ([]auth.ManagedGroup, time.Time, error) {
opts := []Option{
WithLimit(limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(lastPageItem))
}
oidcAccts, listTime, err := repo.ListManagedGroups(ctx, authMethodId, opts...)
if err != nil {
return nil, time.Time{}, err
}
var managedGroups []auth.ManagedGroup
for _, mg := range oidcAccts {
managedGroups = append(managedGroups, mg)
}
return managedGroups, listTime, nil
}
return pagination.List(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedManagedGroupCount)
}
@@ -0,0 +1,632 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oidc_test
import (
"context"
"slices"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"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/db/timestamp"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestService_ListManagedGroups(t *testing.T) {
// Set database read timeout to avoid duplicates in response
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
ctx := context.Background()
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
kmsCache := kms.TestKms(t, conn, wrapper)
iamRepo := iam.TestRepo(t, conn, wrapper)
org, _ := iam.TestScopes(t, iamRepo)
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
repo, err := oidc.NewRepository(ctx, rw, rw, kmsCache)
assert.NoError(t, err)
require.NotNil(t, repo)
authMethod := oidc.TestAuthMethod(
t, conn, databaseWrapper, org.PublicId, oidc.ActivePrivateState,
"alice-rp", "fido",
oidc.WithSigningAlgs(oidc.RS256),
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice1.com")[0]),
oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]),
)
oidcMgs := []*oidc.ManagedGroup{
oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter),
oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter),
oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter),
oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter),
oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter),
}
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(
oidc.ManagedGroup{},
store.ManagedGroup{},
timestamp.Timestamp{},
timestamppb.Timestamp{},
),
}
var mgs []auth.ManagedGroup
for _, mg := range oidcMgs {
mgs = append(mgs, mg)
}
// since we sort by create time descending, we need to reverse the slice
slices.Reverse(mgs)
// Run analyze to update host estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
t.Run("ListManagedGroups validation", func(t *testing.T) {
t.Parallel()
t.Run("missing grants hash", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := oidc.ListManagedGroups(ctx, nil, 1, filterFunc, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := oidc.ListManagedGroups(ctx, []byte("some hash"), 0, filterFunc, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("negative page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := oidc.ListManagedGroups(ctx, []byte("some hash"), -1, filterFunc, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
_, err := oidc.ListManagedGroups(ctx, []byte("some hash"), 1, nil, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := oidc.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, nil, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing auth method ID", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err := oidc.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, repo, "")
require.ErrorContains(t, err, "missing auth method ID")
})
})
t.Run("ListManagedGroupsPage validation", func(t *testing.T) {
t.Parallel()
t.Run("missing grants hash", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsPage(ctx, nil, 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("negative page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, nil, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err = oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing token")
})
t.Run("wrong token type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a pagination token component")
})
t.Run("nil repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing auth method ID", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, "")
require.ErrorContains(t, err, "missing auth method ID")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a managed group resource type")
})
})
t.Run("ListManagedGroupsRefresh validation", func(t *testing.T) {
t.Parallel()
t.Run("missing grants hash", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefresh(ctx, nil, 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 0, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("negative page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), -1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, nil, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err = oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, nil, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing token")
})
t.Run("nil repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, nil, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing auth method ID", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, "")
require.ErrorContains(t, err, "missing auth method ID")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewStartRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a managed group resource type")
})
})
t.Run("ListManagedGroupsRefreshPage validation", func(t *testing.T) {
t.Parallel()
t.Run("missing grants hash", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefreshPage(ctx, nil, 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing grants hash")
})
t.Run("zero page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 0, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("negative page size", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), -1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "page size must be at least 1")
})
t.Run("nil filter func", func(t *testing.T) {
t.Parallel()
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, nil, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing filter item callback")
})
t.Run("nil token", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
_, err = oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, nil, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing token")
})
t.Run("wrong token type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewPagination(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), "some-id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a refresh token component")
})
t.Run("nil repo", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, nil, authMethod.GetPublicId())
require.ErrorContains(t, err, "missing repo")
})
t.Run("missing credential store id", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.ManagedGroup, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, "")
require.ErrorContains(t, err, "missing auth method ID")
})
t.Run("wrong token resource type", func(t *testing.T) {
t.Parallel()
filterFunc := func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return true, nil
}
tok, err := listtoken.NewRefresh(ctx, fiveDaysAgo, resource.Target, []byte("some hash"), fiveDaysAgo, fiveDaysAgo, fiveDaysAgo, "some other id", fiveDaysAgo)
require.NoError(t, err)
_, err = oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, tok, repo, authMethod.GetPublicId())
require.ErrorContains(t, err, "token did not have a managed group resource type")
})
})
t.Run("simple pagination", func(t *testing.T) {
filterFunc := func(context.Context, auth.ManagedGroup) (bool, error) {
return true, nil
}
resp, err := oidc.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.NotNil(t, resp.ListToken)
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp.CompleteListing)
require.Equal(t, resp.EstimatedItemCount, 5)
require.Empty(t, resp.DeletedIds)
require.Len(t, resp.Items, 1)
require.Empty(t, cmp.Diff(resp.Items[0], mgs[0], cmpOpts...))
resp2, err := oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp2.CompleteListing)
require.Equal(t, resp2.EstimatedItemCount, 5)
require.Empty(t, resp2.DeletedIds)
require.Len(t, resp2.Items, 1)
require.Empty(t, cmp.Diff(resp2.Items[0], mgs[1], cmpOpts...))
resp3, err := oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 5)
require.Empty(t, resp3.DeletedIds)
require.Len(t, resp3.Items, 1)
require.Empty(t, cmp.Diff(resp3.Items[0], mgs[2], cmpOpts...))
resp4, err := oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp4.CompleteListing)
require.Equal(t, resp4.EstimatedItemCount, 5)
require.Empty(t, resp4.DeletedIds)
require.Len(t, resp4.Items, 1)
require.Empty(t, cmp.Diff(resp4.Items[0], mgs[3], cmpOpts...))
resp5, err := oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp5.CompleteListing)
require.Equal(t, resp5.EstimatedItemCount, 5)
require.Empty(t, resp5.DeletedIds)
require.Len(t, resp5.Items, 1)
require.Empty(t, cmp.Diff(resp5.Items[0], mgs[4], cmpOpts...))
// Finished initial pagination phase, request refresh
// Expect no results.
resp6, err := oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp5.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp6.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp6.CompleteListing)
require.Equal(t, resp6.EstimatedItemCount, 5)
require.Empty(t, resp6.DeletedIds)
require.Empty(t, resp6.Items)
// Create some new mgs
mg1 := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)
mg2 := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)
t.Cleanup(func() {
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg1.GetPublicId())
require.NoError(t, err)
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg2.GetPublicId())
require.NoError(t, err)
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
})
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
// Refresh again, should get mg2
resp7, err := oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp6.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp7.CompleteListing)
require.Equal(t, resp7.EstimatedItemCount, 7)
require.Empty(t, resp7.DeletedIds)
require.Len(t, resp7.Items, 1)
require.Empty(t, cmp.Diff(resp7.Items[0], mg2, cmpOpts...))
// Refresh again, should get mg1
resp8, err := oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp7.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp8.CompleteListing)
require.Equal(t, resp8.EstimatedItemCount, 7)
require.Empty(t, resp8.DeletedIds)
require.Len(t, resp8.Items, 1)
require.Empty(t, cmp.Diff(resp8.Items[0], mg1, cmpOpts...))
// Refresh again, should get no results
resp9, err := oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp8.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp9.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp9.CompleteListing)
require.Equal(t, resp9.EstimatedItemCount, 7)
require.Empty(t, resp9.DeletedIds)
require.Empty(t, resp9.Items)
})
t.Run("simple pagination with aggressive filtering", func(t *testing.T) {
filterFunc := func(ctx context.Context, mg auth.ManagedGroup) (bool, error) {
return mg.GetPublicId() == mgs[1].GetPublicId() ||
mg.GetPublicId() == mgs[len(mgs)-1].GetPublicId(), nil
}
resp, err := oidc.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.NotNil(t, resp.ListToken)
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp.CompleteListing)
require.Equal(t, resp.EstimatedItemCount, 5)
require.Empty(t, resp.DeletedIds)
require.Len(t, resp.Items, 1)
require.Empty(t, cmp.Diff(resp.Items[0], mgs[1], cmpOpts...))
resp2, err := oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.NotNil(t, resp2.ListToken)
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp2.CompleteListing)
require.Equal(t, resp2.EstimatedItemCount, 5)
require.Empty(t, resp2.DeletedIds)
require.Len(t, resp2.Items, 1)
require.Empty(t, cmp.Diff(resp2.Items[0], mgs[len(mgs)-1], cmpOpts...))
// request a refresh, nothing should be returned
resp3, err := oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 5)
require.Empty(t, resp3.DeletedIds)
require.Empty(t, resp3.Items)
// Create some new mgs
mg1 := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)
mg2 := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)
mg3 := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)
t.Cleanup(func() {
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg1.GetPublicId())
require.NoError(t, err)
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg2.GetPublicId())
require.NoError(t, err)
repo.DeleteManagedGroup(ctx, org.GetPublicId(), mg3.GetPublicId())
require.NoError(t, err)
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
})
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
filterFunc = func(_ context.Context, mg auth.ManagedGroup) (bool, error) {
return mg.GetPublicId() == mg3.GetPublicId() ||
mg.GetPublicId() == mg1.GetPublicId(), nil
}
// Refresh again, should get mg3
resp4, err := oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp3.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp4.CompleteListing)
require.Equal(t, resp4.EstimatedItemCount, 8)
require.Empty(t, resp4.DeletedIds)
require.Len(t, resp4.Items, 1)
require.Empty(t, cmp.Diff(resp4.Items[0], mg3, cmpOpts...))
// Refresh again, should get mg1
resp5, err := oidc.ListManagedGroupsRefreshPage(ctx, []byte("some hash"), 1, filterFunc, resp4.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp5.CompleteListing)
require.Equal(t, resp5.EstimatedItemCount, 8)
require.Empty(t, resp5.DeletedIds)
require.Len(t, resp5.Items, 1)
require.Empty(t, cmp.Diff(resp5.Items[0], mg1, cmpOpts...))
})
t.Run("simple pagination with deletion", func(t *testing.T) {
filterFunc := func(context.Context, auth.ManagedGroup) (bool, error) {
return true, nil
}
deletedMGId := mgs[0].GetPublicId()
repo.DeleteManagedGroup(ctx, org.GetPublicId(), deletedMGId)
require.NoError(t, err)
mgs = mgs[1:]
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
resp, err := oidc.ListManagedGroups(ctx, []byte("some hash"), 1, filterFunc, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.NotNil(t, resp.ListToken)
require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash"))
require.False(t, resp.CompleteListing)
require.Equal(t, resp.EstimatedItemCount, 4)
require.Empty(t, resp.DeletedIds)
require.Len(t, resp.Items, 1)
require.Empty(t, cmp.Diff(resp.Items[0], mgs[0], cmpOpts...))
// request remaining results
resp2, err := oidc.ListManagedGroupsPage(ctx, []byte("some hash"), 3, filterFunc, resp.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp2.CompleteListing)
require.Equal(t, resp2.EstimatedItemCount, 4)
require.Empty(t, resp2.DeletedIds)
require.Len(t, resp2.Items, 3)
require.Empty(t, cmp.Diff(resp2.Items, mgs[1:], cmpOpts...))
deletedMGId = mgs[0].GetPublicId()
repo.DeleteManagedGroup(ctx, org.GetPublicId(), deletedMGId)
require.NoError(t, err)
mgs = mgs[1:]
// Run analyze to update count estimate
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
// request a refresh, nothing should be returned except the deleted id
resp3, err := oidc.ListManagedGroupsRefresh(ctx, []byte("some hash"), 1, filterFunc, resp2.ListToken, repo, authMethod.GetPublicId())
require.NoError(t, err)
require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash"))
require.True(t, resp3.CompleteListing)
require.Equal(t, resp3.EstimatedItemCount, 3)
require.Contains(t, resp3.DeletedIds, deletedMGId)
require.Empty(t, resp3.Items)
})
}
@@ -0,0 +1,79 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oidc
import (
"context"
"time"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListManagedGroupsPage lists up to page size oidc managed groups, filtering out entries that
// do not pass the filter item function. It will automatically request
// more oidc managed groups from the database, at page size chunks, to fill the page.
// It will start its paging based on the information in the token.
// It returns a new list token used to continue pagination or refresh items.
// Managed groups are ordered by create time descending (most recently created first).
func ListManagedGroupsPage(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[auth.ManagedGroup],
tok *listtoken.Token,
repo *Repository,
authMethodId string,
) (*pagination.ListResponse[auth.ManagedGroup], error) {
const op = "oidc.ListManagedGroupsPage"
switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case filterItemFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
case tok == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case authMethodId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method ID")
case tok.ResourceType != resource.ManagedGroup:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a managed group resource type")
}
if _, ok := tok.Subtype.(*listtoken.PaginationToken); !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a pagination token component")
}
listItemsFn := func(ctx context.Context, lastPageItem auth.ManagedGroup, limit int) ([]auth.ManagedGroup, time.Time, error) {
opts := []Option{
WithLimit(limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(lastPageItem))
} else {
lastItem, err := tok.LastItem(ctx)
if err != nil {
return nil, time.Time{}, err
}
opts = append(opts, WithStartPageAfterItem(lastItem))
}
oidcManagedGroups, listTime, err := repo.ListManagedGroups(ctx, authMethodId, opts...)
if err != nil {
return nil, time.Time{}, err
}
var mgs []auth.ManagedGroup
for _, acct := range oidcManagedGroups {
mgs = append(mgs, acct)
}
return mgs, listTime, nil
}
return pagination.ListPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedManagedGroupCount, tok)
}
@@ -0,0 +1,81 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oidc
import (
"context"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListManagedGroupsRefresh lists oidc managed groups according to the page size
// and list token, filtering out entries that do not
// pass the filter item fn. It returns a new list token
// based on the old one, the grants hash, and the returned
// oidc managed groups.
func ListManagedGroupsRefresh(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[auth.ManagedGroup],
tok *listtoken.Token,
repo *Repository,
authMethodId string,
) (*pagination.ListResponse[auth.ManagedGroup], error) {
const op = "oidc.ListManagedGroupsRefresh"
switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case filterItemFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
case tok == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case authMethodId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method ID")
case tok.ResourceType != resource.ManagedGroup:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a managed group resource type")
}
rt, ok := tok.Subtype.(*listtoken.StartRefreshToken)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a start-refresh token component")
}
listItemsFn := func(ctx context.Context, lastPageItem auth.ManagedGroup, limit int) ([]auth.ManagedGroup, time.Time, error) {
opts := []Option{
WithLimit(limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(lastPageItem))
}
// Add the database read timeout to managed group for any creations missed due to concurrent
// transactions in the initial pagination phase.
oidcManagedGroups, listTime, err := repo.ListManagedGroupsRefresh(ctx, authMethodId, rt.PreviousPhaseUpperBound.Add(-globals.RefreshReadLookbackDuration), opts...)
if err != nil {
return nil, time.Time{}, err
}
var managedGroups []auth.ManagedGroup
for _, managedGroup := range oidcManagedGroups {
managedGroups = append(managedGroups, managedGroup)
}
return managedGroups, listTime, nil
}
listDeletedIdsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) {
// Add the database read timeout to managed group for any deletes missed due to concurrent
// transactions in the original list pagination phase.
return repo.listDeletedManagedGroupIds(ctx, since.Add(-globals.RefreshReadLookbackDuration))
}
return pagination.ListRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedManagedGroupCount, listDeletedIdsFn, tok)
}
@@ -0,0 +1,91 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oidc
import (
"context"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/types/resource"
)
// ListManagedGroupsRefreshPage lists up to page size managed groups, filtering out entries that
// do not pass the filter item function. It will automatically request
// more managed groups from the database, at page size chunks, to fill the page.
// It will start its paging based on the information in the token.
// It returns a new list token used to continue pagination or refresh items.
// Managed groups are ordered by update time descending (most recently updated first).
// Managed groups may contain items that were already returned during the initial
// pagination phase. It also returns a list of any managed groups deleted since the
// last response.
func ListManagedGroupsRefreshPage(
ctx context.Context,
grantsHash []byte,
pageSize int,
filterItemFn pagination.ListFilterFunc[auth.ManagedGroup],
tok *listtoken.Token,
repo *Repository,
authMethodId string,
) (*pagination.ListResponse[auth.ManagedGroup], error) {
const op = "oidc.ListManagedGroupsRefreshPage"
switch {
case len(grantsHash) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash")
case pageSize < 1:
return nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1")
case filterItemFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback")
case tok == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token")
case repo == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing repo")
case authMethodId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method ID")
case tok.ResourceType != resource.ManagedGroup:
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a managed group resource type")
}
rt, ok := tok.Subtype.(*listtoken.RefreshToken)
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a refresh token component")
}
listItemsFn := func(ctx context.Context, lastPageItem auth.ManagedGroup, limit int) ([]auth.ManagedGroup, time.Time, error) {
opts := []Option{
WithLimit(limit),
}
if lastPageItem != nil {
opts = append(opts, WithStartPageAfterItem(lastPageItem))
} else {
lastItem, err := tok.LastItem(ctx)
if err != nil {
return nil, time.Time{}, err
}
opts = append(opts, WithStartPageAfterItem(lastItem))
}
// Add the database read timeout to managed group for any creations missed due to concurrent
// transactions in the original list pagination phase.
oidcManagedGroups, listTime, err := repo.ListManagedGroupsRefresh(ctx, authMethodId, rt.PhaseLowerBound.Add(-globals.RefreshReadLookbackDuration), opts...)
if err != nil {
return nil, time.Time{}, err
}
var managedGroups []auth.ManagedGroup
for _, managedGroup := range oidcManagedGroups {
managedGroups = append(managedGroups, managedGroup)
}
return managedGroups, listTime, nil
}
listDeletedIdsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) {
// Add the database read timeout to managed group for any deletes missed due to concurrent
// transactions in the original list pagination phase.
return repo.listDeletedManagedGroupIds(ctx, since.Add(-globals.RefreshReadLookbackDuration))
}
return pagination.ListRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, repo.estimatedManagedGroupCount, listDeletedIdsFn, tok)
}
+1 -1
View File
@@ -279,7 +279,7 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error {
services.RegisterSessionServiceServer(s, ss)
}
if _, ok := currentServices[services.ManagedGroupService_ServiceDesc.ServiceName]; !ok {
mgs, err := managed_groups.NewService(c.baseContext, c.OidcRepoFn, c.LdapRepoFn)
mgs, err := managed_groups.NewService(c.baseContext, c.OidcRepoFn, c.LdapRepoFn, c.conf.RawConfig.Controller.MaxPageSize)
if err != nil {
return fmt.Errorf("failed to create managed groups handler service: %w", err)
}
@@ -19,6 +19,8 @@ import (
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/errors"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/types/action"
@@ -93,14 +95,15 @@ func init() {
type Service struct {
pbs.UnsafeManagedGroupServiceServer
oidcRepoFn common.OidcAuthRepoFactory
ldapRepoFn common.LdapAuthRepoFactory
oidcRepoFn common.OidcAuthRepoFactory
ldapRepoFn common.LdapAuthRepoFactory
maxPageSize uint
}
var _ pbs.ManagedGroupServiceServer = (*Service)(nil)
// NewService returns a managed group service which handles managed group related requests to boundary.
func NewService(ctx context.Context, oidcRepo common.OidcAuthRepoFactory, ldapRepo common.LdapAuthRepoFactory) (Service, error) {
func NewService(ctx context.Context, oidcRepo common.OidcAuthRepoFactory, ldapRepo common.LdapAuthRepoFactory, maxPageSize uint) (Service, error) {
const op = "managed_groups.NewService"
switch {
case oidcRepo == nil:
@@ -108,70 +111,187 @@ func NewService(ctx context.Context, oidcRepo common.OidcAuthRepoFactory, ldapRe
case ldapRepo == nil:
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing ldap repository provided")
}
return Service{oidcRepoFn: oidcRepo, ldapRepoFn: ldapRepo}, nil
if maxPageSize == 0 {
maxPageSize = uint(globals.DefaultMaxPageSize)
}
return Service{oidcRepoFn: oidcRepo, ldapRepoFn: ldapRepo, maxPageSize: maxPageSize}, nil
}
// ListManagedGroups implements the interface pbs.ManagedGroupsServiceServer.
func (s Service) ListManagedGroups(ctx context.Context, req *pbs.ListManagedGroupsRequest) (*pbs.ListManagedGroupsResponse, error) {
const op = "managed_groups.(Service).ListManagedGroups"
if err := validateListRequest(ctx, req); err != nil {
return nil, err
return nil, errors.Wrap(ctx, err, op)
}
_, authResults := s.parentAndAuthResult(ctx, req.GetAuthMethodId(), action.List)
if authResults.Error != nil {
return nil, authResults.Error
}
ul, err := s.listFromRepo(ctx, req.GetAuthMethodId())
authMethodId := req.GetAuthMethodId()
pageSize := int(s.maxPageSize)
// Use the requested page size only if it is smaller than
// the configured max.
if req.GetPageSize() != 0 && uint(req.GetPageSize()) < s.maxPageSize {
pageSize = int(req.GetPageSize())
}
var filterItemFn func(ctx context.Context, item auth.ManagedGroup) (bool, error)
switch {
case req.GetFilter() != "":
// Only use a filter if we need to
filter, err := handlers.NewFilter(ctx, req.GetFilter())
if err != nil {
return nil, err
}
// TODO: replace the need for this function with some way to convert the `filter`
// to a domain type. This would allow filtering to happen in the domain, and we could
// remove this callback altogether.
filterItemFn = func(ctx context.Context, item auth.ManagedGroup) (bool, error) {
outputOpts, ok := newOutputOpts(ctx, item, authMethodId, authResults)
if !ok {
return false, nil
}
pbItem, err := toProto(ctx, item, outputOpts...)
if err != nil {
return false, err
}
filterable, err := subtypes.Filterable(ctx, pbItem)
if err != nil {
return false, err
}
return filter.Match(filterable), nil
}
default:
filterItemFn = func(ctx context.Context, item auth.ManagedGroup) (bool, error) {
return true, nil
}
}
grantsHash, err := authResults.GrantsHash(ctx)
if err != nil {
return nil, err
}
if len(ul) == 0 {
return &pbs.ListManagedGroupsResponse{}, nil
var listResp *pagination.ListResponse[auth.ManagedGroup]
var sortBy string
switch globals.ResourceInfoFromPrefix(authMethodId).Subtype {
case ldap.Subtype:
repo, err := s.ldapRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if req.GetListToken() == "" {
sortBy = "created_time"
listResp, err = ldap.ListManagedGroups(ctx, grantsHash, pageSize, filterItemFn, repo, authMethodId)
if err != nil {
return nil, err
}
} else {
listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.ManagedGroup, grantsHash)
if err != nil {
return nil, err
}
switch st := listToken.Subtype.(type) {
case *listtoken.PaginationToken:
sortBy = "created_time"
listResp, err = ldap.ListManagedGroupsPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, authMethodId)
if err != nil {
return nil, err
}
case *listtoken.StartRefreshToken:
sortBy = "updated_time"
listResp, err = ldap.ListManagedGroupsRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, authMethodId)
if err != nil {
return nil, err
}
case *listtoken.RefreshToken:
sortBy = "updated_time"
listResp, err = ldap.ListManagedGroupsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, authMethodId)
if err != nil {
return nil, err
}
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st)
}
}
case oidc.Subtype:
repo, err := s.oidcRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if req.GetListToken() == "" {
sortBy = "created_time"
listResp, err = oidc.ListManagedGroups(ctx, grantsHash, pageSize, filterItemFn, repo, authMethodId)
if err != nil {
return nil, err
}
} else {
listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.ManagedGroup, grantsHash)
if err != nil {
return nil, err
}
switch st := listToken.Subtype.(type) {
case *listtoken.PaginationToken:
sortBy = "created_time"
listResp, err = oidc.ListManagedGroupsPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, authMethodId)
if err != nil {
return nil, err
}
case *listtoken.StartRefreshToken:
sortBy = "updated_time"
listResp, err = oidc.ListManagedGroupsRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, authMethodId)
if err != nil {
return nil, err
}
case *listtoken.RefreshToken:
sortBy = "updated_time"
listResp, err = oidc.ListManagedGroupsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, authMethodId)
if err != nil {
return nil, err
}
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st)
}
}
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unknown auth method type for id: %s", authMethodId)
}
filter, err := handlers.NewFilter(ctx, req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.ManagedGroup, 0, len(ul))
res := perms.Resource{
ScopeId: authResults.Scope.Id,
Type: resource.ManagedGroup,
Pin: req.GetAuthMethodId(),
}
for _, mg := range ul {
res.Id = mg.GetPublicId()
authorizedActions := authResults.FetchActionSetForId(ctx, mg.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(mg.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings()
if len(authorizedActions) == 0 {
finalItems := make([]*pb.ManagedGroup, 0, len(listResp.Items))
for _, item := range listResp.Items {
outputOpts, ok := newOutputOpts(ctx, item, authMethodId, authResults)
if !ok {
continue
}
outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions))
pbItem, err := toProto(ctx, item, outputOpts...)
if err != nil {
continue
}
finalItems = append(finalItems, pbItem)
}
respType := "delta"
if listResp.CompleteListing {
respType = "complete"
}
resp := &pbs.ListManagedGroupsResponse{
Items: finalItems,
EstItemCount: uint32(listResp.EstimatedItemCount),
RemovedIds: listResp.DeletedIds,
ResponseType: respType,
SortBy: sortBy,
SortDir: "desc",
}
item, err := toProto(ctx, mg, outputOpts...)
if listResp.ListToken != nil {
resp.ListToken, err = handlers.MarshalListToken(ctx, listResp.ListToken, pbs.ResourceType_RESOURCE_TYPE_MANAGED_GROUP)
if err != nil {
return nil, err
}
// This comes last so that we can use item fields in the filter after
// the allowed fields are populated above
filterable, err := subtypes.Filterable(ctx, item)
if err != nil {
return nil, err
}
if filter.Match(filterable) {
finalItems = append(finalItems, item)
}
}
return &pbs.ListManagedGroupsResponse{Items: finalItems}, nil
return resp, nil
}
// GetManagedGroup implements the interface pbs.ManagedGroupServiceServer.
@@ -588,39 +708,6 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool,
return rows > 0, nil
}
func (s Service) listFromRepo(ctx context.Context, authMethodId string) ([]auth.ManagedGroup, error) {
const op = "managed_groups.(Service).listFromRepo"
var outUl []auth.ManagedGroup
switch globals.ResourceInfoFromPrefix(authMethodId).Subtype {
case oidc.Subtype:
oidcRepo, err := s.oidcRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
oidcl, err := oidcRepo.ListManagedGroups(ctx, authMethodId, oidc.WithLimit(-1))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, a := range oidcl {
outUl = append(outUl, a)
}
case ldap.Subtype:
ldapRepo, err := s.ldapRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
ldapUl, err := ldapRepo.ListManagedGroups(ctx, authMethodId, ldap.WithLimit(ctx, -1))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, a := range ldapUl {
outUl = append(outUl, a)
}
}
return outUl, nil
}
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (auth.AuthMethod, requestauth.VerifyResults) {
const op = "managed_groups.(Service)."
res := requestauth.VerifyResults{}
@@ -908,3 +995,27 @@ func validateListRequest(ctx context.Context, req *pbs.ListManagedGroupsRequest)
}
return nil
}
func newOutputOpts(ctx context.Context, item auth.ManagedGroup, authMethodId string, authResults requestauth.VerifyResults) ([]handlers.Option, bool) {
res := perms.Resource{
ScopeId: authResults.Scope.Id,
Type: resource.ManagedGroup,
Pin: authMethodId,
}
res.Id = item.GetPublicId()
authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(item.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings()
if len(authorizedActions) == 0 {
return nil, false
}
outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions))
}
return outputOpts, true
}
@@ -7,7 +7,7 @@ import (
"context"
"errors"
"fmt"
"sort"
"slices"
"strconv"
"strings"
"testing"
@@ -16,16 +16,22 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth/ldap"
ldapstore "github.com/hashicorp/boundary/internal/auth/ldap/store"
"github.com/hashicorp/boundary/internal/auth/oidc"
oidcstore "github.com/hashicorp/boundary/internal/auth/oidc/store"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/managed_groups"
"github.com/hashicorp/boundary/internal/db"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/scope"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/managedgroups"
@@ -94,7 +100,7 @@ func TestNewService(t *testing.T) {
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := managed_groups.NewService(ctx, tc.oidcRepo, tc.ldapRepo)
_, err := managed_groups.NewService(ctx, tc.oidcRepo, tc.ldapRepo, 1000)
if tc.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErrContains)
@@ -121,7 +127,7 @@ func TestGet(t *testing.T) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(t, err, "Couldn't create new managed groups service.")
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
@@ -268,6 +274,8 @@ func TestGet(t *testing.T) {
func TestListOidc(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
@@ -333,6 +341,12 @@ func TestListOidc(t *testing.T) {
})
}
slices.Reverse(wantSomeManagedGroups)
slices.Reverse(wantOtherManagedGroups)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
cases := []struct {
name string
req *pbs.ListManagedGroupsRequest
@@ -343,17 +357,33 @@ func TestListOidc(t *testing.T) {
{
name: "List Some ManagedGroups",
req: &pbs.ListManagedGroupsRequest{AuthMethodId: amSomeManagedGroups.GetPublicId()},
res: &pbs.ListManagedGroupsResponse{Items: wantSomeManagedGroups},
res: &pbs.ListManagedGroupsResponse{
Items: wantSomeManagedGroups,
EstItemCount: 3,
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List Other ManagedGroups",
req: &pbs.ListManagedGroupsRequest{AuthMethodId: amOtherManagedGroups.GetPublicId()},
res: &pbs.ListManagedGroupsResponse{Items: wantOtherManagedGroups},
res: &pbs.ListManagedGroupsResponse{
Items: wantOtherManagedGroups,
EstItemCount: 3,
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List No ManagedGroups",
req: &pbs.ListManagedGroupsRequest{AuthMethodId: amNoManagedGroups.GetPublicId()},
res: &pbs.ListManagedGroupsResponse{},
res: &pbs.ListManagedGroupsResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Unfound Auth Method",
@@ -366,7 +396,13 @@ func TestListOidc(t *testing.T) {
AuthMethodId: amSomeManagedGroups.GetPublicId(),
Filter: fmt.Sprintf(`"/item/name"==%q`, wantSomeManagedGroups[1].Name.GetValue()),
},
res: &pbs.ListManagedGroupsResponse{Items: wantSomeManagedGroups[1:2]},
res: &pbs.ListManagedGroupsResponse{
Items: wantSomeManagedGroups[1:2],
EstItemCount: 1,
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
skipAnon: true,
},
{
@@ -375,7 +411,11 @@ func TestListOidc(t *testing.T) {
AuthMethodId: amSomeManagedGroups.GetPublicId(),
Filter: `"/item/id"=="noManagedGroupmatchesthis"`,
},
res: &pbs.ListManagedGroupsResponse{},
res: &pbs.ListManagedGroupsResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter Bad Format",
@@ -386,7 +426,7 @@ func TestListOidc(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(err, "Couldn't create new managed group service.")
got, gErr := s.ListManagedGroups(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
@@ -397,17 +437,14 @@ func TestListOidc(t *testing.T) {
} else {
require.NoError(gErr)
}
sort.Slice(got.Items, func(i, j int) bool {
return strings.Compare(got.Items[i].GetName().GetValue(),
got.Items[j].GetName().GetValue()) < 0
})
assert.Empty(cmp.Diff(
got,
tc.res,
protocmp.Transform(),
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
), "ListManagedGroups() with scope %q got response %q, wanted %q", tc.req, got, tc.res)
// Now test with anon
@@ -433,6 +470,8 @@ func TestListLdap(t *testing.T) {
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
@@ -493,6 +532,12 @@ func TestListLdap(t *testing.T) {
})
}
slices.Reverse(wantSomeManagedGroups)
slices.Reverse(wantOtherManagedGroups)
_, err = sqlDb.ExecContext(ctx, "analyze")
require.NoError(t, err)
cases := []struct {
name string
req *pbs.ListManagedGroupsRequest
@@ -504,17 +549,33 @@ func TestListLdap(t *testing.T) {
{
name: "List Some ManagedGroups",
req: &pbs.ListManagedGroupsRequest{AuthMethodId: amSomeManagedGroups.GetPublicId()},
res: &pbs.ListManagedGroupsResponse{Items: wantSomeManagedGroups},
res: &pbs.ListManagedGroupsResponse{
Items: wantSomeManagedGroups,
EstItemCount: 3,
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List Other ManagedGroups",
req: &pbs.ListManagedGroupsRequest{AuthMethodId: amOtherManagedGroups.GetPublicId()},
res: &pbs.ListManagedGroupsResponse{Items: wantOtherManagedGroups},
res: &pbs.ListManagedGroupsResponse{
Items: wantOtherManagedGroups,
EstItemCount: 3,
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "List No ManagedGroups",
req: &pbs.ListManagedGroupsRequest{AuthMethodId: amNoManagedGroups.GetPublicId()},
res: &pbs.ListManagedGroupsResponse{},
res: &pbs.ListManagedGroupsResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Unfound Auth Method",
@@ -528,7 +589,13 @@ func TestListLdap(t *testing.T) {
AuthMethodId: amSomeManagedGroups.GetPublicId(),
Filter: fmt.Sprintf(`"/item/name"==%q`, wantSomeManagedGroups[1].Name.GetValue()),
},
res: &pbs.ListManagedGroupsResponse{Items: wantSomeManagedGroups[1:2]},
res: &pbs.ListManagedGroupsResponse{
Items: wantSomeManagedGroups[1:2],
EstItemCount: 1,
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
skipAnon: true,
},
{
@@ -537,7 +604,11 @@ func TestListLdap(t *testing.T) {
AuthMethodId: amSomeManagedGroups.GetPublicId(),
Filter: `"/item/id"=="noManagedGroupmatchesthis"`,
},
res: &pbs.ListManagedGroupsResponse{},
res: &pbs.ListManagedGroupsResponse{
ResponseType: "complete",
SortBy: "created_time",
SortDir: "desc",
},
},
{
name: "Filter Bad Format",
@@ -549,7 +620,7 @@ func TestListLdap(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(err, "Couldn't create new managed group service.")
got, gErr := s.ListManagedGroups(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
@@ -562,17 +633,14 @@ func TestListLdap(t *testing.T) {
} else {
require.NoError(gErr)
}
sort.Slice(got.Items, func(i, j int) bool {
return strings.Compare(got.Items[i].GetName().GetValue(),
got.Items[j].GetName().GetValue()) < 0
})
assert.Empty(cmp.Diff(
got,
tc.res,
protocmp.Transform(),
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
), "ListManagedGroups() with scope %q got response %q, wanted %q", tc.req, got, tc.res)
// Now test with anon
@@ -592,6 +660,645 @@ func TestListLdap(t *testing.T) {
}
}
func TestListPagination(t *testing.T) {
// Set database read timeout to avoid duplicates in response
oldReadTimeout := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldReadTimeout
})
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
sqlDB, err := conn.SqlDB(ctx)
require.NoError(t, err)
rw := db.New(conn)
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
oidcRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kmsCache)
}
iamRepoFn := func() (*iam.Repository, error) {
return iam.NewRepository(ctx, rw, rw, kmsCache)
}
ldapRepoFn := func() (*ldap.Repository, error) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
tokenRepoFn := func() (*authtoken.Repository, error) {
return authtoken.NewRepository(ctx, rw, rw, kmsCache)
}
serversRepoFn := func() (*server.Repository, error) {
return server.NewRepository(ctx, rw, rw, kmsCache)
}
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(t, err)
iamRepo := iam.TestRepo(t, conn, wrap)
tokenRepo, _ := tokenRepoFn()
oidcRepo, _ := oidcRepoFn()
ldapRepo, _ := ldapRepoFn()
o, pwt := iam.TestScopes(t, iamRepo)
t.Run("oidc", func(t *testing.T) {
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
authMethod := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, "somemgs", "fido",
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.somemgs.com")[0]), oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]))
var mgs []*pb.ManagedGroup
for i := 0; i < 10; i++ {
mg := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter, oidc.WithName(strconv.Itoa(i)))
mgs = append(mgs, &pb.ManagedGroup{
Id: mg.GetPublicId(),
AuthMethodId: mg.GetAuthMethodId(),
Name: wrapperspb.String(strconv.Itoa(i)),
CreatedTime: mg.GetCreateTime().GetTimestamp(),
UpdatedTime: mg.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.ManagedGroup_OidcManagedGroupAttributes{
OidcManagedGroupAttributes: &pb.OidcManagedGroupAttributes{
Filter: oidc.TestFakeManagedGroupFilter,
},
},
AuthorizedActions: oidcAuthorizedActions,
})
}
acct := oidc.TestAccount(t, conn, authMethod, "test-login-last")
u := iam.TestUser(t, iamRepo, o.GetPublicId(), iam.WithAccountIds(acct.PublicId))
privProjRole := iam.TestRole(t, conn, pwt.GetPublicId())
iam.TestRoleGrant(t, conn, privProjRole.GetPublicId(), "id=*;type=*;actions=*")
iam.TestUserRole(t, conn, privProjRole.GetPublicId(), u.GetPublicId())
privOrgRole := iam.TestRole(t, conn, o.GetPublicId())
iam.TestRoleGrant(t, conn, privOrgRole.GetPublicId(), "id=*;type=*;actions=*")
iam.TestUserRole(t, conn, privOrgRole.GetPublicId(), u.GetPublicId())
// Since we sort by created_time descending, we reverse the slice
slices.Reverse(mgs)
at, _ := tokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId())
requestInfo := authpb.RequestInfo{
TokenFormat: uint32(auth.AuthTokenTypeBearer),
PublicId: at.GetPublicId(),
Token: at.GetToken(),
}
requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{})
ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kmsCache, &requestInfo)
req := &pbs.ListManagedGroupsRequest{
AuthMethodId: authMethod.GetPublicId(),
Filter: "",
ListToken: "",
PageSize: 2,
}
// Run analyze in the DB to update the estimate tables
_, err = sqlDB.ExecContext(ctx, "analyze")
require.NoError(t, err)
got, err := s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 2)
// all comparisons will be done without list token
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: mgs[0:2],
ResponseType: "delta",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// second page
req.ListToken = got.ListToken
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 2)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: mgs[2:4],
ResponseType: "delta",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// remainder of results
req.ListToken = got.ListToken
req.PageSize = 6
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 6)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: mgs[4:],
ResponseType: "complete",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// create another managed group
mg := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter, oidc.WithName("new-oidc-mg"))
newMg := &pb.ManagedGroup{
Id: mg.GetPublicId(),
AuthMethodId: mg.GetAuthMethodId(),
Name: wrapperspb.String("new-oidc-mg"),
CreatedTime: mg.GetCreateTime().GetTimestamp(),
UpdatedTime: mg.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: oidc.Subtype.String(),
Attrs: &pb.ManagedGroup_OidcManagedGroupAttributes{
OidcManagedGroupAttributes: &pb.OidcManagedGroupAttributes{
Filter: oidc.TestFakeManagedGroupFilter,
},
},
AuthorizedActions: oidcAuthorizedActions,
}
// Add to front since it's the latest updated
mgs = append([]*pb.ManagedGroup{newMg}, mgs...)
// delete different mg
_, err = oidcRepo.DeleteManagedGroup(ctx, o.GetPublicId(), mgs[len(mgs)-1].Id)
require.NoError(t, err)
deletedMg := mgs[len(mgs)-1]
mgs = mgs[:len(mgs)-1]
// update another mg
mgs[1].Name = wrapperspb.String("new-name")
mgs[1].Version = 2
m := &oidc.ManagedGroup{
ManagedGroup: &oidcstore.ManagedGroup{
PublicId: mgs[1].Id,
AuthMethodId: mgs[1].AuthMethodId,
Name: mgs[1].Name.GetValue(),
},
}
um, _, err := oidcRepo.UpdateManagedGroup(ctx, o.GetPublicId(), m, 1, []string{"name"})
require.NoError(t, err)
mgs[1].UpdatedTime = um.GetUpdateTime().GetTimestamp()
mgs[1].Version = um.GetVersion()
// Add to the front since it's most recently updated
mgs = append(
[]*pb.ManagedGroup{mgs[1]},
append(
[]*pb.ManagedGroup{mgs[0]},
mgs[2:]...,
)...,
)
// Run analyze to update postgres estimates
_, err = sqlDB.ExecContext(ctx, "analyze")
require.NoError(t, err)
// request updated results
req.ListToken = got.ListToken
req.PageSize = 1
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: []*pb.ManagedGroup{mgs[0]},
ResponseType: "delta",
ListToken: "",
SortBy: "updated_time",
SortDir: "desc",
RemovedIds: []string{deletedMg.Id},
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// Get next page
req.ListToken = got.ListToken
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: []*pb.ManagedGroup{mgs[1]},
ResponseType: "complete",
ListToken: "",
SortBy: "updated_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// Request new page with filter requiring looping
// to fill the page.
req.ListToken = ""
req.PageSize = 1
req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, mgs[len(mgs)-2].Id, mgs[len(mgs)-1].Id)
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: []*pb.ManagedGroup{mgs[len(mgs)-2]},
ResponseType: "delta",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
// Should be empty again
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
req.ListToken = got.ListToken
// Get the second page
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: []*pb.ManagedGroup{mgs[len(mgs)-1]},
ResponseType: "complete",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
})
t.Run("ldap", func(t *testing.T) {
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
authMethod := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://some-mgs"})
testGroups := []string{"admin", "users"}
var mgs []*pb.ManagedGroup
for i := 0; i < 10; i++ {
mg := ldap.TestManagedGroup(t, conn, authMethod, testGroups, ldap.WithName(ctx, strconv.Itoa(i)))
mgs = append(mgs, &pb.ManagedGroup{
Id: mg.GetPublicId(),
AuthMethodId: mg.GetAuthMethodId(),
Name: wrapperspb.String(strconv.Itoa(i)),
CreatedTime: mg.GetCreateTime().GetTimestamp(),
UpdatedTime: mg.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: ldap.Subtype.String(),
Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{
LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{
GroupNames: testGroups,
},
},
AuthorizedActions: ldapAuthorizedActions,
})
}
acct := ldap.TestAccount(t, conn, authMethod, "test-login-last")
u := iam.TestUser(t, iamRepo, o.GetPublicId(), iam.WithAccountIds(acct.PublicId))
privProjRole := iam.TestRole(t, conn, pwt.GetPublicId())
iam.TestRoleGrant(t, conn, privProjRole.GetPublicId(), "id=*;type=*;actions=*")
iam.TestUserRole(t, conn, privProjRole.GetPublicId(), u.GetPublicId())
privOrgRole := iam.TestRole(t, conn, o.GetPublicId())
iam.TestRoleGrant(t, conn, privOrgRole.GetPublicId(), "id=*;type=*;actions=*")
iam.TestUserRole(t, conn, privOrgRole.GetPublicId(), u.GetPublicId())
// Since we sort by created_time descending, we reverse the slice
slices.Reverse(mgs)
at, _ := tokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId())
requestInfo := authpb.RequestInfo{
TokenFormat: uint32(auth.AuthTokenTypeBearer),
PublicId: at.GetPublicId(),
Token: at.GetToken(),
}
requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{})
ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kmsCache, &requestInfo)
req := &pbs.ListManagedGroupsRequest{
AuthMethodId: authMethod.GetPublicId(),
Filter: "",
ListToken: "",
PageSize: 2,
}
// Run analyze in the DB to update the estimate tables
_, err = sqlDB.ExecContext(ctx, "analyze")
require.NoError(t, err)
got, err := s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 2)
// all comparisons will be done without list token
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: mgs[0:2],
ResponseType: "delta",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// second page
req.ListToken = got.ListToken
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 2)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: mgs[2:4],
ResponseType: "delta",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// remainder of results
req.ListToken = got.ListToken
req.PageSize = 6
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 6)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: mgs[4:],
ResponseType: "complete",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// create another mg
mg := ldap.TestManagedGroup(t, conn, authMethod, testGroups, ldap.WithName(ctx, "new-ldap-name"))
newMg := &pb.ManagedGroup{
Id: mg.GetPublicId(),
AuthMethodId: mg.GetAuthMethodId(),
Name: wrapperspb.String("new-ldap-name"),
CreatedTime: mg.GetCreateTime().GetTimestamp(),
UpdatedTime: mg.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
Version: 1,
Type: ldap.Subtype.String(),
Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{
LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{
GroupNames: testGroups,
},
},
AuthorizedActions: ldapAuthorizedActions,
}
// Add to front since it's the latest updated
mgs = append([]*pb.ManagedGroup{newMg}, mgs...)
// delete different mg
_, err = ldapRepo.DeleteManagedGroup(ctx, o.GetPublicId(), mgs[len(mgs)-1].Id)
require.NoError(t, err)
deletedMg := mgs[len(mgs)-1]
mgs = mgs[:len(mgs)-1]
// update another mg
mgs[1].Name = wrapperspb.String("new-name")
mgs[1].Version = 2
m := &ldap.ManagedGroup{
ManagedGroup: &ldapstore.ManagedGroup{
PublicId: mgs[1].Id,
AuthMethodId: mgs[1].AuthMethodId,
Name: mgs[1].Name.GetValue(),
},
}
um, _, err := ldapRepo.UpdateManagedGroup(ctx, o.GetPublicId(), m, 1, []string{"name"})
require.NoError(t, err)
mgs[1].UpdatedTime = um.GetUpdateTime().GetTimestamp()
mgs[1].Version = um.GetVersion()
// Add to the front since it's most recently updated
mgs = append(
[]*pb.ManagedGroup{mgs[1]},
append(
[]*pb.ManagedGroup{mgs[0]},
mgs[2:]...,
)...,
)
// Run analyze to update postgres estimates
_, err = sqlDB.ExecContext(ctx, "analyze")
require.NoError(t, err)
// request updated results
req.ListToken = got.ListToken
req.PageSize = 1
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: []*pb.ManagedGroup{mgs[0]},
ResponseType: "delta",
ListToken: "",
SortBy: "updated_time",
SortDir: "desc",
RemovedIds: []string{deletedMg.Id},
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// Get next page
req.ListToken = got.ListToken
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: []*pb.ManagedGroup{mgs[1]},
ResponseType: "complete",
ListToken: "",
SortBy: "updated_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
// Request new page with filter requiring looping
// to fill the page.
req.ListToken = ""
req.PageSize = 1
req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, mgs[len(mgs)-2].Id, mgs[len(mgs)-1].Id)
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: []*pb.ManagedGroup{mgs[len(mgs)-2]},
ResponseType: "delta",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
// Should be empty again
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
req.ListToken = got.ListToken
// Get the second page
got, err = s.ListManagedGroups(ctx, req)
require.NoError(t, err)
require.Len(t, got.GetItems(), 1)
assert.Empty(t,
cmp.Diff(
got,
&pbs.ListManagedGroupsResponse{
Items: []*pb.ManagedGroup{mgs[len(mgs)-1]},
ResponseType: "complete",
ListToken: "",
SortBy: "created_time",
SortDir: "desc",
RemovedIds: nil,
EstItemCount: 10,
},
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
protocmp.Transform(),
protocmp.IgnoreFields(&pbs.ListManagedGroupsResponse{}, "list_token"),
),
)
})
}
func TestDelete(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
@@ -624,7 +1331,7 @@ func TestDelete(t *testing.T) {
ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"})
ldapMg := ldap.TestManagedGroup(t, conn, ldapAm, []string{"admin", "users"})
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(t, err, "Error when getting new user service.")
cases := []struct {
@@ -719,7 +1426,7 @@ func TestDelete_twice(t *testing.T) {
)
oidcMg := oidc.TestManagedGroup(t, conn, oidcAm, oidc.TestFakeManagedGroupFilter)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(err, "Error when getting new user service")
req := &pbs.DeleteManagedGroupRequest{
Id: oidcMg.GetPublicId(),
@@ -747,7 +1454,7 @@ func TestCreateOidc(t *testing.T) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(t, err, "Error when getting new managed group service.")
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
@@ -959,7 +1666,7 @@ func TestCreateLdap(t *testing.T) {
return ldap.NewRepository(ctx, rw, rw, kmsCache)
}
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(t, err, "Error when getting new managed group service.")
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
@@ -1183,7 +1890,7 @@ func TestUpdateOidc(t *testing.T) {
oidc.WithSigningAlgs(oidc.RS256),
oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]))
tested, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
tested, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(t, err, "Error when getting new managed_groups service.")
defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}
@@ -1573,7 +2280,7 @@ func TestUpdateLdap(t *testing.T) {
require.NoError(t, err)
am := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"})
tested, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn)
tested, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000)
require.NoError(t, err, "Error when getting new managed_groups service.")
testGroups := []string{"test", "admin"}
@@ -0,0 +1,22 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
-- Add new indexes for the create time and update time queries.
create index auth_ldap_managed_group_create_time_public_id_idx
on auth_ldap_managed_group (create_time desc, public_id desc);
create index auth_ldap_managed_group_update_time_public_id_idx
on auth_ldap_managed_group (update_time desc, public_id desc);
analyze auth_ldap_managed_group;
-- Add new indexes for the create time and update time queries.
create index auth_oidc_managed_group_create_time_public_id_idx
on auth_oidc_managed_group (create_time desc, public_id desc);
create index auth_oidc_managed_group_update_time_public_id_idx
on auth_oidc_managed_group (update_time desc, public_id desc);
analyze auth_oidc_managed_group;
commit;
@@ -0,0 +1,15 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(4);
select has_index('auth_ldap_managed_group', 'auth_ldap_managed_group_create_time_public_id_idx', array['create_time', 'public_id']);
select has_index('auth_ldap_managed_group', 'auth_ldap_managed_group_update_time_public_id_idx', array['update_time', 'public_id']);
select has_index('auth_oidc_managed_group', 'auth_oidc_managed_group_create_time_public_id_idx', array['create_time', 'public_id']);
select has_index('auth_oidc_managed_group', 'auth_oidc_managed_group_update_time_public_id_idx', array['update_time', 'public_id']);
select * from finish();
rollback;
+43
View File
@@ -2114,6 +2114,21 @@
"in": "query",
"required": false,
"type": "string"
},
{
"name": "list_token",
"description": "An opaque token used to continue an existing iteration or\nrequest updated items. If not specified, pagination\nwill start from the beginning.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "page_size",
"description": "The maximum size of a page in this iteration.\nIf unset, the default page size configured will be used.\nIf the page_size is greater than the default page configured,\nan error will be returned.",
"in": "query",
"required": false,
"type": "integer",
"format": "int64"
}
],
"tags": [
@@ -8135,6 +8150,34 @@
"type": "object",
"$ref": "#/definitions/controller.api.resources.managedgroups.v1.ManagedGroup"
}
},
"response_type": {
"type": "string",
"description": "The type of response, either \"delta\" or \"complete\".\nDelta signifies that this is part of a paginated result\nor an update to a previously completed pagination.\nComplete signifies that it is the last page."
},
"list_token": {
"type": "string",
"description": "An opaque token used to continue an existing pagination or\nrequest updated items. Use this token in the next list request\nto request the next page."
},
"sort_by": {
"type": "string",
"description": "The name of the field which the items are sorted by."
},
"sort_dir": {
"type": "string",
"description": "The direction of the sort, either \"asc\" or \"desc\"."
},
"removed_ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of item IDs that have been removed since they were returned\nas part of a pagination. They should be dropped from any client cache.\nThis may contain items that are not known to the cache, if they were\ncreated and deleted between listings."
},
"est_item_count": {
"type": "integer",
"format": "int64",
"description": "An estimate at the total items available. This may change during pagination."
}
}
},
@@ -129,6 +129,15 @@ type ListManagedGroupsRequest struct {
AuthMethodId string `protobuf:"bytes,1,opt,name=auth_method_id,proto3" json:"auth_method_id,omitempty" class:"public" eventstream:"observation"` // @gotags: `class:"public" eventstream:"observation"`
Filter string `protobuf:"bytes,30,opt,name=filter,proto3" json:"filter,omitempty" class:"public"` // @gotags: `class:"public"`
// An opaque token used to continue an existing iteration or
// request updated items. If not specified, pagination
// will start from the beginning.
ListToken string `protobuf:"bytes,40,opt,name=list_token,proto3" json:"list_token,omitempty" class:"public"` // @gotags: `class:"public"`
// The maximum size of a page in this iteration.
// If unset, the default page size configured will be used.
// If the page_size is greater than the default page configured,
// an error will be returned.
PageSize uint32 `protobuf:"varint,50,opt,name=page_size,proto3" json:"page_size,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *ListManagedGroupsRequest) Reset() {
@@ -177,12 +186,46 @@ func (x *ListManagedGroupsRequest) GetFilter() string {
return ""
}
func (x *ListManagedGroupsRequest) GetListToken() string {
if x != nil {
return x.ListToken
}
return ""
}
func (x *ListManagedGroupsRequest) GetPageSize() uint32 {
if x != nil {
return x.PageSize
}
return 0
}
type ListManagedGroupsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Items []*managedgroups.ManagedGroup `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
// The type of response, either "delta" or "complete".
// Delta signifies that this is part of a paginated result
// or an update to a previously completed pagination.
// Complete signifies that it is the last page.
ResponseType string `protobuf:"bytes,2,opt,name=response_type,proto3" json:"response_type,omitempty" class:"public"` // @gotags: `class:"public"`
// An opaque token used to continue an existing pagination or
// request updated items. Use this token in the next list request
// to request the next page.
ListToken string `protobuf:"bytes,3,opt,name=list_token,proto3" json:"list_token,omitempty" class:"public"` // @gotags: `class:"public"`
// The name of the field which the items are sorted by.
SortBy string `protobuf:"bytes,4,opt,name=sort_by,proto3" json:"sort_by,omitempty" class:"public"` // @gotags: `class:"public"`
// The direction of the sort, either "asc" or "desc".
SortDir string `protobuf:"bytes,5,opt,name=sort_dir,proto3" json:"sort_dir,omitempty" class:"public"` // @gotags: `class:"public"`
// A list of item IDs that have been removed since they were returned
// as part of a pagination. They should be dropped from any client cache.
// This may contain items that are not known to the cache, if they were
// created and deleted between listings.
RemovedIds []string `protobuf:"bytes,6,rep,name=removed_ids,proto3" json:"removed_ids,omitempty" class:"public"` // @gotags: `class:"public"`
// An estimate at the total items available. This may change during pagination.
EstItemCount uint32 `protobuf:"varint,7,opt,name=est_item_count,proto3" json:"est_item_count,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *ListManagedGroupsResponse) Reset() {
@@ -224,6 +267,48 @@ func (x *ListManagedGroupsResponse) GetItems() []*managedgroups.ManagedGroup {
return nil
}
func (x *ListManagedGroupsResponse) GetResponseType() string {
if x != nil {
return x.ResponseType
}
return ""
}
func (x *ListManagedGroupsResponse) GetListToken() string {
if x != nil {
return x.ListToken
}
return ""
}
func (x *ListManagedGroupsResponse) GetSortBy() string {
if x != nil {
return x.SortBy
}
return ""
}
func (x *ListManagedGroupsResponse) GetSortDir() string {
if x != nil {
return x.SortDir
}
return ""
}
func (x *ListManagedGroupsResponse) GetRemovedIds() []string {
if x != nil {
return x.RemovedIds
}
return nil
}
func (x *ListManagedGroupsResponse) GetEstItemCount() uint32 {
if x != nil {
return x.EstItemCount
}
return 0
}
type CreateManagedGroupRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -552,129 +637,146 @@ var file_controller_api_services_v1_managed_group_service_proto_rawDesc = []byte
0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x04, 0x69, 0x74, 0x65,
0x6d, 0x22, 0x5a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a,
0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68,
0x6f, 0x64, 0x5f, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18,
0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x6a, 0x0a,
0x6d, 0x22, 0x98, 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26,
0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74,
0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72,
0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1e,
0x0a, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x28, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c,
0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28,
0x0d, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, 0xb0, 0x02, 0x0a,
0x19, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75,
0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x05, 0x69, 0x74,
0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74,
0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75,
0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f,
0x75, 0x70, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x68, 0x0a, 0x19, 0x43, 0x72, 0x65,
0x75, 0x70, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12,
0x1e, 0x0a, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12,
0x18, 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x72,
0x74, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x72,
0x74, 0x5f, 0x64, 0x69, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64,
0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f,
0x76, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x65, 0x73, 0x74, 0x5f, 0x69,
0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x0e, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22,
0x68, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4b, 0x0a, 0x04,
0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x6e,
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f,
0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72,
0x6f, 0x75, 0x70, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x7b, 0x0a, 0x1a, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31,
0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x04, 0x69,
0x74, 0x65, 0x6d, 0x22, 0x7b, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
0x75, 0x72, 0x69, 0x12, 0x4b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d,
0x22, 0xb6, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x4b,
0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63,
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67,
0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x3c, 0x0a, 0x0b, 0x75,
0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0b, 0x75, 0x70,
0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x69, 0x0a, 0x1a, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c,
0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76,
0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x04,
0x69, 0x74, 0x65, 0x6d, 0x22, 0x2b, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
0x64, 0x22, 0x1c, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32,
0xae, 0x08, 0x0a, 0x13, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xc1, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x32, 0x2e, 0x63, 0x6f,
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74,
0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x92, 0x41, 0x1d, 0x12, 0x1b, 0x47, 0x65, 0x74, 0x73, 0x20,
0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x62, 0x04, 0x69, 0x74,
0x65, 0x6d, 0x12, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x2d,
0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xd3, 0x01, 0x0a, 0x11,
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70,
0x73, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x4b, 0x0a, 0x04, 0x69, 0x74, 0x65,
0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x73, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73,
0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70,
0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xb6, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x02, 0x69, 0x64, 0x12, 0x4b, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x04, 0x69, 0x74, 0x65,
0x6d, 0x12, 0x3c, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b,
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61,
0x73, 0x6b, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22,
0x69, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a,
0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f,
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72,
0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47,
0x72, 0x6f, 0x75, 0x70, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x2b, 0x0a, 0x19, 0x44, 0x65,
0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1c, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74,
0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xae, 0x08, 0x0a, 0x13, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xc1, 0x01,
0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75,
0x70, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47,
0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c,
0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e,
0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f,
0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x92, 0x41, 0x1d, 0x12,
0x1b, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x1f, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x64, 0x2d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x7b, 0x69, 0x64,
0x7d, 0x12, 0xd3, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51,
0x92, 0x41, 0x34, 0x12, 0x32, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x20, 0x69, 0x6e, 0x20,
0x61, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20,
0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f,
0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x2d, 0x67, 0x72, 0x6f, 0x75, 0x70,
0x73, 0x12, 0xea, 0x01, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72,
0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x65, 0x92, 0x41, 0x3c, 0x12, 0x3a, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x20, 0x69, 0x6e, 0x20, 0x74,
0x68, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x20, 0x41, 0x75, 0x74, 0x68,
0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x04,
0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x12, 0x2f, 0x76, 0x31, 0x2f,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x2d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0xcc,
0x01, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e,
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, 0x34, 0x12, 0x32, 0x4c, 0x69, 0x73, 0x74, 0x73,
0x20, 0x61, 0x6c, 0x6c, 0x20, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75,
0x70, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63,
0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4,
0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x2d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0xea, 0x01, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x35,
0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c,
0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e,
0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x63,
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x47, 0x92, 0x41, 0x19, 0x12, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x73, 0x20, 0x61, 0x20, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75,
0x70, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04,
0x69, 0x74, 0x65, 0x6d, 0x32, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x64, 0x2d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xc0, 0x01,
0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47,
0x72, 0x6f, 0x75, 0x70, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76,
0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47,
0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x63, 0x6f,
0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x65, 0x92,
0x41, 0x3c, 0x12, 0x3a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69,
0x6e, 0x67, 0x6c, 0x65, 0x20, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75,
0x70, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
0x64, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x20, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d,
0x22, 0x12, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x2d, 0x67, 0x72,
0x6f, 0x75, 0x70, 0x73, 0x12, 0xcc, 0x01, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x35, 0x2e, 0x63, 0x6f,
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x3b, 0x92, 0x41, 0x19, 0x12, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
0x73, 0x20, 0x61, 0x20, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70,
0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x64, 0x2d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d,
0x42, 0x55, 0xa2, 0xe3, 0x29, 0x04, 0x61, 0x75, 0x74, 0x68, 0x5a, 0x4b, 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, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f,
0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x47, 0x92, 0x41, 0x19, 0x12,
0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x4d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x3a, 0x04,
0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x17, 0x2f, 0x76, 0x31, 0x2f,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x2d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x7b,
0x69, 0x64, 0x7d, 0x12, 0xc0, 0x01, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x6e,
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44,
0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75,
0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x92, 0x41, 0x19, 0x12, 0x17,
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a, 0x17, 0x2f,
0x76, 0x31, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x2d, 0x67, 0x72, 0x6f, 0x75, 0x70,
0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x42, 0x55, 0xa2, 0xe3, 0x29, 0x04, 0x61, 0x75, 0x74, 0x68,
0x5a, 0x4b, 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, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e,
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -86,10 +86,39 @@ message GetManagedGroupResponse {
message ListManagedGroupsRequest {
string auth_method_id = 1 [json_name = "auth_method_id"]; // @gotags: `class:"public" eventstream:"observation"`
string filter = 30 [json_name = "filter"]; // @gotags: `class:"public"`
// An opaque token used to continue an existing iteration or
// request updated items. If not specified, pagination
// will start from the beginning.
string list_token = 40 [json_name = "list_token"]; // @gotags: `class:"public"`
// The maximum size of a page in this iteration.
// If unset, the default page size configured will be used.
// If the page_size is greater than the default page configured,
// an error will be returned.
uint32 page_size = 50 [json_name = "page_size"]; // @gotags: `class:"public"`
}
message ListManagedGroupsResponse {
repeated resources.managedgroups.v1.ManagedGroup items = 1;
// The type of response, either "delta" or "complete".
// Delta signifies that this is part of a paginated result
// or an update to a previously completed pagination.
// Complete signifies that it is the last page.
string response_type = 2 [json_name = "response_type"]; // @gotags: `class:"public"`
// An opaque token used to continue an existing pagination or
// request updated items. Use this token in the next list request
// to request the next page.
string list_token = 3 [json_name = "list_token"]; // @gotags: `class:"public"`
// The name of the field which the items are sorted by.
string sort_by = 4 [json_name = "sort_by"]; // @gotags: `class:"public"`
// The direction of the sort, either "asc" or "desc".
string sort_dir = 5 [json_name = "sort_dir"]; // @gotags: `class:"public"`
// A list of item IDs that have been removed since they were returned
// as part of a pagination. They should be dropped from any client cache.
// This may contain items that are not known to the cache, if they were
// created and deleted between listings.
repeated string removed_ids = 6 [json_name = "removed_ids"]; // @gotags: `class:"public"`
// An estimate at the total items available. This may change during pagination.
uint32 est_item_count = 7 [json_name = "est_item_count"]; // @gotags: `class:"public"`
}
message CreateManagedGroupRequest {