From 138a1cd9976a3955ff01a0d7326e957e0d07744a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Durgun?= Date: Fri, 17 Feb 2023 14:25:22 +0300 Subject: [PATCH] enhancement: Add includeDisabled to Admin API and schema deletion to cerbosctl (#1463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhancement: Add includeDisabled to Admin API and schema deletion to cerbosctl Signed-off-by: Oğuzhan Durgun * Address reviews Signed-off-by: Oğuzhan Durgun * Address reviews Signed-off-by: Oğuzhan Durgun --------- Signed-off-by: Oğuzhan Durgun --- .../cerbos/request/v1/hashpb_helpers.pb.go | 4 + api/genpb/cerbos/request/v1/request.pb.go | 18 +- .../cerbos/request/v1/request.pb.validate.go | 2 + .../cerbos/request/v1/request_hashpb.pb.go | 8 + .../cerbos/request/v1/request_vtproto.pb.go | 33 +++ api/genpb/cerbos/svc/v1/svc.pb.gw.go | 18 ++ api/public/cerbos/request/v1/request.proto | 4 + client/admin.go | 11 +- client/admin_test.go | 2 +- client/model.go | 10 + cmd/cerbosctl/del/delete.go | 8 + cmd/cerbosctl/del/delete_test.go | 214 ++++++++++++++++++ cmd/cerbosctl/del/internal/schema/schema.go | 28 +++ cmd/cerbosctl/del/schema.go | 45 ++++ cmd/cerbosctl/disable/disable_test.go | 42 ++-- cmd/cerbosctl/disable/policy.go | 2 +- .../get/derivedroles/derived_roles.go | 6 + cmd/cerbosctl/get/get_test.go | 36 ++- cmd/cerbosctl/get/internal/flagset/filter.go | 9 +- cmd/cerbosctl/get/internal/policy/filter.go | 18 +- .../get/internal/policy/filter_test.go | 2 +- cmd/cerbosctl/get/internal/policy/policy.go | 11 +- .../get/principalpolicy/principal_policy.go | 1 + .../get/resourcepolicy/resource_policy.go | 1 + cmd/cerbosctl/root/root.go | 2 + docs/modules/api/pages/admin_api.adoc | 22 +- docs/modules/cli/pages/cerbosctl.adoc | 21 ++ internal/compile/manager_test.go | 2 +- internal/storage/blob/cloner_test.go | 1 + internal/storage/blob/store.go | 2 +- internal/storage/db/internal/db.go | 18 +- internal/storage/db/internal/tests.go | 2 +- internal/storage/disk/disk.go | 2 +- internal/storage/git/store.go | 2 +- internal/storage/internal/tests.go | 8 +- internal/storage/store.go | 2 +- internal/svc/admin_svc.go | 2 +- internal/test/mocks/Store.go | 14 +- internal/test/policy.go | 18 ++ .../resource_policies/disabled_policy_01.yaml | 21 ++ .../v1/ListPoliciesRequest.schema.json | 7 +- .../openapiv2/cerbos/svc/v1/svc.swagger.json | 9 + 42 files changed, 614 insertions(+), 74 deletions(-) create mode 100644 cmd/cerbosctl/del/delete.go create mode 100644 cmd/cerbosctl/del/delete_test.go create mode 100644 cmd/cerbosctl/del/internal/schema/schema.go create mode 100644 cmd/cerbosctl/del/schema.go create mode 100644 internal/test/testdata/store/resource_policies/disabled_policy_01.yaml diff --git a/api/genpb/cerbos/request/v1/hashpb_helpers.pb.go b/api/genpb/cerbos/request/v1/hashpb_helpers.pb.go index 6833a205f..0ea89d003 100644 --- a/api/genpb/cerbos/request/v1/hashpb_helpers.pb.go +++ b/api/genpb/cerbos/request/v1/hashpb_helpers.pb.go @@ -806,6 +806,10 @@ func cerbos_request_v1_ListAuditLogEntriesRequest_hashpb_sum(m *ListAuditLogEntr } func cerbos_request_v1_ListPoliciesRequest_hashpb_sum(m *ListPoliciesRequest, hasher hash.Hash, ignore map[string]struct{}) { + if _, ok := ignore["cerbos.request.v1.ListPoliciesRequest.include_disabled"]; !ok { + _, _ = hasher.Write(protowire.AppendVarint(nil, protowire.EncodeBool(m.IncludeDisabled))) + + } } func cerbos_request_v1_ListSchemasRequest_hashpb_sum(m *ListSchemasRequest, hasher hash.Hash, ignore map[string]struct{}) { diff --git a/api/genpb/cerbos/request/v1/request.pb.go b/api/genpb/cerbos/request/v1/request.pb.go index 190e4509c..c065a3956 100644 --- a/api/genpb/cerbos/request/v1/request.pb.go +++ b/api/genpb/cerbos/request/v1/request.pb.go @@ -1156,6 +1156,8 @@ type ListPoliciesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + + IncludeDisabled bool `protobuf:"varint,1,opt,name=include_disabled,json=includeDisabled,proto3" json:"include_disabled,omitempty"` } func (x *ListPoliciesRequest) Reset() { @@ -1190,6 +1192,13 @@ func (*ListPoliciesRequest) Descriptor() ([]byte, []int) { return file_cerbos_request_v1_request_proto_rawDescGZIP(), []int{15} } +func (x *ListPoliciesRequest) GetIncludeDisabled() bool { + if x != nil { + return x.IncludeDisabled + } + return false +} + type GetPolicyRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2267,8 +2276,13 @@ var file_cerbos_request_v1_request_proto_rawDesc = []byte{ 0x74, 0x65, 0x72, 0x12, 0x03, 0xf8, 0x42, 0x01, 0x22, 0x2f, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x3a, 0x1a, 0x92, 0x41, 0x17, 0x0a, 0x15, 0x32, 0x13, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x66, - 0x6f, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x33, 0x0a, 0x13, 0x4c, 0x69, 0x73, - 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x6f, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x82, 0x01, 0x0a, 0x13, 0x4c, 0x69, + 0x73, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x4d, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x22, 0x92, 0x41, 0x1b, + 0x32, 0x19, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x20, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x20, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0xe2, 0x41, 0x01, 0x01, 0x52, + 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x3a, 0x1c, 0x92, 0x41, 0x19, 0x0a, 0x17, 0x32, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x84, 0x02, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, diff --git a/api/genpb/cerbos/request/v1/request.pb.validate.go b/api/genpb/cerbos/request/v1/request.pb.validate.go index 6724d718d..507c78d69 100644 --- a/api/genpb/cerbos/request/v1/request.pb.validate.go +++ b/api/genpb/cerbos/request/v1/request.pb.validate.go @@ -3047,6 +3047,8 @@ func (m *ListPoliciesRequest) validate(all bool) error { var errors []error + // no validation rules for IncludeDisabled + if len(errors) > 0 { return ListPoliciesRequestMultiError(errors) } diff --git a/api/genpb/cerbos/request/v1/request_hashpb.pb.go b/api/genpb/cerbos/request/v1/request_hashpb.pb.go index 7c425df02..e888526fe 100644 --- a/api/genpb/cerbos/request/v1/request_hashpb.pb.go +++ b/api/genpb/cerbos/request/v1/request_hashpb.pb.go @@ -152,6 +152,14 @@ func (m *ListAuditLogEntriesRequest_TimeRange) HashPB(hasher hash.Hash, ignore m } } +// HashPB computes a hash of the message using the given hash function +// The ignore set must contain fully-qualified field names (pkg.msg.field) that should be ignored from the hash +func (m *ListPoliciesRequest) HashPB(hasher hash.Hash, ignore map[string]struct{}) { + if m != nil { + cerbos_request_v1_ListPoliciesRequest_hashpb_sum(m, hasher, ignore) + } +} + // HashPB computes a hash of the message using the given hash function // The ignore set must contain fully-qualified field names (pkg.msg.field) that should be ignored from the hash func (m *GetPolicyRequest) HashPB(hasher hash.Hash, ignore map[string]struct{}) { diff --git a/api/genpb/cerbos/request/v1/request_vtproto.pb.go b/api/genpb/cerbos/request/v1/request_vtproto.pb.go index b03f90b0c..eec9f5d3c 100644 --- a/api/genpb/cerbos/request/v1/request_vtproto.pb.go +++ b/api/genpb/cerbos/request/v1/request_vtproto.pb.go @@ -1499,6 +1499,16 @@ func (m *ListPoliciesRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.IncludeDisabled { + i-- + if m.IncludeDisabled { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x8 + } return len(dAtA) - i, nil } @@ -2431,6 +2441,9 @@ func (m *ListPoliciesRequest) SizeVT() (n int) { } var l int _ = l + if m.IncludeDisabled { + n += 2 + } n += len(m.unknownFields) return n } @@ -5831,6 +5844,26 @@ func (m *ListPoliciesRequest) UnmarshalVT(dAtA []byte) error { return fmt.Errorf("proto: ListPoliciesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field IncludeDisabled", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.IncludeDisabled = bool(v != 0) default: iNdEx = preIndex skippy, err := skip(dAtA[iNdEx:]) diff --git a/api/genpb/cerbos/svc/v1/svc.pb.gw.go b/api/genpb/cerbos/svc/v1/svc.pb.gw.go index 3c8983683..38969af7d 100644 --- a/api/genpb/cerbos/svc/v1/svc.pb.gw.go +++ b/api/genpb/cerbos/svc/v1/svc.pb.gw.go @@ -288,10 +288,21 @@ func local_request_CerbosAdminService_AddOrUpdatePolicy_1(ctx context.Context, m } +var ( + filter_CerbosAdminService_ListPolicies_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + func request_CerbosAdminService_ListPolicies_0(ctx context.Context, marshaler runtime.Marshaler, client CerbosAdminServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq requestv1.ListPoliciesRequest var metadata runtime.ServerMetadata + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_CerbosAdminService_ListPolicies_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListPolicies(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err @@ -301,6 +312,13 @@ func local_request_CerbosAdminService_ListPolicies_0(ctx context.Context, marsha var protoReq requestv1.ListPoliciesRequest var metadata runtime.ServerMetadata + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_CerbosAdminService_ListPolicies_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListPolicies(ctx, &protoReq) return msg, metadata, err diff --git a/api/public/cerbos/request/v1/request.proto b/api/public/cerbos/request/v1/request.proto index 85c822ec1..644fd6ad5 100644 --- a/api/public/cerbos/request/v1/request.proto +++ b/api/public/cerbos/request/v1/request.proto @@ -517,6 +517,10 @@ message ListPoliciesRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: {description: "List policies request"} }; + bool include_disabled = 1 [ + (google.api.field_behavior) = OPTIONAL, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Include disabled policies"} + ]; } message GetPolicyRequest { diff --git a/client/admin.go b/client/admin.go index 111618494..0b419e79b 100644 --- a/client/admin.go +++ b/client/admin.go @@ -28,7 +28,7 @@ const ( type AdminClient interface { AddOrUpdatePolicy(ctx context.Context, policies *PolicySet) error AuditLogs(ctx context.Context, opts AuditLogOptions) (<-chan *AuditLogEntry, error) - ListPolicies(ctx context.Context) ([]string, error) + ListPolicies(ctx context.Context, opts ...ListPoliciesOption) ([]string, error) GetPolicy(ctx context.Context, ids ...string) ([]*policyv1.Policy, error) DisablePolicy(ctx context.Context, ids ...string) (uint32, error) AddOrUpdateSchema(ctx context.Context, schemas *SchemaSet) error @@ -175,8 +175,13 @@ func (c *GrpcAdminClient) auditLogs(ctx context.Context, opts AuditLogOptions) ( return resp, nil } -func (c *GrpcAdminClient) ListPolicies(ctx context.Context) ([]string, error) { - req := &requestv1.ListPoliciesRequest{} +func (c *GrpcAdminClient) ListPolicies(ctx context.Context, opts ...ListPoliciesOption) ([]string, error) { + req := &requestv1.ListPoliciesRequest{ + IncludeDisabled: false, + } + for _, opt := range opts { + opt(req) + } if err := req.Validate(); err != nil { return nil, fmt.Errorf("could not validate list policies request: %w", err) } diff --git a/client/admin_test.go b/client/admin_test.go index e1390ff37..163a7e58f 100644 --- a/client/admin_test.go +++ b/client/admin_test.go @@ -118,7 +118,7 @@ func TestListPolicies(t *testing.T) { require.NoError(t, ac.AddOrUpdatePolicy(context.Background(), ps)) t.Run("should get the list of policies", func(t *testing.T) { - have, err := ac.ListPolicies(context.Background()) + have, err := ac.ListPolicies(context.Background(), WithIncludeDisabled()) require.NoError(t, err) require.NotEmpty(t, have) diff --git a/client/model.go b/client/model.go index 9a0b82ed1..e3d9c54e9 100644 --- a/client/model.go +++ b/client/model.go @@ -1225,3 +1225,13 @@ func (e *AuditLogEntry) DecisionLog() (*auditv1.DecisionLogEntry, error) { type PlanResourcesResponse struct { *responsev1.PlanResourcesResponse } + +type ( + ListPoliciesOption func(*requestv1.ListPoliciesRequest) +) + +func WithIncludeDisabled() ListPoliciesOption { + return func(request *requestv1.ListPoliciesRequest) { + request.IncludeDisabled = true + } +} diff --git a/cmd/cerbosctl/del/delete.go b/cmd/cerbosctl/del/delete.go new file mode 100644 index 000000000..501f865af --- /dev/null +++ b/cmd/cerbosctl/del/delete.go @@ -0,0 +1,8 @@ +// Copyright 2021-2023 Zenauth Ltd. +// SPDX-License-Identifier: Apache-2.0 + +package del + +type Cmd struct { + Schema SchemaCmd `cmd:"" aliases:"schemas,s"` +} diff --git a/cmd/cerbosctl/del/delete_test.go b/cmd/cerbosctl/del/delete_test.go new file mode 100644 index 000000000..a52982015 --- /dev/null +++ b/cmd/cerbosctl/del/delete_test.go @@ -0,0 +1,214 @@ +// Copyright 2021-2023 Zenauth Ltd. +// SPDX-License-Identifier: Apache-2.0 + +//go:build !race + +package del_test + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/alecthomas/kong" + "github.com/stretchr/testify/require" + + "github.com/cerbos/cerbos/client" + "github.com/cerbos/cerbos/client/testutil" + cmdclient "github.com/cerbos/cerbos/cmd/cerbosctl/internal/client" + "github.com/cerbos/cerbos/cmd/cerbosctl/internal/flagset" + "github.com/cerbos/cerbos/cmd/cerbosctl/root" + "github.com/cerbos/cerbos/internal/schema" + "github.com/cerbos/cerbos/internal/test" +) + +const ( + adminUsername = "cerbos" + adminPassword = "cerbosAdmin" + readyTimeout = 60 * time.Second + timeout = 30 * time.Second + readyPollInterval = 50 * time.Millisecond +) + +func TestDeleteCmd(t *testing.T) { + s := mkServer(t) + defer s.Stop() //nolint:errcheck + + globals := mkGlobals(t, s.GRPCAddr()) + ctx, _ := context.WithTimeout(context.Background(), timeout) + cctx := mkClients(t, globals) + loadSchemas(t, cctx.AdminClient) + testDeleteCmd(ctx, cctx, globals)(t) +} + +func testDeleteCmd(ctx context.Context, cctx *cmdclient.Context, globals *flagset.Globals) func(*testing.T) { + //nolint:thelper + return func(t *testing.T) { + t.Run("cerbosctl delete", func(t *testing.T) { + t.Run("no arguments provided", func(t *testing.T) { + p := mustNew(t, &root.Cli{}) + _, err := p.Parse([]string{"delete"}) + require.Error(t, err) + }) + t.Run("possible arguments after delete command", func(t *testing.T) { + testCases := []struct { + args []string + wantErr bool + }{ + { + []string{"schemas", "schema", "s"}, + false, + }, + } + + for _, tc := range testCases { + for _, arg := range tc.args { + cli := root.Cli{} + p := mustNew(t, &cli) + _, err := p.Parse([]string{"delete", arg, "principal.json"}) + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } + } + }) + t.Run("delete and check", func(t *testing.T) { + testCases := []struct { + schemaID string + }{ + { + schemaID: "address.json", + }, + { + schemaID: "complex_object.json", + }, + } + for idx, tc := range testCases { + t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) { + p := mustNew(t, &root.Cli{}) + out := bytes.NewBufferString("") + p.Stdout = out + + schemas, err := cctx.AdminClient.GetSchema(ctx, tc.schemaID) + require.NoError(t, err) + require.NotNil(t, schemas) + require.NotNil(t, schemas[0]) + + kctx, err := p.Parse([]string{"delete", "schema", tc.schemaID}) + require.NoError(t, err) + err = kctx.Run(cctx, globals) + require.NoError(t, err) + + require.Contains(t, out.String(), "Number of schemas deleted is 1") + + schemas, err = cctx.AdminClient.GetSchema(ctx, tc.schemaID) + require.Error(t, err) + require.Nil(t, schemas) + }) + } + }) + t.Run("delete nonexisting schema", func(t *testing.T) { + p := mustNew(t, &root.Cli{}) + out := bytes.NewBufferString("") + p.Stdout = out + + kctx, err := p.Parse([]string{"delete", "schema", "nonexistent.json"}) + require.NoError(t, err) + err = kctx.Run(cctx, globals) + require.NoError(t, err) + }) + }) + } +} + +func loadSchemas(t *testing.T, ac client.AdminClient) { + t.Helper() + + fsys := os.DirFS(test.PathToDir(t, filepath.Join("schema", "fs", schema.Directory))) + + s, err := schema.ReadSchemaFromFile(fsys, "address.json") + require.NoError(t, err) + s2, err := schema.ReadSchemaFromFile(fsys, "complex_object.json") + require.NoError(t, err) + + ss := client.NewSchemaSet() + ss.AddSchemas(s) + ss.AddSchemas(s2) + require.NoError(t, ac.AddOrUpdateSchema(context.Background(), ss)) +} + +func mkServerOpts(t *testing.T) []testutil.ServerOpt { + t.Helper() + + serverOpts := []testutil.ServerOpt{ + testutil.WithPolicyRepositorySQLite3(fmt.Sprintf("%s?_fk=true", filepath.Join(t.TempDir(), "cerbos.db"))), + testutil.WithAdminAPI(adminUsername, adminPassword), + } + + return serverOpts +} + +func mkServer(t *testing.T) *testutil.ServerInfo { + t.Helper() + + s, err := testutil.StartCerbosServer(mkServerOpts(t)...) + require.NoError(t, err) + require.Eventually(t, serverIsReady(s), readyTimeout, readyPollInterval) + + return s +} + +func serverIsReady(s *testutil.ServerInfo) func() bool { + return func() bool { + ctx, cancelFunc := context.WithTimeout(context.Background(), readyPollInterval) + defer cancelFunc() + + ready, err := s.IsReady(ctx) + if err != nil { + return false + } + + return ready + } +} + +func mkGlobals(t *testing.T, address string) *flagset.Globals { + t.Helper() + + return &flagset.Globals{ + Server: address, + Username: adminUsername, + Password: adminPassword, + Plaintext: true, + } +} + +func mkClients(t *testing.T, globals *flagset.Globals) *cmdclient.Context { + t.Helper() + + c, err := cmdclient.GetClient(globals) + require.NoError(t, err) + + ac, err := cmdclient.GetAdminClient(globals) + require.NoError(t, err) + + return &cmdclient.Context{Client: c, AdminClient: ac} +} + +func mustNew(t *testing.T, cli any) *kong.Kong { + t.Helper() + options := []kong.Option{ + kong.Name("cerbosctl"), + kong.Description("A CLI for managing Cerbos"), + kong.UsageOnError(), + } + parser, err := kong.New(cli, options...) + require.NoError(t, err) + return parser +} diff --git a/cmd/cerbosctl/del/internal/schema/schema.go b/cmd/cerbosctl/del/internal/schema/schema.go new file mode 100644 index 000000000..e002610fa --- /dev/null +++ b/cmd/cerbosctl/del/internal/schema/schema.go @@ -0,0 +1,28 @@ +// Copyright 2021-2023 Zenauth Ltd. +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/cerbos/cerbos/client" + "github.com/cerbos/cerbos/cmd/cerbosctl/internal" +) + +func Delete(c client.AdminClient, ids ...string) (uint32, error) { + var deletedSchemas uint32 + for idx := range ids { + if idx%internal.MaxIDPerReq == 0 { + idxEnd := internal.MinInt(idx+internal.MaxIDPerReq, len(ids)) + var err error + deleted, err := c.DeleteSchema(context.Background(), ids[idx:idxEnd]...) + if err != nil { + return 0, fmt.Errorf("error while deleting schema: %w", err) + } + deletedSchemas += deleted + } + } + return deletedSchemas, nil +} diff --git a/cmd/cerbosctl/del/schema.go b/cmd/cerbosctl/del/schema.go new file mode 100644 index 000000000..0047f4142 --- /dev/null +++ b/cmd/cerbosctl/del/schema.go @@ -0,0 +1,45 @@ +// Copyright 2021-2023 Zenauth Ltd. +// SPDX-License-Identifier: Apache-2.0 + +package del + +import ( + "fmt" + + "github.com/alecthomas/kong" + + "github.com/cerbos/cerbos/cmd/cerbosctl/del/internal/schema" + "github.com/cerbos/cerbos/cmd/cerbosctl/internal/client" +) + +const schemaCmdHelp = `# Delete schemas +cerbosctl delete schemas principal.json +cerbosctl delete schema principal.json +cerbosctl delete s principal.json + +# Delete multiple schemas +cerbosctl delete schemas principal.json leave_request.json +cerbosctl delete schema principal.json leave_request.json +cerbosctl delete s principal.json leave_request.json` + +type SchemaCmd struct { + SchemaIds []string `arg:"" name:"id" help:"list of schema ids to delete"` +} + +func (c *Cmd) Run(k *kong.Kong, ctx *client.Context) error { + if len(c.Schema.SchemaIds) == 0 { + return fmt.Errorf("no schema id(s) provided") + } + + deletedSchemas, err := schema.Delete(ctx.AdminClient, c.Schema.SchemaIds...) + if err != nil { + return fmt.Errorf("failed to delete schemas: %w", err) + } + + _, _ = fmt.Fprintf(k.Stdout, "Number of schemas deleted is %d", deletedSchemas) + return nil +} + +func (sc *SchemaCmd) Help() string { + return schemaCmdHelp +} diff --git a/cmd/cerbosctl/disable/disable_test.go b/cmd/cerbosctl/disable/disable_test.go index 196cf3ab9..36142dc68 100644 --- a/cmd/cerbosctl/disable/disable_test.go +++ b/cmd/cerbosctl/disable/disable_test.go @@ -70,7 +70,7 @@ func testDisableCmd(ctx context.Context, cctx *cmdclient.Context, globals *flags for _, arg := range tc.args { cli := root.Cli{} p := mustNew(t, &cli) - _, err := p.Parse([]string{"disable", arg}) + _, err := p.Parse([]string{"disable", arg, "resource.leave_request.vdefault"}) if tc.wantErr { require.Error(t, err) } else { @@ -81,28 +81,28 @@ func testDisableCmd(ctx context.Context, cctx *cmdclient.Context, globals *flags }) t.Run("disable and check", func(t *testing.T) { testCases := []struct { - policyKey string - wantErr bool + policyKey string + wantDisabled bool }{ { - policyKey: "derived_roles.my_derived_roles_1", - wantErr: false, + policyKey: "derived_roles.my_derived_roles_1", + wantDisabled: true, }, { - policyKey: "principal.donald_duck_1.vdefault", - wantErr: true, + policyKey: "principal.donald_duck_1.vdefault", + wantDisabled: false, }, { - policyKey: "principal.donald_duck_1.vdefault/acme.hr", - wantErr: false, + policyKey: "principal.donald_duck_1.vdefault/acme.hr", + wantDisabled: true, }, { - policyKey: "resource.leave_request_1.vdefault", - wantErr: true, + policyKey: "resource.leave_request_1.vdefault", + wantDisabled: false, }, { - policyKey: "resource.leave_request_1.vdefault/acme.hr.uk", - wantErr: false, + policyKey: "resource.leave_request_1.vdefault/acme.hr.uk", + wantDisabled: true, }, } for idx, tc := range testCases { @@ -120,19 +120,23 @@ func testDisableCmd(ctx context.Context, cctx *cmdclient.Context, globals *flags kctx, err := p.Parse([]string{"disable", "policy", tc.policyKey}) require.NoError(t, err) err = kctx.Run(cctx, globals) - if tc.wantErr { - require.Error(t, err) - } else { + if tc.wantDisabled { require.NoError(t, err) + } else { + require.Error(t, err) } policies, err = cctx.AdminClient.GetPolicy(ctx, tc.policyKey) require.NoError(t, err) - if tc.wantErr { + if tc.wantDisabled { require.NotNil(t, policies) - } else { - require.Nil(t, policies) + require.NotNil(t, policies[0]) + require.True(t, policies[0].Disabled) require.Contains(t, out.String(), "Number of policies disabled is 1") + } else { + require.NotNil(t, policies) + require.NotNil(t, policies[0]) + require.False(t, policies[0].Disabled) } }) } diff --git a/cmd/cerbosctl/disable/policy.go b/cmd/cerbosctl/disable/policy.go index 172f0e46f..d5f169d31 100644 --- a/cmd/cerbosctl/disable/policy.go +++ b/cmd/cerbosctl/disable/policy.go @@ -23,7 +23,7 @@ cerbosctl disable policy derived_roles.my_derived_roles resource.leave_request.d cerbosctl disable p derived_roles.my_derived_roles resource.leave_request.default` type PolicyCmd struct { - PolicyIds []string `arg:"" name:"id" optional:"" help:"list of policy ids to disable"` + PolicyIds []string `arg:"" name:"id" help:"list of policy ids to disable"` } func (c *Cmd) Run(k *kong.Kong, ctx *client.Context) error { diff --git a/cmd/cerbosctl/get/derivedroles/derived_roles.go b/cmd/cerbosctl/get/derivedroles/derived_roles.go index 39daa5e95..6d04df335 100644 --- a/cmd/cerbosctl/get/derivedroles/derived_roles.go +++ b/cmd/cerbosctl/get/derivedroles/derived_roles.go @@ -24,6 +24,11 @@ cerbosctl get derived_roles --name my_derived_roles cerbosctl get derived_roles --sort-by policyId cerbosctl get derived_roles --sort-by name +# List all policies including disabled policies +cerbosctl get derived_roles --include-disabled +cerbosctl get principal_policy --include-disabled +cerbosctl get resource_policy --include-disabled + # Get derived role policy definition (disk, git, blob stores) cerbosctl get derived_roles blog_derived_roles.yaml @@ -39,6 +44,7 @@ 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 diff --git a/cmd/cerbosctl/get/get_test.go b/cmd/cerbosctl/get/get_test.go index 0b00c0bb7..4cf20b81e 100644 --- a/cmd/cerbosctl/get/get_test.go +++ b/cmd/cerbosctl/get/get_test.go @@ -72,6 +72,8 @@ func testGetCmd(clientCtx *cmdclient.Context, globals *flagset.Globals) func(*te {strings.Split("get derived_roles --version=abc", " "), true}, {strings.Split("get derived_roles a.b.c --no-headers", " "), true}, {strings.Split("get derived_roles a.b.c --sort-by policyId", " "), true}, + {strings.Split("get derived_roles a.b.c --include-disabled", " "), true}, + {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}, } @@ -127,20 +129,24 @@ 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 + args []string + wantCount int + wantCountWithDisabled int }{ { - args: []string{"principal_policy", "principal_policies", "pp"}, - wantCount: policiesPerType * 3, + args: []string{"principal_policy", "principal_policies", "pp"}, + wantCount: policiesPerType * 3, + wantCountWithDisabled: policiesPerType * 4, }, { - args: []string{"derived_role", "derived_roles", "dr"}, - wantCount: policiesPerType, + args: []string{"derived_role", "derived_roles", "dr"}, + wantCount: policiesPerType, + wantCountWithDisabled: policiesPerType * 2, }, { - args: []string{"resource_policy", "resource_policies", "rp"}, - wantCount: policiesPerType * 4, + args: []string{"resource_policy", "resource_policies", "rp"}, + wantCount: policiesPerType * 4, + wantCountWithDisabled: policiesPerType * 5, }, } @@ -155,6 +161,14 @@ func testGetCmd(clientCtx *cmdclient.Context, globals *flagset.Globals) func(*te err = ctx.Run(clientCtx, globals) require.NoError(t, err) require.Equal(t, tc.wantCount, noOfPoliciesInCmdOutput(t, out.String())) + + out = bytes.NewBufferString("") + p.Stdout = out + ctx, err = p.Parse([]string{"get", arg, "--include-disabled", "--no-headers"}) + require.NoError(t, err) + err = ctx.Run(clientCtx, globals) + require.NoError(t, err) + require.Equal(t, tc.wantCountWithDisabled, noOfPoliciesInCmdOutput(t, out.String())) } } }) @@ -266,12 +280,16 @@ func loadPolicies(t *testing.T, ac client.AdminClient) { ps.AddPolicies(test.GenPrincipalPolicy(test.Suffix(strconv.Itoa(i)))) ps.AddPolicies(test.GenResourcePolicy(test.Suffix(strconv.Itoa(i)))) ps.AddPolicies(test.GenDerivedRoles(test.Suffix(strconv.Itoa(i)))) + + ps.AddPolicies(test.GenDisabledPrincipalPolicy(test.Suffix(fmt.Sprintf("_disabled_%d", i)))) + ps.AddPolicies(test.GenDisabledResourcePolicy(test.Suffix(fmt.Sprintf("_disabled_%d", i)))) + ps.AddPolicies(test.GenDisabledDerivedRoles(test.Suffix(fmt.Sprintf("_disabled_%d", i)))) + ps.AddPolicies(withScope(test.GenResourcePolicy(test.Suffix(strconv.Itoa(i))), "acme")) ps.AddPolicies(withScope(test.GenResourcePolicy(test.Suffix(strconv.Itoa(i))), "acme.hr")) ps.AddPolicies(withScope(test.GenResourcePolicy(test.Suffix(strconv.Itoa(i))), "acme.hr.uk")) ps.AddPolicies(withScope(test.GenPrincipalPolicy(test.Suffix(strconv.Itoa(i))), "acme")) ps.AddPolicies(withScope(test.GenPrincipalPolicy(test.Suffix(strconv.Itoa(i))), "acme.hr")) - require.NoError(t, ac.AddOrUpdatePolicy(context.Background(), ps)) } } diff --git a/cmd/cerbosctl/get/internal/flagset/filter.go b/cmd/cerbosctl/get/internal/flagset/filter.go index 00277389c..424d42445 100644 --- a/cmd/cerbosctl/get/internal/flagset/filter.go +++ b/cmd/cerbosctl/get/internal/flagset/filter.go @@ -10,11 +10,16 @@ import ( ) type Filters struct { - Name []string `help:"Filter policies by name"` - Version []string `help:"Filter policies by version"` + Name []string `help:"Filter policies by name"` + Version []string `help:"Filter policies by version"` + IncludeDisabled bool `help:"Include disabled policies"` } func (f Filters) Validate(kind policy.Kind, listing bool) error { + if !listing && f.IncludeDisabled { + return fmt.Errorf("--include-disabled is only available when listing") + } + if !listing && (len(f.Name) > 0 || len(f.Version) > 0) { return fmt.Errorf("--name and --version flags are only available when listing") } diff --git a/cmd/cerbosctl/get/internal/policy/filter.go b/cmd/cerbosctl/get/internal/policy/filter.go index 962cc5fed..d8577489b 100644 --- a/cmd/cerbosctl/get/internal/policy/filter.go +++ b/cmd/cerbosctl/get/internal/policy/filter.go @@ -10,13 +10,17 @@ import ( ) type filterDef struct { - names map[string]struct{} - versions map[string]struct{} - kind policy.Kind + names map[string]struct{} + versions map[string]struct{} + kind policy.Kind + includeDisabled bool } -func newFilterDef(kind policy.Kind, names, versions []string) *filterDef { - f := &filterDef{kind: kind} +func newFilterDef(kind policy.Kind, names, versions []string, includeDisabled bool) *filterDef { + f := &filterDef{ + kind: kind, + includeDisabled: includeDisabled, + } if len(names) > 0 { f.names = make(map[string]struct{}, len(names)) for _, n := range names { @@ -35,6 +39,10 @@ func newFilterDef(kind policy.Kind, names, versions []string) *filterDef { } func (fd *filterDef) filter(p policy.Wrapper) bool { + if !fd.includeDisabled && p.Disabled { + return false + } + if p.Kind != fd.kind { return false } diff --git a/cmd/cerbosctl/get/internal/policy/filter_test.go b/cmd/cerbosctl/get/internal/policy/filter_test.go index 957422a21..e54c9b354 100644 --- a/cmd/cerbosctl/get/internal/policy/filter_test.go +++ b/cmd/cerbosctl/get/internal/policy/filter_test.go @@ -100,7 +100,7 @@ func TestFilter(t *testing.T) { } func filter(policies []policy.Wrapper, names, versions []string, kind policy.Kind) []policy.Wrapper { - fd := newFilterDef(kind, names, versions) + fd := newFilterDef(kind, names, versions, true) filtered := make([]policy.Wrapper, 0, len(policies)) for _, p := range policies { if fd.filter(p) { diff --git a/cmd/cerbosctl/get/internal/policy/policy.go b/cmd/cerbosctl/get/internal/policy/policy.go index 70d696f33..0e540405e 100644 --- a/cmd/cerbosctl/get/internal/policy/policy.go +++ b/cmd/cerbosctl/get/internal/policy/policy.go @@ -33,7 +33,12 @@ func DoCmd(k *kong.Kong, ac client.AdminClient, kind policy.Kind, filters *flags } func List(k *kong.Kong, c client.AdminClient, filters *flagset.Filters, format *flagset.Format, sortFlags *flagset.Sort, kind policy.Kind) error { - policyIds, err := c.ListPolicies(context.Background()) + var opts []client.ListPoliciesOption + if filters.IncludeDisabled { + opts = append(opts, client.WithIncludeDisabled()) + } + + policyIds, err := c.ListPolicies(context.Background(), opts...) if err != nil { return fmt.Errorf("error while requesting policies: %w", err) } @@ -43,7 +48,7 @@ func List(k *kong.Kong, c client.AdminClient, filters *flagset.Filters, format * tw.SetHeader(getHeaders(kind)) } - fd := newFilterDef(kind, filters.Name, filters.Version) + fd := newFilterDef(kind, filters.Name, filters.Version, filters.IncludeDisabled) for idx := range policyIds { if idx%internal.MaxIDPerReq == 0 { @@ -81,7 +86,7 @@ func List(k *kong.Kong, c client.AdminClient, filters *flagset.Filters, format * func Get(k *kong.Kong, c client.AdminClient, format *flagset.Format, kind policy.Kind, ids ...string) error { foundPolicy := false - fd := newFilterDef(kind, nil, nil) + fd := newFilterDef(kind, nil, nil, true) for idx := range ids { if idx%internal.MaxIDPerReq == 0 { diff --git a/cmd/cerbosctl/get/principalpolicy/principal_policy.go b/cmd/cerbosctl/get/principalpolicy/principal_policy.go index ac3842403..491369c94 100644 --- a/cmd/cerbosctl/get/principalpolicy/principal_policy.go +++ b/cmd/cerbosctl/get/principalpolicy/principal_policy.go @@ -40,6 +40,7 @@ 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 diff --git a/cmd/cerbosctl/get/resourcepolicy/resource_policy.go b/cmd/cerbosctl/get/resourcepolicy/resource_policy.go index ec96c8c0b..c9bc5bbc8 100644 --- a/cmd/cerbosctl/get/resourcepolicy/resource_policy.go +++ b/cmd/cerbosctl/get/resourcepolicy/resource_policy.go @@ -40,6 +40,7 @@ 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 diff --git a/cmd/cerbosctl/root/root.go b/cmd/cerbosctl/root/root.go index 0c4fbb643..f092ab062 100644 --- a/cmd/cerbosctl/root/root.go +++ b/cmd/cerbosctl/root/root.go @@ -6,6 +6,7 @@ package root import ( "github.com/cerbos/cerbos/cmd/cerbosctl/audit" "github.com/cerbos/cerbos/cmd/cerbosctl/decisions" + "github.com/cerbos/cerbos/cmd/cerbosctl/del" "github.com/cerbos/cerbos/cmd/cerbosctl/disable" "github.com/cerbos/cerbos/cmd/cerbosctl/get" "github.com/cerbos/cerbos/cmd/cerbosctl/internal/flagset" @@ -38,6 +39,7 @@ type Cli struct { Version version.Cmd `cmd:"" help:"Show cerbosctl and PDP version"` Get get.Cmd `cmd:"" help:"List or view policies and schemas"` flagset.Globals + Delete del.Cmd `cmd:"" help:"Delete schemas"` Disable disable.Cmd `cmd:"" help:"Disable policies"` Put put.Cmd `cmd:"" help:"Put policies or schemas"` Decisions decisions.Cmd `cmd:"" help:"Interactive decision log viewer"` diff --git a/docs/modules/api/pages/admin_api.adoc b/docs/modules/api/pages/admin_api.adoc index 379bc3a17..2b23e23fb 100644 --- a/docs/modules/api/pages/admin_api.adoc +++ b/docs/modules/api/pages/admin_api.adoc @@ -133,12 +133,20 @@ NOTE: This endpoint is still under development and should be considered unstable Issue a GET request to the endpoint to list the policies available in the store. +Use `includeDisabled=true` query parameter in order to include disabled policies in the response. + [source,shell] ---- curl -k -u cerbos:cerbosAdmin \ 'https://localhost:3592/admin/policies?pretty' ---- +[source,shell] +---- +curl -k -u cerbos:cerbosAdmin \ + 'https://localhost:3592/admin/policies?pretty&includeDisabled=true' +---- + === Get Policies ---- @@ -149,7 +157,6 @@ NOTE: This endpoint is still under development and should be considered unstable Issue a GET request to the endpoint with the list of IDs (the `id` query parameter can be repeated multiple times) to retrieve. The list of IDs available in the store can be retrieved using the `ListPolicies` API call described above. - [source,shell] ---- curl -k -u cerbos:cerbosAdmin \ @@ -173,6 +180,14 @@ curl -k -u cerbos:cerbosAdmin -X DELETE \ 'https://localhost:3592/admin/policy?id=principal.donald_duck.vdefault&id=derived_roles.my_derived_roles' ---- +.Response +[source,json,linenums] +---- +{ + "disabledPolicies": 2 <1> +} +---- +<1> Number of policies disabled == Schema Management @@ -282,8 +297,11 @@ curl -k -u cerbos:cerbosAdmin -X DELETE \ .Response [source,json,linenums] ---- -{} +{ + "deletedSchemas": 2 <1> +} ---- +<1> Number of schemas deleted [#store-management] == Store Management diff --git a/docs/modules/cli/pages/cerbosctl.adoc b/docs/modules/cli/pages/cerbosctl.adoc index 2a6a19ce1..dc1bb4b66 100644 --- a/docs/modules/cli/pages/cerbosctl.adoc +++ b/docs/modules/cli/pages/cerbosctl.adoc @@ -159,6 +159,25 @@ Use the arrow keys (or Vim keys kbd:[h], kbd:[j], kbd:[k], kbd:[l]) to scroll ho cerbosctl decisions --tail=20 ---- +[#delete] +== `delete` + +This command deletes the schemas with the specified ids. + +.Delete schemas +---- +cerbosctl delete schemas principal.json +cerbosctl delete schema principal.json +cerbosctl delete s principal.json +---- + +.Delete multiple schemas +---- +cerbosctl delete schemas principal.json leave_request.json +cerbosctl delete schema principal.json leave_request.json +cerbosctl delete s principal.json leave_request.json +---- + [#disable] == `disable` @@ -188,6 +207,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`. +You can include disabled policies in the results by adding `--include-disabled` flag. + .List derived roles ---- cerbosctl get derived_roles diff --git a/internal/compile/manager_test.go b/internal/compile/manager_test.go index f33f54091..e2582da9e 100644 --- a/internal/compile/manager_test.go +++ b/internal/compile/manager_test.go @@ -288,7 +288,7 @@ func (ms *MockStore) Delete(ctx context.Context, ids ...namer.ModuleID) error { return args.Error(0) } -func (ms *MockStore) ListPolicyIDs(ctx context.Context) ([]string, error) { +func (ms *MockStore) ListPolicyIDs(ctx context.Context, _ bool) ([]string, error) { args := ms.MethodCalled("ListPolicyIDs", ctx) if res := args.Get(0); res == nil { return nil, args.Error(0) diff --git a/internal/storage/blob/cloner_test.go b/internal/storage/blob/cloner_test.go index adda4e37c..88df687a0 100644 --- a/internal/storage/blob/cloner_test.go +++ b/internal/storage/blob/cloner_test.go @@ -36,6 +36,7 @@ func TestCloneResult(t *testing.T) { "principal_policies/policy_02_acme.hr.yaml", "principal_policies/policy_02_acme.yaml", "principal_policies/policy_03.yaml", + "resource_policies/disabled_policy_01.yaml", "resource_policies/policy_01.yaml", "resource_policies/policy_02.yaml", "resource_policies/policy_03.yaml", diff --git a/internal/storage/blob/store.go b/internal/storage/blob/store.go index e206d103c..d2019783d 100644 --- a/internal/storage/blob/store.go +++ b/internal/storage/blob/store.go @@ -314,7 +314,7 @@ func (s *Store) GetDependents(_ context.Context, ids ...namer.ModuleID) (map[nam return s.idx.GetDependents(ids...) } -func (s *Store) ListPolicyIDs(ctx context.Context) ([]string, error) { +func (s *Store) ListPolicyIDs(ctx context.Context, _ bool) ([]string, error) { return s.idx.ListPolicyIDs(ctx) } diff --git a/internal/storage/db/internal/db.go b/internal/storage/db/internal/db.go index bd2c37d11..48ac20082 100644 --- a/internal/storage/db/internal/db.go +++ b/internal/storage/db/internal/db.go @@ -14,6 +14,7 @@ import ( "strings" "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" "github.com/jackc/pgtype" "go.opencensus.io/stats" "go.opencensus.io/tag" @@ -37,7 +38,7 @@ type DBStorage interface { GetDependents(ctx context.Context, ids ...namer.ModuleID) (map[namer.ModuleID][]namer.ModuleID, error) HasDescendants(ctx context.Context, ids ...namer.ModuleID) (map[namer.ModuleID]bool, error) Delete(ctx context.Context, ids ...namer.ModuleID) error - ListPolicyIDs(ctx context.Context) ([]string, error) + ListPolicyIDs(ctx context.Context, includeDisabled bool) ([]string, error) ListSchemaIDs(ctx context.Context) ([]string, error) AddOrUpdateSchema(ctx context.Context, schemas ...*schemav1.Schema) error Disable(ctx context.Context, policyKey ...string) (uint32, error) @@ -161,10 +162,7 @@ func (s *dbStorage) LoadPolicy(ctx context.Context, policyKey ...string) ([]*pol goqu.C(PolicyTblDisabledCol), goqu.C(PolicyTblDefinitionCol), ). - Where( - goqu.C(PolicyTblIDCol).In(moduleIDs), - goqu.C(PolicyTblDisabledCol).Neq(goqu.V(true)), - ). + Where(goqu.C(PolicyTblIDCol).In(moduleIDs)). ScanStructsContext(ctx, &recs); err != nil { return nil, fmt.Errorf("failed to get policies: %w", err) } @@ -173,6 +171,7 @@ func (s *dbStorage) LoadPolicy(ctx context.Context, policyKey ...string) ([]*pol for i, rec := range recs { pk := namer.PolicyKey(rec.Definition.Policy) wp := policy.Wrap(policy.WithMetadata(rec.Definition.Policy, "", nil, pk)) + wp.Disabled = rec.Disabled policies[i] = &wp } @@ -603,8 +602,13 @@ func (s *dbStorage) Delete(ctx context.Context, ids ...namer.ModuleID) error { return nil } -func (s *dbStorage) ListPolicyIDs(ctx context.Context) ([]string, error) { +func (s *dbStorage) ListPolicyIDs(ctx context.Context, includeDisabled bool) ([]string, error) { var policyCoords []namer.PolicyCoords + var whereExprs []exp.Expression + if !includeDisabled { + whereExprs = append(whereExprs, goqu.C(PolicyTblDisabledCol).Neq(goqu.V(true))) + } + err := s.db.From(PolicyTbl). Select( goqu.C(PolicyTblKindCol), @@ -612,7 +616,7 @@ func (s *dbStorage) ListPolicyIDs(ctx context.Context) ([]string, error) { goqu.C(PolicyTblVerCol), goqu.COALESCE(goqu.C(PolicyTblScopeCol), "").As(PolicyTblScopeCol), ). - Where(goqu.C(PolicyTblDisabledCol).Neq(goqu.V(true))). + Where(whereExprs...). Order( goqu.C(PolicyTblKindCol).Asc(), goqu.C(PolicyTblNameCol).Asc(), diff --git a/internal/storage/db/internal/tests.go b/internal/storage/db/internal/tests.go index 3c90e0516..b041a92f8 100644 --- a/internal/storage/db/internal/tests.go +++ b/internal/storage/db/internal/tests.go @@ -221,7 +221,7 @@ func TestSuite(store DBStorage) func(*testing.T) { t.Run("list_policies", func(t *testing.T) { t.Run("should be able to list policies", func(t *testing.T) { - have, err := store.ListPolicyIDs(ctx) + have, err := store.ListPolicyIDs(ctx, false) require.NoError(t, err) require.Len(t, have, len(policyList)) diff --git a/internal/storage/disk/disk.go b/internal/storage/disk/disk.go index c27b0cf32..aadda673b 100644 --- a/internal/storage/disk/disk.go +++ b/internal/storage/disk/disk.go @@ -91,7 +91,7 @@ func (s *Store) GetDependents(_ context.Context, ids ...namer.ModuleID) (map[nam return s.idx.GetDependents(ids...) } -func (s *Store) ListPolicyIDs(ctx context.Context) ([]string, error) { +func (s *Store) ListPolicyIDs(ctx context.Context, _ bool) ([]string, error) { return s.idx.ListPolicyIDs(ctx) } diff --git a/internal/storage/git/store.go b/internal/storage/git/store.go index b951dbb64..409314987 100644 --- a/internal/storage/git/store.go +++ b/internal/storage/git/store.go @@ -142,7 +142,7 @@ func (s *Store) GetDependents(_ context.Context, ids ...namer.ModuleID) (map[nam return s.idx.GetDependents(ids...) } -func (s *Store) ListPolicyIDs(ctx context.Context) ([]string, error) { +func (s *Store) ListPolicyIDs(ctx context.Context, _ bool) ([]string, error) { return s.idx.ListPolicyIDs(ctx) } diff --git a/internal/storage/internal/tests.go b/internal/storage/internal/tests.go index de839296a..a0078bc19 100644 --- a/internal/storage/internal/tests.go +++ b/internal/storage/internal/tests.go @@ -24,21 +24,21 @@ func TestSuiteReloadable(store storage.Store, addFn, deleteFn MutateStoreFn) fun r, ok := store.(storage.Reloadable) require.True(t, ok, "Store is not reloadable") - policies, err := store.ListPolicyIDs(context.Background()) + policies, err := store.ListPolicyIDs(context.Background(), false) require.NoError(t, err) require.Len(t, policies, 0) err = addFn() require.NoError(t, err) - policies, err = store.ListPolicyIDs(context.Background()) + policies, err = store.ListPolicyIDs(context.Background(), false) require.NoError(t, err) require.Len(t, policies, 0) err = r.Reload(context.Background()) require.NoError(t, err) - policies, err = store.ListPolicyIDs(context.Background()) + policies, err = store.ListPolicyIDs(context.Background(), false) require.NoError(t, err) require.NotZero(t, len(policies)) @@ -48,7 +48,7 @@ func TestSuiteReloadable(store storage.Store, addFn, deleteFn MutateStoreFn) fun err = r.Reload(context.Background()) require.NoError(t, err) - policies, err = store.ListPolicyIDs(context.Background()) + policies, err = store.ListPolicyIDs(context.Background(), false) require.NoError(t, err) require.Len(t, policies, 0) } diff --git a/internal/storage/store.go b/internal/storage/store.go index 753f4710f..262073e9e 100644 --- a/internal/storage/store.go +++ b/internal/storage/store.go @@ -96,7 +96,7 @@ type Store interface { // Driver is the name of the storage backend implementation. Driver() string // ListPolicyIDs returns the policy IDs in the store - ListPolicyIDs(context.Context) ([]string, error) + ListPolicyIDs(context.Context, bool) ([]string, error) // ListSchemaIDs returns the schema ids in the store ListSchemaIDs(context.Context) ([]string, error) // LoadSchema loads the given schema from the store. diff --git a/internal/svc/admin_svc.go b/internal/svc/admin_svc.go index b8bd569b9..02eade4e6 100644 --- a/internal/svc/admin_svc.go +++ b/internal/svc/admin_svc.go @@ -121,7 +121,7 @@ func (cas *CerbosAdminService) ListPolicies(ctx context.Context, req *requestv1. return nil, status.Error(codes.NotFound, "store is not configured") } - policyIds, err := cas.store.ListPolicyIDs(context.Background()) + policyIds, err := cas.store.ListPolicyIDs(context.Background(), req.IncludeDisabled) if err != nil { ctxzap.Extract(ctx).Error("Could not get policy ids", zap.Error(err)) return nil, status.Error(codes.Internal, "could not get policy ids") diff --git a/internal/test/mocks/Store.go b/internal/test/mocks/Store.go index 5b0a5bc9c..07945f77f 100644 --- a/internal/test/mocks/Store.go +++ b/internal/test/mocks/Store.go @@ -31,13 +31,13 @@ func (_m *Store) Driver() string { return r0 } -// ListPolicyIDs provides a mock function with given fields: _a0 -func (_m *Store) ListPolicyIDs(_a0 context.Context) ([]string, error) { - ret := _m.Called(_a0) +// ListPolicyIDs provides a mock function with given fields: _a0, _a1 +func (_m *Store) ListPolicyIDs(_a0 context.Context, _a1 bool) ([]string, error) { + ret := _m.Called(_a0, _a1) var r0 []string - if rf, ok := ret.Get(0).(func(context.Context) []string); ok { - r0 = rf(_a0) + if rf, ok := ret.Get(0).(func(context.Context, bool) []string); ok { + r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) @@ -45,8 +45,8 @@ func (_m *Store) ListPolicyIDs(_a0 context.Context) ([]string, error) { } var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) + if rf, ok := ret.Get(1).(func(context.Context, bool) error); ok { + r1 = rf(_a0, _a1) } else { r1 = ret.Error(1) } diff --git a/internal/test/policy.go b/internal/test/policy.go index 03297ad29..ec3d1c23c 100644 --- a/internal/test/policy.go +++ b/internal/test/policy.go @@ -120,6 +120,12 @@ func buildAndCondition(expr ...string) *policyv1.Condition { } } +func GenDisabledResourcePolicy(mod NameMod) *policyv1.Policy { + p := GenResourcePolicy(mod) + p.Disabled = true + return p +} + // GenResourcePolicy generates a sample resource policy with some names modified by the NameMod. func GenResourcePolicy(mod NameMod) *policyv1.Policy { return &policyv1.Policy{ @@ -245,6 +251,12 @@ func (ppb *PrincipalPolicyBuilder) Build() *policyv1.Policy { } } +func GenDisabledPrincipalPolicy(mod NameMod) *policyv1.Policy { + p := GenPrincipalPolicy(mod) + p.Disabled = true + return p +} + func GenPrincipalPolicy(mod NameMod) *policyv1.Policy { return &policyv1.Policy{ ApiVersion: "api.cerbos.dev/v1", @@ -316,6 +328,12 @@ func (drb *DerivedRolesBuilder) Build() *policyv1.Policy { } } +func GenDisabledDerivedRoles(mod NameMod) *policyv1.Policy { + p := GenDerivedRoles(mod) + p.Disabled = true + return p +} + func GenDerivedRoles(mod NameMod) *policyv1.Policy { return &policyv1.Policy{ ApiVersion: "api.cerbos.dev/v1", diff --git a/internal/test/testdata/store/resource_policies/disabled_policy_01.yaml b/internal/test/testdata/store/resource_policies/disabled_policy_01.yaml new file mode 100644 index 000000000..2ab44c042 --- /dev/null +++ b/internal/test/testdata/store/resource_policies/disabled_policy_01.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: "api.cerbos.dev/v1" +disabled: true +resourcePolicy: + version: "default" + resource: disabled_leave_request + rules: + - actions: ["*"] + effect: EFFECT_ALLOW + roles: + - support + - admin + + - actions: + - create + - view + - update + - delete + effect: EFFECT_ALLOW + derivedRoles: + - buyer diff --git a/schema/jsonschema/cerbos/request/v1/ListPoliciesRequest.schema.json b/schema/jsonschema/cerbos/request/v1/ListPoliciesRequest.schema.json index d1c404c21..7ae651eb3 100644 --- a/schema/jsonschema/cerbos/request/v1/ListPoliciesRequest.schema.json +++ b/schema/jsonschema/cerbos/request/v1/ListPoliciesRequest.schema.json @@ -2,5 +2,10 @@ "$id": "https://api.cerbos.dev/cerbos/request/v1/ListPoliciesRequest.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", - "additionalProperties": false + "additionalProperties": false, + "properties": { + "includeDisabled": { + "type": "boolean" + } + } } diff --git a/schema/openapiv2/cerbos/svc/v1/svc.swagger.json b/schema/openapiv2/cerbos/svc/v1/svc.swagger.json index 57e485c1e..f37a5f1ef 100644 --- a/schema/openapiv2/cerbos/svc/v1/svc.swagger.json +++ b/schema/openapiv2/cerbos/svc/v1/svc.swagger.json @@ -140,6 +140,15 @@ } } }, + "parameters": [ + { + "name": "includeDisabled", + "description": "Include disabled policies", + "in": "query", + "required": false, + "type": "boolean" + } + ], "tags": [ "CerbosAdminService" ],