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:
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
|
||||
}
|
||||
Reference in new issue
Block a user