Managed group list pagination (#4208)
* wip * managed groups pagination * address PR comments * missed one change
This commit is contained in:
1 parent
f49ba4662c
commit
52b1c1b4c7
31 files changed
+4059
-278
No files matched your search
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
`
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
`
|
||||
)
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user