Add ability to filter List*Requests (#952)

This commit is contained in:
Todd Knight authored and GitHub committed 2021-02-22 10:09:25 -07:00
1 parent 3c33d05c96
commit 97b404032b
53 files changed
+2624 -1748

No files matched your search

@@ -77,6 +77,10 @@ func (s Service) ListAccounts(ctx context.Context, req *pbs.ListAccountsRequest)
if err != nil {
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.Account, 0, len(ul))
res := &perms.Resource{
ScopeId: authResults.Scope.Id,
@@ -86,7 +90,10 @@ func (s Service) ListAccounts(ctx context.Context, req *pbs.ListAccountsRequest)
for _, item := range ul {
item.Scope = authResults.Scope
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
@@ -502,6 +509,9 @@ func validateListRequest(req *pbs.ListAccountsRequest) error {
if !handlers.ValidId(password.AuthMethodPrefix, req.GetAuthMethodId()) {
badFields["auth_method_id"] = "Invalid formatted identifier."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
@@ -144,30 +144,51 @@ func TestList(t *testing.T) {
}
cases := []struct {
name string
authMethod string
res *pbs.ListAccountsResponse
err error
name string
req *pbs.ListAccountsRequest
res *pbs.ListAccountsResponse
err error
}{
{
name: "List Some Accounts",
authMethod: amSomeAccounts.GetPublicId(),
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts},
name: "List Some Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts},
},
{
name: "List Other Accounts",
authMethod: amOtherAccounts.GetPublicId(),
res: &pbs.ListAccountsResponse{Items: wantOtherAccounts},
name: "List Other Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amOtherAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{Items: wantOtherAccounts},
},
{
name: "List No Accounts",
authMethod: amNoAccounts.GetPublicId(),
res: &pbs.ListAccountsResponse{},
name: "List No Accounts",
req: &pbs.ListAccountsRequest{AuthMethodId: amNoAccounts.GetPublicId()},
res: &pbs.ListAccountsResponse{},
},
{
name: "Unfound Auth Method",
authMethod: password.AuthMethodPrefix + "_DoesntExis",
err: handlers.ApiErrorWithCode(codes.NotFound),
name: "Unfound Auth Method",
req: &pbs.ListAccountsRequest{AuthMethodId: password.AuthMethodPrefix + "_DoesntExis"},
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Filter Some Accounts",
req: &pbs.ListAccountsRequest{
AuthMethodId: amSomeAccounts.GetPublicId(),
Filter: fmt.Sprintf(`"/item/attributes/login_name"==%q`, wantSomeAccounts[1].Attributes.AsMap()["login_name"]),
},
res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]},
},
{
name: "Filter All Accounts",
req: &pbs.ListAccountsRequest{
AuthMethodId: amSomeAccounts.GetPublicId(),
Filter: `"/item/id"=="noaccountmatchesthis"`,
},
res: &pbs.ListAccountsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
@@ -176,12 +197,14 @@ func TestList(t *testing.T) {
s, err := accounts.NewService(repoFn)
require.NoError(err, "Couldn't create new user service.")
got, gErr := s.ListAccounts(auth.DisabledAuthTestContext(auth.WithScopeId(o.GetPublicId())), &pbs.ListAccountsRequest{AuthMethodId: tc.authMethod})
got, gErr := s.ListAccounts(auth.DisabledAuthTestContext(auth.WithScopeId(o.GetPublicId())), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListAccounts() with auth method %q got error %v, wanted %v", tc.authMethod, gErr, tc.err)
assert.True(errors.Is(gErr, tc.err), "ListAccounts() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err)
} else {
require.NoError(gErr)
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListUsers() with scope %q got response %q, wanted %q", tc.authMethod, got, tc.res)
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
@@ -141,6 +141,10 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe
if err != nil {
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.AuthMethod, 0, len(ul))
res := &perms.Resource{
Type: resource.AuthMethod,
@@ -149,7 +153,10 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
if err := populateCollectionAuthorizedActions(ctx, authResults, item); err != nil {
return nil, err
@@ -605,6 +612,9 @@ func validateListRequest(req *pbs.ListAuthMethodsRequest) error {
req.GetScopeId() != scope.Global.String() {
badFields["scope_id"] = "This field must be 'global' or a valid org scope id."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -226,6 +226,24 @@ func TestList(t *testing.T) {
Items: append(wantSomeAuthMethods, wantOtherAuthMethods...),
},
},
{
name: "Filter To Some Auth Methods",
req: &pbs.ListAuthMethodsRequest{
ScopeId: "global", Recursive: true,
Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithAuthMethods.GetPublicId()),
},
res: &pbs.ListAuthMethodsResponse{Items: wantSomeAuthMethods},
},
{
name: "Filter All Auth Methods",
req: &pbs.ListAuthMethodsRequest{ScopeId: oWithAuthMethods.GetPublicId(), Filter: `"/item/id"=="nothingmatchesthis"`},
res: &pbs.ListAuthMethodsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListAuthMethodsRequest{ScopeId: oWithAuthMethods.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -237,6 +255,8 @@ func TestList(t *testing.T) {
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListAuthMethods() for scope %q got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err)
} else {
require.NoError(gErr)
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAuthMethods() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res)
})
@@ -74,6 +74,10 @@ func (s Service) ListAuthTokens(ctx context.Context, req *pbs.ListAuthTokensRequ
if err != nil {
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.AuthToken, 0, len(ul))
res := &perms.Resource{
Type: resource.AuthToken,
@@ -82,7 +86,10 @@ func (s Service) ListAuthTokens(ctx context.Context, req *pbs.ListAuthTokensRequ
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
@@ -251,6 +258,9 @@ func validateListRequest(req *pbs.ListAuthTokensRequest) error {
req.GetScopeId() != scope.Global.String() {
badFields["scope_id"] = "This field must be 'global' or a valid org scope id."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -2,6 +2,7 @@ package authtokens_test
import (
"errors"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
@@ -204,6 +205,24 @@ func TestList(t *testing.T) {
req: &pbs.ListAuthTokensRequest{ScopeId: "global", Recursive: true},
res: &pbs.ListAuthTokensResponse{Items: allTokens},
},
{
name: "Filter to Some Tokens",
req: &pbs.ListAuthTokensRequest{
ScopeId: "global", Recursive: true,
Filter: fmt.Sprintf(`"/item/scope/id"==%q`, orgWithSomeTokens.GetPublicId()),
},
res: &pbs.ListAuthTokensResponse{Items: wantSomeTokens},
},
{
name: "Filter All Tokens",
req: &pbs.ListAuthTokensRequest{ScopeId: orgWithOtherTokens.GetPublicId(), Filter: `"/item/scope/id"=="thisdoesntmatch"`},
res: &pbs.ListAuthTokensResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListAuthTokensRequest{ScopeId: orgWithSomeTokens.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -214,6 +233,8 @@ func TestList(t *testing.T) {
if tc.err != nil {
require.Error(t, gErr)
assert.True(t, errors.Is(gErr, tc.err), "ListAuthTokens() with scope %q got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err)
} else {
require.NoError(t, gErr)
}
assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListAuthTokens() with scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res)
})
@@ -0,0 +1,85 @@
package handlers
import (
"reflect"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/go-bexpr"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// filterItem captures all the different namespaces that can be used when
// filtering an item.
type filterItem struct {
Item interface{} `json:"item"`
}
type Filter struct {
eval *bexpr.Evaluator
}
// NewFilter returns a Filter which can be evluated against. An empty string paramter indicates
// all items passed to it should succeed.
func NewFilter(f string) (*Filter, error) {
const op = "handlers.NewFilter"
if f == "" {
return &Filter{}, nil
}
e, err := bexpr.CreateEvaluator(f, bexpr.WithTagName("json"), bexpr.WithHookFn(wellKnownTypeFilterHook))
if err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg("couldn't build filter"), errors.WithCode(errors.InvalidParameter))
}
return &Filter{eval: e}, nil
}
// Match returns if the provided interface matches the filter.
// If the filter does not match the structure of the object being Matched, false is returned.
// TODO: Support more than just matching against the item being filtered. Also allow matching against
// values in the request or the request context itself.
func (f *Filter) Match(item interface{}) bool {
if f.eval == nil {
return true
}
m, err := f.eval.Evaluate(filterItem{Item: item})
// There isn't a clear way to differentiate between a JSON Pointer which doesn't represent
// the structure of the object being Matched and a JSON Pointer which references a field which
// is part of a sub structure that is nil in this item. Because of this, any filter which would
// result in an error using the underlying library is simply interpreted as not a match.
return err == nil && m
}
// wellKnownTypeFilterHook is passed to bexpr to treat all proto well-known
// types as the types they wrap for comparison.
func wellKnownTypeFilterHook(v reflect.Value) reflect.Value {
if !v.CanInterface() {
return v
}
ret := v.Interface()
switch pm := v.Interface().(type) {
case *wrapperspb.BoolValue:
ret = pm.GetValue()
case *wrapperspb.BytesValue:
ret = pm.GetValue()
case *wrapperspb.StringValue:
ret = pm.GetValue()
case *wrapperspb.DoubleValue:
ret = pm.GetValue()
case *wrapperspb.FloatValue:
ret = pm.GetValue()
case *wrapperspb.Int32Value:
ret = pm.GetValue()
case *wrapperspb.Int64Value:
ret = pm.GetValue()
case *wrapperspb.UInt32Value:
ret = pm.GetValue()
case *wrapperspb.UInt64Value:
ret = pm.GetValue()
case *structpb.Struct:
ret = pm.AsMap()
case *timestamppb.Timestamp:
ret = pm.AsTime()
}
return reflect.ValueOf(ret)
}
@@ -0,0 +1,137 @@
package handlers
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func TestNewFilter_everythingMatchesEmpty(t *testing.T) {
f, err := NewFilter("")
require.NoError(t, err)
for _, v := range []interface{}{
nil,
"foo",
"",
1,
-1,
map[string]string{},
[]string{"foo"},
[]int(nil),
(*filterItem)(nil),
struct{ foo string }{foo: "foo"},
filterItem{Item: struct{ foo string }{foo: "foo"}},
} {
assert.True(t, f.Match(v), "Trying to match %v", v)
}
}
func TestNewFilter(t *testing.T) {
type embedded struct {
Name string `json:"name"`
}
type multiLevel struct {
E *embedded `json:"e"`
}
cases := []struct {
name string
filter string
fErr bool
in interface{}
match bool
}{
{
name: "bad format",
filter: `random strings that dont match a format`,
fErr: true,
},
{
name: "no leading /item",
filter: `""=="foo"`,
in: "foo",
match: false,
},
{
name: "simple string",
filter: `"/item"=="foo"`,
in: "foo",
match: true,
},
{
name: "struct",
filter: `"/item/id"=="foo"`,
in: struct {
ID string `json:"id"`
}{ID: "foo"},
match: true,
},
{
name: "doesnt match struct fields",
filter: `"/item/name"=="foo"`,
in: struct {
ID string `json:"id"`
}{ID: "foo"},
match: false,
},
{
name: "proto well known types",
filter: `"/item/id"=="foo"`,
in: struct {
ID *wrapperspb.StringValue `json:"id"`
}{ID: wrapperspb.String("foo")},
match: true,
},
{
name: "pointer include proto well known type structure",
filter: `"/item/id/value"=="foo"`,
in: struct {
ID *wrapperspb.StringValue `json:"id"`
}{ID: wrapperspb.String("foo")},
match: false,
},
{
name: "multi level struct",
filter: `"/item/e/name"=="foo"`,
in: multiLevel{E: &embedded{Name: "foo"}},
match: true,
},
{
name: "multi level struct",
filter: `"/item/e/name"=="foo"`,
in: multiLevel{E: nil},
match: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f, err := NewFilter(tc.filter)
if tc.fErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.match, f.Match(tc.in))
})
}
}
func TestWellKnownTypeFilterHook(t *testing.T) {
conversions := map[interface{}]interface{}{
wrapperspb.String("foo"): "foo",
wrapperspb.UInt64(123): uint64(123),
wrapperspb.Int64(123): int64(123),
wrapperspb.UInt32(123): uint32(123),
wrapperspb.Int32(123): int32(123),
wrapperspb.Float(123): float32(123),
wrapperspb.Double(123): float64(123),
wrapperspb.Bytes([]byte("foo")): []byte("foo"),
wrapperspb.Bool(true): true,
}
for in, out := range conversions {
assert.Equal(t, reflect.ValueOf(out).Interface(), wellKnownTypeFilterHook(reflect.ValueOf(in)).Interface())
}
}
@@ -88,6 +88,10 @@ func (s Service) ListGroups(ctx context.Context, req *pbs.ListGroupsRequest) (*p
if err != nil {
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.Group, 0, len(gl))
res := &perms.Resource{
Type: resource.Group,
@@ -96,7 +100,10 @@ func (s Service) ListGroups(ctx context.Context, req *pbs.ListGroupsRequest) (*p
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
@@ -493,6 +500,9 @@ func validateListRequest(req *pbs.ListGroupsRequest) error {
req.GetScopeId() != scope.Global.String() {
badFields["scope_id"] = "Incorrectly formatted identifier."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -290,6 +290,26 @@ func TestList(t *testing.T) {
Items: totalGroups,
},
},
{
name: "Filter to org groups",
req: &pbs.ListGroupsRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithGroups.GetPublicId())},
res: &pbs.ListGroupsResponse{Items: wantOrgGroups},
},
{
name: "Filter to project groups",
req: &pbs.ListGroupsRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, pWithGroups.GetPublicId())},
res: &pbs.ListGroupsResponse{Items: wantProjGroups},
},
{
name: "Filter to no groups",
req: &pbs.ListGroupsRequest{ScopeId: "global", Recursive: true, Filter: `"/item/id"=="doesntmatch"`},
res: &pbs.ListGroupsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListGroupsRequest{ScopeId: "global", Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -125,6 +125,10 @@ func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogs
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.HostCatalog, 0, len(ul))
res := &perms.Resource{
Type: resource.HostCatalog,
@@ -133,7 +137,10 @@ func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogs
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
if err := populateCollectionAuthorizedActions(ctx, authResults, item); err != nil {
return nil, err
@@ -457,6 +464,9 @@ func validateListRequest(req *pbs.ListHostCatalogsRequest) error {
!req.GetRecursive() {
badFields["scope_id"] = "This field must be a valid project scope ID or the list operation must be recursive."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -232,6 +232,24 @@ func TestList(t *testing.T) {
Items: append(wantSomeCatalogs, wantOtherCatalogs...),
},
},
{
name: "Filter To Some Catalogs",
req: &pbs.ListHostCatalogsRequest{
ScopeId: scope.Global.String(), Recursive: true,
Filter: fmt.Sprintf(`"/item/scope/id"==%q`, pWithCatalogs.GetPublicId()),
},
res: &pbs.ListHostCatalogsResponse{Items: wantSomeCatalogs},
},
{
name: "Filter To No Catalogs",
req: &pbs.ListHostCatalogsRequest{ScopeId: pWithCatalogs.GetPublicId(), Filter: `"/item/id"=="doesnt match"`},
res: &pbs.ListHostCatalogsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListHostCatalogsRequest{ScopeId: pWithCatalogs.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -79,6 +79,10 @@ func (s Service) ListHostSets(ctx context.Context, req *pbs.ListHostSetsRequest)
if err != nil {
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.HostSet, 0, len(hl))
res := &perms.Resource{
ScopeId: authResults.Scope.Id,
@@ -88,7 +92,10 @@ func (s Service) ListHostSets(ctx context.Context, req *pbs.ListHostSetsRequest)
for _, item := range hl {
item.Scope = authResults.Scope
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
@@ -534,6 +541,9 @@ func validateListRequest(req *pbs.ListHostSetsRequest) error {
if !handlers.ValidId(static.HostCatalogPrefix, req.GetHostCatalogId()) {
badFields["host_catalog_id"] = "The field is incorrectly formatted."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -145,20 +145,35 @@ func TestList(t *testing.T) {
}
cases := []struct {
name string
hostCatalogId string
res *pbs.ListHostSetsResponse
err error
name string
req *pbs.ListHostSetsRequest
res *pbs.ListHostSetsResponse
err error
}{
{
name: "List Many Host Sets",
hostCatalogId: hc.GetPublicId(),
res: &pbs.ListHostSetsResponse{Items: wantHs},
name: "List Many Host Sets",
req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId()},
res: &pbs.ListHostSetsResponse{Items: wantHs},
},
{
name: "List No Host Sets",
hostCatalogId: hcNoHosts.GetPublicId(),
res: &pbs.ListHostSetsResponse{},
name: "List No Host Sets",
req: &pbs.ListHostSetsRequest{HostCatalogId: hcNoHosts.GetPublicId()},
res: &pbs.ListHostSetsResponse{},
},
{
name: "Filter To One Host Set",
req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, wantHs[1].GetId())},
res: &pbs.ListHostSetsResponse{Items: wantHs[1:2]},
},
{
name: "Filter To No Host Sets",
req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"/item/name"=="doesnt match"`},
res: &pbs.ListHostSetsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
@@ -167,12 +182,14 @@ func TestList(t *testing.T) {
s, err := host_sets.NewService(repoFn)
require.NoError(err, "Couldn't create new host set service.")
got, gErr := s.ListHostSets(auth.DisabledAuthTestContext(auth.WithScopeId(proj.GetPublicId())), &pbs.ListHostSetsRequest{HostCatalogId: tc.hostCatalogId})
got, gErr := s.ListHostSets(auth.DisabledAuthTestContext(auth.WithScopeId(proj.GetPublicId())), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListHostSets(%q) got error %v, wanted %v", tc.hostCatalogId, gErr, tc.err)
assert.True(errors.Is(gErr, tc.err), "ListHostSets(%q) got error %v, wanted %v", tc.req, gErr, tc.err)
} else {
require.NoError(gErr)
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostSets(%q) got response %q, wanted %q", tc.hostCatalogId, got, tc.res)
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostSets(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
@@ -77,6 +77,10 @@ func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs
if err != nil {
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.Host, 0, len(hl))
res := &perms.Resource{
ScopeId: authResults.Scope.Id,
@@ -86,7 +90,10 @@ func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs
for _, item := range hl {
item.Scope = authResults.Scope
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
@@ -460,6 +467,9 @@ func validateListRequest(req *pbs.ListHostsRequest) error {
if !handlers.ValidId(static.HostCatalogPrefix, req.GetHostCatalogId()) {
badFields["host_catalog_id"] = "The field is incorrectly formatted."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -140,20 +140,35 @@ func TestList(t *testing.T) {
}
cases := []struct {
name string
hostCatalogId string
res *pbs.ListHostsResponse
err error
name string
req *pbs.ListHostsRequest
res *pbs.ListHostsResponse
err error
}{
{
name: "List Many Hosts",
hostCatalogId: hc.GetPublicId(),
res: &pbs.ListHostsResponse{Items: wantHs},
name: "List Many Hosts",
req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId()},
res: &pbs.ListHostsResponse{Items: wantHs},
},
{
name: "List No Hosts",
hostCatalogId: hcNoHosts.GetPublicId(),
res: &pbs.ListHostsResponse{},
name: "List No Hosts",
req: &pbs.ListHostsRequest{HostCatalogId: hcNoHosts.GetPublicId()},
res: &pbs.ListHostsResponse{},
},
{
name: "Filter to One Host",
req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, wantHs[1].GetId())},
res: &pbs.ListHostsResponse{Items: wantHs[1:2]},
},
{
name: "Filter to No Hosts",
req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"/item/id"=="doesnt match"`},
res: &pbs.ListHostsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
@@ -162,12 +177,14 @@ func TestList(t *testing.T) {
s, err := hosts.NewService(repoFn)
require.NoError(err, "Couldn't create new host set service.")
got, gErr := s.ListHosts(auth.DisabledAuthTestContext(auth.WithScopeId(proj.GetPublicId())), &pbs.ListHostsRequest{HostCatalogId: tc.hostCatalogId})
got, gErr := s.ListHosts(auth.DisabledAuthTestContext(auth.WithScopeId(proj.GetPublicId())), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListHosts(%q) got error %v, wanted %v", tc.hostCatalogId, gErr, tc.err)
assert.True(errors.Is(gErr, tc.err), "ListHosts(%q) got error %v, wanted %v", tc.req, gErr, tc.err)
} else {
require.NoError(gErr)
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHosts(%q) got response %q, wanted %q", tc.hostCatalogId, got, tc.res)
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
@@ -92,6 +92,10 @@ func (s Service) ListRoles(ctx context.Context, req *pbs.ListRolesRequest) (*pbs
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.Role, 0, len(items))
res := &perms.Resource{
Type: resource.Role,
@@ -100,7 +104,10 @@ func (s Service) ListRoles(ctx context.Context, req *pbs.ListRolesRequest) (*pbs
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
@@ -686,6 +693,9 @@ func validateListRequest(req *pbs.ListRolesRequest) error {
req.GetScopeId() != scope.Global.String() {
badFields["scope_id"] = "Improperly formatted field."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -222,6 +222,7 @@ func TestList(t *testing.T) {
name string
req *pbs.ListRolesRequest
res *pbs.ListRolesResponse
err error
}{
{
name: "List Many Role",
@@ -264,6 +265,26 @@ func TestList(t *testing.T) {
Items: totalRoles,
},
},
{
name: "Filter to org roles",
req: &pbs.ListRolesRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithRoles.GetPublicId())},
res: &pbs.ListRolesResponse{Items: wantOrgRoles},
},
{
name: "Filter to proj roles",
req: &pbs.ListRolesRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, pWithRoles.GetPublicId())},
res: &pbs.ListRolesResponse{Items: wantProjRoles},
},
{
name: "Filter to no roles",
req: &pbs.ListRolesRequest{ScopeId: pWithRoles.GetPublicId(), Filter: `"/item/id"=="doesnt match"`},
res: &pbs.ListRolesResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListRolesRequest{ScopeId: pWithRoles.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -272,7 +293,12 @@ func TestList(t *testing.T) {
require.NoError(err, "Couldn't create new role service.")
got, gErr := s.ListRoles(auth.DisabledAuthTestContext(auth.WithScopeId(tc.req.GetScopeId())), tc.req)
assert.NoError(gErr)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err))
} else {
require.NoError(gErr)
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListRoles(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
@@ -147,6 +147,10 @@ func (s Service) ListScopes(ctx context.Context, req *pbs.ListScopesRequest) (*p
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.Scope, 0, len(pl))
res := &perms.Resource{
Type: resource.Scope,
@@ -155,7 +159,10 @@ func (s Service) ListScopes(ctx context.Context, req *pbs.ListScopesRequest) (*p
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
if err := populateCollectionAuthorizedActions(ctx, authResults, item); err != nil {
return nil, err
@@ -583,6 +590,9 @@ func validateListRequest(req *pbs.ListScopesRequest) error {
if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(scope.Org.Prefix(), req.GetScopeId()) {
badFields["scope_id"] = "Must be 'global' or a valid org scope id when listing."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
@@ -283,6 +283,12 @@ func TestList(t *testing.T) {
req: &pbs.ListScopesRequest{ScopeId: p1.GetPublicId()},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Filter To Single Org",
scopeId: scope.Global.String(),
req: &pbs.ListScopesRequest{ScopeId: "global", Filter: fmt.Sprintf(`"/item/id"==%q`, initialOrgs[1].GetId())},
res: &pbs.ListScopesResponse{Items: initialOrgs[1:2]},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -368,6 +374,23 @@ func TestList(t *testing.T) {
req: &pbs.ListScopesRequest{ScopeId: scope.Global.String(), Recursive: true},
res: &pbs.ListScopesResponse{Items: totalScopes},
},
{
name: "Filter To Orgs",
scopeId: scope.Global.String(),
req: &pbs.ListScopesRequest{ScopeId: scope.Global.String(), Recursive: true, Filter: fmt.Sprintf(`"/item/scope/type"==%q`, scope.Global.String())},
res: &pbs.ListScopesResponse{Items: wantOrgs},
},
{
name: "Filter To Projects",
scopeId: scope.Global.String(),
req: &pbs.ListScopesRequest{ScopeId: scope.Global.String(), Recursive: true, Filter: fmt.Sprintf(`"/item/scope/type"==%q`, scope.Org.String())},
res: &pbs.ListScopesResponse{Items: wantProjects},
},
{
name: "Filter Bad Format",
req: &pbs.ListScopesRequest{ScopeId: scope.Global.String(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -112,6 +112,10 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest)
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.Session, 0, len(seslist))
res := &perms.Resource{
Type: resource.Session,
@@ -120,19 +124,25 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest)
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
authorizedActions := authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res))
if len(authorizedActions) > 0 {
onlySelf := true
for _, v := range authorizedActions {
if v != action.ReadSelf && v != action.CancelSelf {
onlySelf = false
break
}
}
if !onlySelf || item.GetUserId() == authResults.UserId {
item.AuthorizedActions = authorizedActions.Strings()
finalItems = append(finalItems, item)
if len(authorizedActions) == 0 {
continue
}
onlySelf := true
for _, v := range authorizedActions {
if v != action.ReadSelf && v != action.CancelSelf {
onlySelf = false
break
}
}
if onlySelf && item.GetUserId() != authResults.UserId {
continue
}
item.AuthorizedActions = authorizedActions.Strings()
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
return &pbs.ListSessionsResponse{Items: finalItems}, nil
@@ -322,6 +332,9 @@ func validateListRequest(req *pbs.ListSessionsRequest) error {
!req.GetRecursive() {
badFields["scope_id"] = "This field must be a valid project scope ID or the list operation must be recursive."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -1,7 +1,10 @@
package sessions_test
import (
"context"
"errors"
"fmt"
"net/http/httptest"
"testing"
"time"
@@ -15,11 +18,13 @@ import (
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/servers"
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
"github.com/hashicorp/boundary/internal/servers/controller/handlers/sessions"
"github.com/hashicorp/boundary/internal/session"
"github.com/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
@@ -137,6 +142,90 @@ func TestGetSession(t *testing.T) {
}
}
func TestList_Self(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
wrap := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrap)
logger := hclog.New(nil)
iamRepo := iam.TestRepo(t, conn, wrap)
rw := db.New(conn)
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
sessRepoFn := func() (*session.Repository, error) {
return session.NewRepository(rw, rw, kms)
}
tokenRepoFn := func() (*authtoken.Repository, error) {
return authtoken.NewRepository(rw, rw, kms)
}
serversRepoFn := func() (*servers.Repository, error) {
return servers.NewRepository(rw, rw, kms)
}
o, pWithSessions := iam.TestScopes(t, iamRepo)
at := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
uId := at.GetIamUserId()
otherAt := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
hc := static.TestCatalogs(t, conn, pWithSessions.GetPublicId(), 1)[0]
hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0]
h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0]
static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h})
tar := target.TestTcpTarget(t, conn, pWithSessions.GetPublicId(), "test", target.WithHostSets([]string{hs.GetPublicId()}))
// By default a user can read/cancel their own sessions.
session.TestSession(t, conn, wrap, session.ComposedOf{
UserId: uId,
HostId: h.GetPublicId(),
TargetId: tar.GetPublicId(),
HostSetId: hs.GetPublicId(),
AuthTokenId: at.GetPublicId(),
ScopeId: pWithSessions.GetPublicId(),
Endpoint: "tcp://127.0.0.1:22",
})
s, err := sessions.NewService(sessRepoFn, iamRepoFn)
require.NoError(t, err, "Couldn't create new session service.")
cases := []struct {
name string
requester *authtoken.AuthToken
count int
}{
{
name: "List Self Sessions",
requester: at,
count: 1,
},
{
name: "Can't List Others Sessions",
requester: otherAt,
count: 0,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Setup the auth request information
req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/sessions?scope_id=%s", pWithSessions.GetPublicId()), nil)
requestInfo := auth.RequestInfo{
Path: req.URL.Path,
Method: req.Method,
TokenFormat: auth.AuthTokenTypeBearer,
PublicId: tc.requester.GetPublicId(),
Token: tc.requester.GetToken(),
}
ctx := auth.NewVerifierContext(context.Background(), logger, iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo)
got, err := s.ListSessions(ctx, &pbs.ListSessionsRequest{ScopeId: pWithSessions.GetPublicId()})
require.NoError(t, err)
assert.Equal(t, tc.count, len(got.GetItems()), got.GetItems())
})
}
}
func TestList(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
wrap := db.TestWrapper(t)
@@ -270,6 +359,29 @@ func TestList(t *testing.T) {
req: &pbs.ListSessionsRequest{ScopeId: scope.Global.String(), Recursive: true},
res: &pbs.ListSessionsResponse{Items: totalSession},
},
{
name: "Filter To Single Sessions",
req: &pbs.ListSessionsRequest{ScopeId: pWithSessions.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, totalSession[4].Id)},
res: &pbs.ListSessionsResponse{Items: totalSession[4:5]},
},
{
name: "Filter To Many Sessions",
req: &pbs.ListSessionsRequest{
ScopeId: scope.Global.String(), Recursive: true,
Filter: fmt.Sprintf(`"/item/scope/id" matches "^%s"`, pWithSessions.GetPublicId()[:8]),
},
res: &pbs.ListSessionsResponse{Items: wantSession},
},
{
name: "Filter To Nothing",
req: &pbs.ListSessionsRequest{ScopeId: pWithSessions.GetPublicId(), Filter: `"/item/id" == ""`},
res: &pbs.ListSessionsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListSessionsRequest{ScopeId: pWithSessions.GetPublicId(), Filter: `//badformat/`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -280,8 +392,11 @@ func TestList(t *testing.T) {
if tc.err != nil {
require.Error(t, gErr)
assert.True(t, errors.Is(gErr, tc.err), "ListSessions(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
} else {
require.NoError(t, gErr)
}
if tc.res != nil {
require.Equal(t, len(tc.res.GetItems()), len(got.GetItems()), "Didn't get expected number of sessions: %v", got.GetItems())
for i, wantSess := range tc.res.GetItems() {
assert.True(t, got.GetItems()[i].GetExpirationTime().AsTime().Sub(wantSess.GetExpirationTime().AsTime()) < 10*time.Millisecond)
wantSess.ExpirationTime = got.GetItems()[i].GetExpirationTime()
@@ -136,6 +136,10 @@ func (s Service) ListTargets(ctx context.Context, req *pbs.ListTargetsRequest) (
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.Target, 0, len(ul))
res := &perms.Resource{
Type: resource.Target,
@@ -144,7 +148,10 @@ func (s Service) ListTargets(ctx context.Context, req *pbs.ListTargetsRequest) (
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
@@ -947,6 +954,9 @@ func validateListRequest(req *pbs.ListTargetsRequest) error {
!req.GetRecursive() {
badFields["scope_id"] = "This field must be a valid project scope ID or the list operation must be recursive."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -210,6 +210,21 @@ func TestList(t *testing.T) {
req: &pbs.ListTargetsRequest{ScopeId: scope.Global.String(), Recursive: true},
res: &pbs.ListTargetsResponse{Items: totalTars},
},
{
name: "Filter To Many Targets",
req: &pbs.ListTargetsRequest{ScopeId: scope.Global.String(), Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, proj.GetPublicId())},
res: &pbs.ListTargetsResponse{Items: wantTars},
},
{
name: "Filter To No Targets",
req: &pbs.ListTargetsRequest{ScopeId: proj.GetPublicId(), Filter: `"/item/id"=="doesnt match"`},
res: &pbs.ListTargetsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListTargetsRequest{ScopeId: proj.GetPublicId(), Filter: `"/badformat/"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -221,6 +236,8 @@ func TestList(t *testing.T) {
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListTargets(%q) got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err)
} else {
require.NoError(gErr)
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListTargets(%q) scope %q, got response %q, wanted %q", tc.name, tc.req.GetScopeId(), got, tc.res)
})
@@ -89,6 +89,10 @@ func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs
if err != nil {
return nil, err
}
filter, err := handlers.NewFilter(req.GetFilter())
if err != nil {
return nil, err
}
finalItems := make([]*pb.User, 0, len(ul))
res := &perms.Resource{
Type: resource.User,
@@ -97,7 +101,10 @@ func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) > 0 {
if len(item.AuthorizedActions) == 0 {
continue
}
if filter.Match(item) {
finalItems = append(finalItems, item)
}
}
@@ -490,6 +497,9 @@ func validateListRequest(req *pbs.ListUsersRequest) error {
req.GetScopeId() != scope.Global.String() {
badFields["scope_id"] = "Must be 'global' or a valid org scope id when listing."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFields)
}
@@ -187,6 +187,21 @@ func TestList(t *testing.T) {
req: &pbs.ListUsersRequest{ScopeId: "global", Recursive: true},
res: &pbs.ListUsersResponse{Items: totalUsers},
},
{
name: "Filter Many Users",
req: &pbs.ListUsersRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithUsers.GetPublicId())},
res: &pbs.ListUsersResponse{Items: wantUsers},
},
{
name: "Filter To No Users",
req: &pbs.ListUsersRequest{ScopeId: oWithUsers.GetPublicId(), Filter: `"/item/id"=="doesntmatch"`},
res: &pbs.ListUsersResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListUsersRequest{ScopeId: oWithUsers.GetPublicId(), Filter: `"//id/"=="bad"`},
err: handlers.InvalidArgumentErrorf("bad format", nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {