Skip to content

Commit

Permalink
enhancement: Add ListPolicies filtering ability to cerbosctl get (#…
Browse files Browse the repository at this point in the history
…1649)

* enhancement: Add `ListPolicies` `filtering` support to `cerbosctl get`

Signed-off-by: Sam Lock <sam@swlock.co.uk>

* add docs

Signed-off-by: Sam Lock <sam@swlock.co.uk>

---------

Signed-off-by: Sam Lock <sam@swlock.co.uk>
  • Loading branch information
Sambigeara committed Jun 19, 2023
1 parent c2fcf27 commit f21ecf7
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 19 deletions.
1 change: 0 additions & 1 deletion cmd/cerbosctl/get/derivedroles/derived_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ cerbosctl get derived_roles derived_roles.my_derived_roles -ojson
# Get derived role policy definition as pretty json
cerbosctl get derived_roles derived_roles.my_derived_roles -oprettyjson`

//nolint:govet
type Cmd struct {
flagset.Filters
flagset.Format
Expand Down
52 changes: 39 additions & 13 deletions cmd/cerbosctl/get/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ func testGetCmd(clientCtx *cmdclient.Context, globals *flagset.Globals) func(*te
{strings.Split("get derived_roles --include-disabled", " "), false},
{strings.Split("get derived_roles --sort-by policyId", " "), false},
{strings.Split("get derived_roles --sort-by version", " "), true},
// regexp filtering
{strings.Split("get derived_roles --name-regexp=a --scope-regexp=a", " "), false},
{strings.Split("get derived_roles --name-regexp=a --scope-regexp=a --version-regexp=a", " "), true},
{strings.Split("get derived_roles --name=a --name-regexp=a", " "), true},
{strings.Split("get resource_policies --name-regexp=a --scope-regexp=a --version-regexp=a", " "), false},
{strings.Split("get resource_policies --name=a --name-regexp=a", " "), true},
{strings.Split("get resource_policies --version=a --version-regexp=a", " "), true},
{strings.Split("get principal_policies --name-regexp=a --scope-regexp=a --version-regexp=a", " "), false},
{strings.Split("get principal_policies --name=a --name-regexp=a", " "), true},
{strings.Split("get principal_policies --version=a --version-regexp=a", " "), true},
}
for _, tc := range testCases {
p := mustNew(t, &root.Cli{})
Expand Down Expand Up @@ -129,33 +139,41 @@ func testGetCmd(clientCtx *cmdclient.Context, globals *flagset.Globals) func(*te
})
t.Run("compare policy count", func(t *testing.T) {
testCases := []struct {
args []string
wantCount int
wantCountWithDisabled int
args []string
regexpArg string
wantCount int
wantCountWithDisabled int
wantCountWithRegexpFilter int
}{
{
args: []string{"principal_policy", "principal_policies", "pp"},
wantCount: policiesPerType * 3,
wantCountWithDisabled: policiesPerType * 4,
args: []string{"principal_policy", "principal_policies", "pp"},
wantCount: policiesPerType * 3,
wantCountWithDisabled: policiesPerType * 4,
regexpArg: "--scope-regexp=acme",
wantCountWithRegexpFilter: policiesPerType * 2,
},
{
args: []string{"derived_role", "derived_roles", "dr"},
wantCount: policiesPerType,
wantCountWithDisabled: policiesPerType * 2,
args: []string{"derived_role", "derived_roles", "dr"},
wantCount: policiesPerType,
wantCountWithDisabled: policiesPerType * 2,
regexpArg: "--name-regexp=my_derived_",
wantCountWithRegexpFilter: policiesPerType * 2,
},
{
args: []string{"resource_policy", "resource_policies", "rp"},
wantCount: policiesPerType * 4,
wantCountWithDisabled: policiesPerType * 5,
args: []string{"resource_policy", "resource_policies", "rp"},
wantCount: policiesPerType * 4,
wantCountWithDisabled: policiesPerType * 5,
regexpArg: "--scope-regexp=acme",
wantCountWithRegexpFilter: policiesPerType * 3,
},
}

for _, tc := range testCases {
for _, arg := range tc.args {
p := mustNew(t, &root.Cli{})

out := bytes.NewBufferString("")
p.Stdout = out

ctx, err := p.Parse([]string{"get", arg, "--no-headers"})
require.NoError(t, err)
err = ctx.Run(clientCtx, globals)
Expand All @@ -169,6 +187,14 @@ func testGetCmd(clientCtx *cmdclient.Context, globals *flagset.Globals) func(*te
err = ctx.Run(clientCtx, globals)
require.NoError(t, err)
require.Equal(t, tc.wantCountWithDisabled, noOfPoliciesInCmdOutput(t, out.String()))

out = bytes.NewBufferString("")
p.Stdout = out
ctx, err = p.Parse([]string{"get", arg, "--include-disabled", tc.regexpArg, "--no-headers"})
require.NoError(t, err)
err = ctx.Run(clientCtx, globals)
require.NoError(t, err)
require.Equal(t, tc.wantCountWithRegexpFilter, noOfPoliciesInCmdOutput(t, out.String()))
}
}
})
Expand Down
20 changes: 18 additions & 2 deletions cmd/cerbosctl/get/internal/flagset/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import (
"github.com/cerbos/cerbos/internal/policy"
)

//nolint:govet
type Filters struct {
Name []string `help:"Filter policies by name"`
NameRegexp string `help:"Filter policies by name, using regular expression"`
Version []string `help:"Filter policies by version"`
VersionRegexp string `help:"Filter policies by version, using regular expression"`
ScopeRegexp string `help:"Filter policies by scope, using regular expression"`
IncludeDisabled bool `help:"Include disabled policies"`
}

Expand All @@ -24,8 +28,20 @@ func (f Filters) Validate(kind policy.Kind, listing bool) error {
return fmt.Errorf("--name and --version flags are only available when listing")
}

if kind == policy.DerivedRolesKind && len(f.Version) > 0 {
return fmt.Errorf("--version flag is not available when listing derived roles")
if !listing && (f.NameRegexp != "" || f.VersionRegexp != "" || f.ScopeRegexp != "") {
return fmt.Errorf("--{name|version|scope}-regexp flags are only available when listing")
}

if kind == policy.DerivedRolesKind && (len(f.Version) > 0 || f.VersionRegexp != "") {
return fmt.Errorf("--version and --version-regexp flags are not available when listing derived roles")
}

if len(f.Name) > 0 && f.NameRegexp != "" {
return fmt.Errorf("--name and --name-regexp flags cannot be used together")
}

if len(f.Version) > 0 && f.VersionRegexp != "" {
return fmt.Errorf("--version and --version-regexp flags cannot be used together")
}

return nil
Expand Down
9 changes: 9 additions & 0 deletions cmd/cerbosctl/get/internal/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ func List(k *kong.Kong, c client.AdminClient, filters *flagset.Filters, format *
if filters.IncludeDisabled {
opts = append(opts, client.WithIncludeDisabled())
}
if filters.NameRegexp != "" {
opts = append(opts, client.WithNameRegexp(filters.NameRegexp))
}
if filters.ScopeRegexp != "" {
opts = append(opts, client.WithScopeRegexp(filters.ScopeRegexp))
}
if filters.VersionRegexp != "" {
opts = append(opts, client.WithVersionRegexp(filters.VersionRegexp))
}

policyIds, err := c.ListPolicies(context.Background(), opts...)
if err != nil {
Expand Down
1 change: 0 additions & 1 deletion cmd/cerbosctl/get/principalpolicy/principal_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ cerbosctl get principal_policies principal.donald_duck.default -ojson
# Get principal policy definition as pretty json
cerbosctl get principal_policies principal.donald_duck.default -oprettyjson`

//nolint:govet
type Cmd struct {
flagset.Filters
flagset.Format
Expand Down
1 change: 0 additions & 1 deletion cmd/cerbosctl/get/resourcepolicy/resource_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ cerbosctl get resource_policies resource.leave_request.default -ojson
# Get resource policy definition as pretty json
cerbosctl get resource_policies resource.leave_request.default -oprettyjson`

//nolint:govet
type Cmd struct {
flagset.Filters
flagset.Format
Expand Down
20 changes: 20 additions & 0 deletions docs/modules/cli/pages/cerbosctl.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ You can also retrieve individual policies or schemas by their identifiers and vi

You can filter the output using the `name` and `version` flags. Each flag accepts multiple comma-separated values which are OR'ed together. For example, `--name=a.yaml,b.yaml` matches policies that are either named `a.yaml` or `b.yaml`.

Separately, you can filter the output using the `name-regexp`, `version-regexp` and `scope-regexp` flags. Each flag accepts a regular expression string. These are separate from the `name` and `version` flags above, and cannot be used with their respective counterparts.

You can include disabled policies in the results by adding `--include-disabled` flag.

.List derived roles
Expand Down Expand Up @@ -256,12 +258,30 @@ cerbosctl get derived_roles --name my_policy,a_policy
cerbosctl get dr --name my_policy,a_policy
----

.List derived_roles where `name` is `my_policy` or `a_policy`, using regular expression
----
cerbosctl get derived_roles --name-regexp "^(my|a)_policy\$"
cerbosctl get dr --name-regexp "^(my|a)_policy\$"
----

.List principal_policies where `version` is `default` or `v1`
----
cerbosctl get principal_policies --version default,v1
cerbosctl get pp --version default,v1
----

.List principal_policies where `version` is `default` or `v1`, using regular expression
----
cerbosctl get principal_policies --version-regexp "(default|v1)"
cerbosctl get pp --version-regexp "(default|v1)"
----

.List resource_policies where `scope` includes the substring `foo`, using regular expression
----
cerbosctl get resource_policies --scope-regexp foo
cerbosctl get rp --scope-regexp foo
----

.List derived_roles and sort by column `policyId` or `name`
----
cerbosctl get derived_roles --sort-by policyId
Expand Down
2 changes: 1 addition & 1 deletion internal/svc/admin_svc.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func (cas *CerbosAdminService) ListPolicies(ctx context.Context, req *requestv1.
// We've historically supported ListPolicies on non-mutable stores, but later introduced filters are not scalable.
// Therefore, if any of the filters in question are passed and the store is not mutable, we reject the request.
if _, ok := cas.store.(storage.MutableStore); !ok && (req.NameRegexp != "" || req.ScopeRegexp != "" || req.VersionRegexp != "") {
return nil, status.Error(codes.Unimplemented, "Store does not support filters")
return nil, status.Error(codes.Unimplemented, "Store does not support regexp filters")
}

filterParams := storage.ListPolicyIDsParams{
Expand Down

0 comments on commit f21ecf7

Please sign in to comment.