Auth Method list pagination (#4165)

* wip

* implement list pagination for auth methods

* remove unused code

* update tests to fix sorting

* address PR comments

* simplify query, add token tests

* change test name

* wip sql testing

* add boolean column to indicate whether auth methods should be returned to unauthenticated users, remove secrets from api response

* is_active_public not null

* remove secrets and unwrapping

* move not null to immediately after data migration

* order public_id descending

* fix(db): Fix auth method migrations (#4193)

* wip

* fix db tests

* fix failing api tests

* reverse order of create time and update time in test

---------

Co-authored-by: Michael Gaffney <mike@gaffney.cc>
This commit is contained in:
Michael MiltonandMichael Gaffney authored and Johan Brandhorst-Satzkorn committed 2024-01-05 07:43:21 -08:00
1 parent 0fad770d8a
commit 8d50a0913f
34 files changed
+4340 -445

No files matched your search

@@ -26,6 +26,8 @@ import (
"github.com/hashicorp/boundary/internal/errors"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/kms"
"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"
@@ -57,8 +59,6 @@ const (
authMethodIdField = "auth_method_id"
tokenTypeField = "type"
isPrimaryField = "is_primary"
domain = "auth"
)
var (
@@ -84,18 +84,31 @@ var (
type Service struct {
pbs.UnsafeAuthMethodServiceServer
kms *kms.Kms
pwRepoFn common.PasswordAuthRepoFactory
oidcRepoFn common.OidcAuthRepoFactory
iamRepoFn common.IamRepoFactory
atRepoFn common.AuthTokenRepoFactory
ldapRepoFn common.LdapAuthRepoFactory
kms *kms.Kms
pwRepoFn common.PasswordAuthRepoFactory
oidcRepoFn common.OidcAuthRepoFactory
iamRepoFn common.IamRepoFactory
atRepoFn common.AuthTokenRepoFactory
ldapRepoFn common.LdapAuthRepoFactory
amRepoFn common.AuthMethodRepoFactory
maxPageSize uint
}
var _ pbs.AuthMethodServiceServer = (*Service)(nil)
// NewService returns a auth method service which handles auth method related requests to boundary.
func NewService(ctx context.Context, kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoFn common.OidcAuthRepoFactory, iamRepoFn common.IamRepoFactory, atRepoFn common.AuthTokenRepoFactory, ldapRepoFn common.LdapAuthRepoFactory, opt ...handlers.Option) (Service, error) {
func NewService(
ctx context.Context,
kms *kms.Kms,
pwRepoFn common.PasswordAuthRepoFactory,
oidcRepoFn common.OidcAuthRepoFactory,
iamRepoFn common.IamRepoFactory,
atRepoFn common.AuthTokenRepoFactory,
ldapRepoFn common.LdapAuthRepoFactory,
amRepoFn common.AuthMethodRepoFactory,
maxPageSize uint,
opt ...handlers.Option,
) (Service, error) {
const op = "authmethods.NewService"
if kms == nil {
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing kms")
@@ -115,15 +128,32 @@ func NewService(ctx context.Context, kms *kms.Kms, pwRepoFn common.PasswordAuthR
if atRepoFn == nil {
return Service{}, fmt.Errorf("nil auth token repository provided")
}
s := Service{kms: kms, pwRepoFn: pwRepoFn, oidcRepoFn: oidcRepoFn, iamRepoFn: iamRepoFn, atRepoFn: atRepoFn, ldapRepoFn: ldapRepoFn}
if amRepoFn == nil {
return Service{}, fmt.Errorf("nil auth method repository provided")
}
if maxPageSize == 0 {
maxPageSize = uint(globals.DefaultMaxPageSize)
}
s := Service{
kms: kms,
pwRepoFn: pwRepoFn,
oidcRepoFn: oidcRepoFn,
iamRepoFn: iamRepoFn,
atRepoFn: atRepoFn,
ldapRepoFn: ldapRepoFn,
amRepoFn: amRepoFn,
maxPageSize: maxPageSize,
}
return s, nil
}
// ListAuthMethods implements the interface pbs.AuthMethodServiceServer.
func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRequest) (*pbs.ListAuthMethodsResponse, error) {
const op = "authmethods.(Service).ListAuthMethods"
if err := validateListRequest(ctx, req); err != nil {
return nil, err
return nil, errors.Wrap(ctx, err, op)
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
if authResults.Error != nil {
@@ -138,6 +168,21 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe
return nil, authResults.Error
}
}
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())
}
withUnauthenticatedUser := false
reqCtx, ok := requests.RequestContextFromCtx(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
if reqCtx.UserId == globals.AnonymousUserId {
withUnauthenticatedUser = true
}
scopeIds, scopeInfoMap, err := scopeids.GetListingScopeIds(
ctx, s.iamRepoFn, authResults, req.GetScopeId(), resource.AuthMethod, req.GetRecursive())
@@ -149,63 +194,127 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe
return &pbs.ListAuthMethodsResponse{}, nil
}
ul, err := s.listFromRepo(ctx, scopeIds, authResults)
if err != nil {
return nil, err
}
if len(ul) == 0 {
return &pbs.ListAuthMethodsResponse{}, nil
var filterItemFn func(ctx context.Context, item auth.AuthMethod) (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.AuthMethod) (bool, error) {
outputOpts, ok, err := newOutputOpts(ctx, item, scopeInfoMap, authResults)
if err != nil {
return false, errors.Wrap(ctx, err, op)
}
if !ok {
return false, nil
}
pbItem, err := toAuthMethodProto(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.AuthMethod) (bool, error) {
return true, nil
}
}
filter, err := handlers.NewFilter(ctx, req.GetFilter())
repo, err := s.amRepoFn()
if err != nil {
return nil, err
return nil, errors.Wrap(ctx, err, op)
}
finalItems := make([]*pb.AuthMethod, 0, len(ul))
res := perms.Resource{
Type: resource.AuthMethod,
}
for _, am := range ul {
res.Id = am.GetPublicId()
res.ScopeId = am.GetScopeId()
authorizedActions := authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(am.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings()
if len(authorizedActions) == 0 {
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(scopeInfoMap[am.GetScopeId()]))
grantsHash, err := authResults.GrantsHash(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var listResp *pagination.ListResponse[auth.AuthMethod]
var sortBy string
if req.GetListToken() == "" {
sortBy = "created_time"
listResp, err = auth.ListAuthMethods(ctx, grantsHash, pageSize, filterItemFn, repo, scopeIds, withUnauthenticatedUser)
if err != nil {
return nil, err
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions))
} else {
listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.AuthMethod, grantsHash)
if err != nil {
return nil, err
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId())
switch st := listToken.Subtype.(type) {
case *listtoken.PaginationToken:
sortBy = "created_time"
listResp, err = auth.ListAuthMethodsPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds, withUnauthenticatedUser)
if err != nil {
return nil, err
}
outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions))
}
item, err := toAuthMethodProto(ctx, am, outputOpts...)
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)
case *listtoken.StartRefreshToken:
sortBy = "updated_time"
listResp, err = auth.ListAuthMethodsRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds, withUnauthenticatedUser)
if err != nil {
return nil, err
}
case *listtoken.RefreshToken:
sortBy = "updated_time"
listResp, err = auth.ListAuthMethodsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds, withUnauthenticatedUser)
if err != nil {
return nil, err
}
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st)
}
}
return &pbs.ListAuthMethodsResponse{Items: finalItems}, nil
finalItems := make([]*pb.AuthMethod, 0, len(listResp.Items))
for _, item := range listResp.Items {
outputOpts, ok, err := newOutputOpts(ctx, item, scopeInfoMap, authResults)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if !ok {
continue
}
pbItem, err := toAuthMethodProto(ctx, item, outputOpts...)
if err != nil {
continue
}
finalItems = append(finalItems, pbItem)
}
respType := "delta"
if listResp.CompleteListing {
respType = "complete"
}
resp := &pbs.ListAuthMethodsResponse{
Items: finalItems,
EstItemCount: uint32(listResp.EstimatedItemCount),
RemovedIds: listResp.DeletedIds,
ResponseType: respType,
SortBy: sortBy,
SortDir: "desc",
}
if listResp.ListToken != nil {
resp.ListToken, err = handlers.MarshalListToken(ctx, listResp.ListToken, pbs.ResourceType_RESOURCE_TYPE_AUTH_METHOD)
if err != nil {
return nil, err
}
}
return resp, nil
}
// GetAuthMethod implements the interface pbs.AuthMethodServiceServer.
@@ -499,54 +608,6 @@ func (s Service) getFromRepo(ctx context.Context, id string) (auth.AuthMethod, e
return am, nil
}
func (s Service) listFromRepo(ctx context.Context, scopeIds []string, authResults requestauth.VerifyResults) ([]auth.AuthMethod, error) {
const op = "authmethods.(Service).listFromRepo"
reqCtx, ok := requests.RequestContextFromCtx(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
var outUl []auth.AuthMethod
oidcRepo, err := s.oidcRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
ol, err := oidcRepo.ListAuthMethods(ctx, scopeIds, oidc.WithUnauthenticatedUser(reqCtx.UserId == globals.AnonymousUserId), oidc.WithLimit(-1))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, item := range ol {
outUl = append(outUl, item)
}
repo, err := s.pwRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
pl, err := repo.ListAuthMethods(ctx, scopeIds)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, item := range pl {
outUl = append(outUl, item)
}
ldapRepo, err := s.ldapRepoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
ll, err := ldapRepo.ListAuthMethods(ctx, scopeIds, ldap.WithUnauthenticatedUser(ctx, reqCtx.UserId == globals.AnonymousUserId))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, item := range ll {
outUl = append(outUl, item)
}
return outUl, nil
}
func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (auth.AuthMethod, error) {
const op = "authmethods.(Service).createInRepo"
var out auth.AuthMethod
@@ -1508,3 +1569,33 @@ func transformAuthenticateResponseAttributes(ctx context.Context, msg proto.Mess
}
return nil
}
func newOutputOpts(ctx context.Context, item auth.AuthMethod, scopeInfoMap map[string]*scopes.ScopeInfo, authResults requestauth.VerifyResults) ([]handlers.Option, bool, error) {
res := perms.Resource{
Type: resource.AuthMethod,
}
res.Id = item.GetPublicId()
res.ScopeId = item.GetScopeId()
authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(item.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings()
if len(authorizedActions) == 0 {
return nil, false, nil
}
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(scopeInfoMap[item.GetScopeId()]))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, item.GetPublicId())
if err != nil {
return nil, false, err
}
outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions))
}
return outputOpts, true, nil
}