From 9ef86c62bc4f7284edaf9711b919d454f6dc928e Mon Sep 17 00:00:00 2001 From: Charith Ellawala Date: Wed, 28 Jan 2026 15:23:09 +0000 Subject: [PATCH 1/2] Request context support Signed-off-by: Charith Ellawala --- .../dev/cerbos/sdk/CerbosBlockingClient.java | 48 +++++++++++++++++-- .../sdk/CheckResourcesRequestBuilder.java | 7 +++ .../authzen/authorization/v1/evaluation.proto | 2 +- .../proto/authzen/authorization/v1/svc.proto | 2 +- src/main/proto/cerbos/audit/v1/audit.proto | 24 +++++++++- src/main/proto/cerbos/effect/v1/effect.proto | 2 +- src/main/proto/cerbos/engine/v1/engine.proto | 6 ++- src/main/proto/cerbos/policy/v1/policy.proto | 3 +- .../proto/cerbos/request/v1/request.proto | 36 +++++++++++++- .../proto/cerbos/response/v1/response.proto | 40 +++++++++++++++- src/main/proto/cerbos/schema/v1/schema.proto | 2 +- src/main/proto/cerbos/svc/v1/svc.proto | 28 ++++++++++- .../proto/cerbos/telemetry/v1/telemetry.proto | 2 +- .../cerbos/sdk/CerbosBlockingClientTest.java | 7 ++- .../dev/cerbos/sdk/CerbosClientTests.java | 1 - 15 files changed, 193 insertions(+), 17 deletions(-) diff --git a/src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java b/src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java index 32416cb..a736d0a 100644 --- a/src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java +++ b/src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java @@ -5,9 +5,12 @@ package dev.cerbos.sdk; +import com.google.protobuf.Value; +import dev.cerbos.api.v1.audit.Audit; import dev.cerbos.api.v1.request.Request; import dev.cerbos.api.v1.response.Response; import dev.cerbos.api.v1.svc.CerbosServiceGrpc; +import dev.cerbos.sdk.builders.AttributeValue; import dev.cerbos.sdk.builders.AuxData; import dev.cerbos.sdk.builders.Principal; import dev.cerbos.sdk.builders.Resource; @@ -20,6 +23,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * CerbosBlockingClient provides a client implementation that blocks waiting for a response from the @@ -30,6 +34,7 @@ public class CerbosBlockingClient { private final long timeoutMillis; private final Optional auxData; private final Optional headerMetadata; + private final Optional> requestAnnotations; CerbosBlockingClient( @@ -43,14 +48,16 @@ public class CerbosBlockingClient { this.timeoutMillis = timeoutMillis; this.auxData = Optional.empty(); this.headerMetadata = Optional.empty(); + this.requestAnnotations = Optional.empty(); } CerbosBlockingClient( - CerbosServiceGrpc.CerbosServiceBlockingStub cerbosStub, long timeoutMillis, Optional auxData, Optional headerMetadata) { + CerbosServiceGrpc.CerbosServiceBlockingStub cerbosStub, long timeoutMillis, Optional auxData, Optional headerMetadata, Optional> requestAnnotations) { this.cerbosStub = cerbosStub; this.timeoutMillis = timeoutMillis; this.auxData = auxData; this.headerMetadata = headerMetadata; + this.requestAnnotations = requestAnnotations; } private CerbosServiceGrpc.CerbosServiceBlockingStub withClient() { @@ -65,7 +72,7 @@ private CerbosServiceGrpc.CerbosServiceBlockingStub withClient() { * @return new CerbosBlockingClient configured to attach the auxiliary data to requests. */ public CerbosBlockingClient with(AuxData auxData) { - return new CerbosBlockingClient(cerbosStub, timeoutMillis, Optional.ofNullable(auxData), headerMetadata); + return new CerbosBlockingClient(cerbosStub, timeoutMillis, Optional.ofNullable(auxData), headerMetadata, requestAnnotations); } /** @@ -75,7 +82,7 @@ public CerbosBlockingClient with(AuxData auxData) { * @return new CerbosBlockingClient configured to attach given headers to the requests. */ public CerbosBlockingClient withHeaders(Metadata md) { - return new CerbosBlockingClient(cerbosStub, timeoutMillis, auxData, Optional.ofNullable(md)); + return new CerbosBlockingClient(cerbosStub, timeoutMillis, auxData, Optional.ofNullable(md), requestAnnotations); } /** @@ -90,6 +97,23 @@ public CerbosBlockingClient withHeaders(Map headers) { return withHeaders(md); } + /** + * Attach the given key-value pairs to the request context of the Cerbos requests. + * These values are captured by the audit logs and can be used to provide additional context for log analysis. + * Passing null clears the annotations. + * + * @param annotations key-value pairs of annotations to add to the context. + * @return new CerbosBlockingClient configured to attach the given annotations to requests. + */ + public CerbosBlockingClient withRequestAnnotations(Map annotations) { + if (annotations == null) { + return new CerbosBlockingClient(cerbosStub, timeoutMillis, auxData, headerMetadata, Optional.empty()); + } + + Map valueMap = annotations.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, v -> v.getValue().toValue())); + return new CerbosBlockingClient(cerbosStub, timeoutMillis, auxData, headerMetadata, Optional.of(valueMap)); + } + /** * Check whether the principal is allowed to perform the actions on the given resource. * @@ -103,6 +127,7 @@ public CerbosBlockingClient withHeaders(Map headers) { public CheckResult check(Principal principal, Resource resource, String... actions) { Request.AuxData ad = this.auxData.map(AuxData::toAuxData).orElseGet(Request.AuxData::getDefaultInstance); + Audit.RequestContext reqCtx = this.requestAnnotations.map(a -> Audit.RequestContext.newBuilder().putAllAnnotations(a).build()).orElse(Audit.RequestContext.getDefaultInstance()); Request.CheckResourcesRequest request = Request.CheckResourcesRequest.newBuilder() .setRequestId(RequestId.generate()) @@ -115,6 +140,10 @@ public CheckResult check(Principal principal, Resource resource, String... actio .build()) .build(); + if (requestAnnotations.isPresent()) { + request = request.toBuilder().setRequestContext(Audit.RequestContext.newBuilder().putAllAnnotations(requestAnnotations.get()).build()).build(); + } + try { Response.CheckResourcesResponse response = withClient().checkResources(request); if (response.getResultsCount() == 1) { @@ -136,6 +165,7 @@ public CheckResourcesRequestBuilder batch(Principal principal) { return new CheckResourcesRequestBuilder( this::withClient, this.auxData.map(AuxData::toAuxData).orElseGet(Request.AuxData::getDefaultInstance), + this.requestAnnotations, principal.toPrincipal()); } @@ -148,7 +178,7 @@ public CheckResourcesRequestBuilder batch(Principal principal) { */ public CheckResourcesRequestBuilder batch(Principal principal, AuxData auxData) { return new CheckResourcesRequestBuilder( - this::withClient, auxData.toAuxData(), principal.toPrincipal()); + this::withClient, auxData.toAuxData(), this.requestAnnotations, principal.toPrincipal()); } /** @@ -173,6 +203,11 @@ public PlanResourcesResult plan(Principal principal, Resource resource, String a .setAuxData(ad) .setAction(action) .build(); + + if (requestAnnotations.isPresent()) { + request = request.toBuilder().setRequestContext(Audit.RequestContext.newBuilder().putAllAnnotations(requestAnnotations.get()).build()).build(); + } + try { Response.PlanResourcesResponse response = withClient().planResources(request); return new PlanResourcesResult(response); @@ -203,6 +238,11 @@ public PlanResourcesResult plan(Principal principal, Resource resource, Iterable .setAuxData(ad) .addAllActions(actions) .build(); + + if (requestAnnotations.isPresent()) { + request = request.toBuilder().setRequestContext(Audit.RequestContext.newBuilder().putAllAnnotations(requestAnnotations.get()).build()).build(); + } + try { Response.PlanResourcesResponse response = withClient().planResources(request); return new PlanResourcesResult(response); diff --git a/src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java b/src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java index 7df0e6e..d8d4516 100644 --- a/src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java +++ b/src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java @@ -5,6 +5,8 @@ package dev.cerbos.sdk; +import com.google.protobuf.Value; +import dev.cerbos.api.v1.audit.Audit; import dev.cerbos.api.v1.engine.Engine; import dev.cerbos.api.v1.request.Request; import dev.cerbos.api.v1.response.Response; @@ -14,6 +16,8 @@ import io.grpc.StatusRuntimeException; import java.util.Arrays; +import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -24,6 +28,7 @@ public class CheckResourcesRequestBuilder { CheckResourcesRequestBuilder( Supplier clientStub, Request.AuxData auxData, + Optional> requestAnnotations, Engine.Principal principal) { this.clientStub = clientStub; this.requestBuilder = @@ -31,6 +36,8 @@ public class CheckResourcesRequestBuilder { .setRequestId(RequestId.generate()) .setPrincipal(principal) .setAuxData(auxData); + requestAnnotations.map(a -> this.requestBuilder.setRequestContext(Audit.RequestContext.newBuilder().putAllAnnotations(a).build())); + } /** diff --git a/src/main/proto/authzen/authorization/v1/evaluation.proto b/src/main/proto/authzen/authorization/v1/evaluation.proto index cd56cc0..e7c3ba0 100644 --- a/src/main/proto/authzen/authorization/v1/evaluation.proto +++ b/src/main/proto/authzen/authorization/v1/evaluation.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; diff --git a/src/main/proto/authzen/authorization/v1/svc.proto b/src/main/proto/authzen/authorization/v1/svc.proto index 25c30a2..3af2128 100644 --- a/src/main/proto/authzen/authorization/v1/svc.proto +++ b/src/main/proto/authzen/authorization/v1/svc.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; diff --git a/src/main/proto/cerbos/audit/v1/audit.proto b/src/main/proto/cerbos/audit/v1/audit.proto index 994e9f2..51a06e1 100644 --- a/src/main/proto/cerbos/audit/v1/audit.proto +++ b/src/main/proto/cerbos/audit/v1/audit.proto @@ -1,13 +1,16 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; package cerbos.audit.v1; +import "buf/validate/validate.proto"; import "cerbos/engine/v1/engine.proto"; import "cerbos/policy/v1/policy.proto"; +import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; option csharp_namespace = "Cerbos.Api.V1.Audit"; option go_package = "github.com/cerbos/cerbos/api/genpb/cerbos/audit/v1;auditv1"; @@ -22,6 +25,7 @@ message AccessLogEntry { uint32 status_code = 6; bool oversized = 7; PolicySource policy_source = 8; + optional RequestContext request_context = 9; } message DecisionLogEntry { @@ -54,6 +58,7 @@ message DecisionLogEntry { AuditTrail audit_trail = 16; bool oversized = 17; PolicySource policy_source = 18; + optional RequestContext request_context = 19; } message MetaValues { @@ -132,3 +137,20 @@ message PolicySource { EmbeddedPDP embedded_pdp = 6; } } + +message RequestContext { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: {description: "Optional metadata to attach to the request. This information will be captured in the audit logs if audit logging is enabled."} + }; + map annotations = 1 [ + (buf.validate.field).map.keys = { + string: {min_len: 1} + }, + (buf.validate.field).map.values.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Key-value pairs of annotations." + min_properties: 1 + example: "{\"app-name\": \"awesome-app\", \"app-version\": \"1.2.3\"}" + } + ]; +} diff --git a/src/main/proto/cerbos/effect/v1/effect.proto b/src/main/proto/cerbos/effect/v1/effect.proto index 8e3f47d..d01b560 100644 --- a/src/main/proto/cerbos/effect/v1/effect.proto +++ b/src/main/proto/cerbos/effect/v1/effect.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; diff --git a/src/main/proto/cerbos/engine/v1/engine.proto b/src/main/proto/cerbos/engine/v1/engine.proto index cd03145..2ba94c8 100644 --- a/src/main/proto/cerbos/engine/v1/engine.proto +++ b/src/main/proto/cerbos/engine/v1/engine.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; @@ -172,6 +172,10 @@ message OutputEntry { description: "Dynamic output, determined by user defined rule output." example: "\"some_string\"" }]; + string action = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Action that was being evaluated when this output was produced." + example: "\"view\"" + }]; } message Resource { diff --git a/src/main/proto/cerbos/policy/v1/policy.proto b/src/main/proto/cerbos/policy/v1/policy.proto index 684d3aa..51290de 100644 --- a/src/main/proto/cerbos/policy/v1/policy.proto +++ b/src/main/proto/cerbos/policy/v1/policy.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; @@ -131,6 +131,7 @@ message RolePolicy { option (buf.validate.oneof).required = true; string role = 1 [(buf.validate.field).string = {pattern: "^[^!*?\\[\\]{}]+$"}]; } + string version = 6 [(buf.validate.field).string = {pattern: "^[\\w]*$"}]; repeated string parent_roles = 5 [(buf.validate.field).repeated = { unique: true items: { diff --git a/src/main/proto/cerbos/request/v1/request.proto b/src/main/proto/cerbos/request/v1/request.proto index d28d2a0..a18e682 100644 --- a/src/main/proto/cerbos/request/v1/request.proto +++ b/src/main/proto/cerbos/request/v1/request.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; @@ -6,6 +6,7 @@ syntax = "proto3"; package cerbos.request.v1; import "buf/validate/validate.proto"; +import "cerbos/audit/v1/audit.proto"; import "cerbos/engine/v1/engine.proto"; import "cerbos/policy/v1/policy.proto"; import "cerbos/schema/v1/schema.proto"; @@ -72,6 +73,7 @@ message PlanResourcesRequest { AuxData aux_data = 5 [(google.api.field_behavior) = OPTIONAL]; bool include_meta = 6 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Opt to receive request processing metadata in the response."}]; + optional cerbos.audit.v1.RequestContext request_context = 8; } // Deprecated. See CheckResourcesRequest. @@ -267,6 +269,7 @@ message CheckResourcesRequest { } ]; AuxData aux_data = 5; + optional cerbos.audit.v1.RequestContext request_context = 6; } message AuxData { @@ -610,6 +613,28 @@ message GetPolicyRequest { ]; } +message DeletePolicyRequest { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: {description: "Delete policy request"} + }; + repeated string id = 1 [ + (google.api.field_behavior) = REQUIRED, + (buf.validate.field).repeated = { + unique: true + min_items: 1 + max_items: 20 + items: { + string: {min_len: 1} + } + }, + (buf.validate.field).required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Unique identifier for the policy" + example: "\"principal.sarah.vdefault\"" + } + ]; +} + message DisablePolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: {description: "Disable policy request"} @@ -619,6 +644,7 @@ message DisablePolicyRequest { (buf.validate.field).repeated = { unique: true min_items: 1 + max_items: 20 items: { string: {min_len: 1} } @@ -771,3 +797,11 @@ message ReloadStoreRequest { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Wait until the reloading process finishes"} ]; } + +message PurgeStoreRevisionsRequest { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: {description: "Purge store revisions request"} + }; + + uint32 keep_last = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "Keep last N revisions for each policy. If not specified or set to zero, all revisions will be deleted."}]; +} diff --git a/src/main/proto/cerbos/response/v1/response.proto b/src/main/proto/cerbos/response/v1/response.proto index 101e295..5e5d248 100644 --- a/src/main/proto/cerbos/response/v1/response.proto +++ b/src/main/proto/cerbos/response/v1/response.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; @@ -427,13 +427,43 @@ message GetPolicyResponse { repeated cerbos.policy.v1.Policy policies = 1; } +message DeletePolicyResponse { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: {description: "Delete policy response"} + }; + + uint32 deleted_policies = 1; +} + +message DeletePolicyErrorDetails { + map errors = 1; +} + message DisablePolicyResponse { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: {description: "Disable policy response"} }; + uint32 disabled_policies = 1; } +message DisablePolicyErrorDetails { + map errors = 1; +} + +message IntegrityErrors { + message BreaksScopeChain { + repeated string descendants = 1; + } + + message RequiredByOtherPolicies { + repeated string dependents = 1; + } + + BreaksScopeChain breaks_scope_chain = 1; + RequiredByOtherPolicies required_by_other_policies = 2; +} + message EnablePolicyResponse { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: {description: "Enable policy response"} @@ -551,3 +581,11 @@ message ReloadStoreResponse { json_schema: {description: "Reload store response"} }; } + +message PurgeStoreRevisionsResponse { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: {description: "Purge store revisions response"} + }; + + uint32 affected_rows = 1; +} diff --git a/src/main/proto/cerbos/schema/v1/schema.proto b/src/main/proto/cerbos/schema/v1/schema.proto index 6f78fb7..569a868 100644 --- a/src/main/proto/cerbos/schema/v1/schema.proto +++ b/src/main/proto/cerbos/schema/v1/schema.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; diff --git a/src/main/proto/cerbos/svc/v1/svc.proto b/src/main/proto/cerbos/svc/v1/svc.proto index 7439fad..7cfd265 100644 --- a/src/main/proto/cerbos/svc/v1/svc.proto +++ b/src/main/proto/cerbos/svc/v1/svc.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; @@ -155,6 +155,19 @@ service CerbosAdminService { }; } + rpc DeletePolicy(cerbos.request.v1.DeletePolicyRequest) returns (cerbos.response.v1.DeletePolicyResponse) { + option (google.api.http) = {post: "/admin/policy/delete"}; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Delete policy" + security: { + security_requirement: { + key: "BasicAuth" + value: {} + } + } + }; + } + rpc DisablePolicy(cerbos.request.v1.DisablePolicyRequest) returns (cerbos.response.v1.DisablePolicyResponse) { option (google.api.http) = { post: "/admin/policy/disable" @@ -278,6 +291,19 @@ service CerbosAdminService { } }; } + + rpc PurgeStoreRevisions(cerbos.request.v1.PurgeStoreRevisionsRequest) returns (cerbos.response.v1.PurgeStoreRevisionsResponse) { + option (google.api.http) = {delete: "/admin/store/revisions"}; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Purge store revisions" + security: { + security_requirement: { + key: "BasicAuth" + value: {} + } + } + }; + } } service CerbosPlaygroundService { diff --git a/src/main/proto/cerbos/telemetry/v1/telemetry.proto b/src/main/proto/cerbos/telemetry/v1/telemetry.proto index ae475d0..41d324e 100644 --- a/src/main/proto/cerbos/telemetry/v1/telemetry.proto +++ b/src/main/proto/cerbos/telemetry/v1/telemetry.proto @@ -1,4 +1,4 @@ -// Copyright 2021-2025 Zenauth Ltd. +// Copyright 2021-2026 Zenauth Ltd. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; diff --git a/src/test/java/dev/cerbos/sdk/CerbosBlockingClientTest.java b/src/test/java/dev/cerbos/sdk/CerbosBlockingClientTest.java index bc3ebac..0ccda11 100644 --- a/src/test/java/dev/cerbos/sdk/CerbosBlockingClientTest.java +++ b/src/test/java/dev/cerbos/sdk/CerbosBlockingClientTest.java @@ -5,6 +5,7 @@ package dev.cerbos.sdk; +import dev.cerbos.sdk.builders.AttributeValue; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestInstance; import org.slf4j.Logger; @@ -33,6 +34,10 @@ class CerbosBlockingClientTest extends CerbosClientTests { @BeforeAll public void initClient() throws CerbosClientBuilder.InvalidClientConfigurationException { String target = cerbosContainer.getTarget(); - this.client = new CerbosClientBuilder(target).withPlaintext().buildBlockingClient().withHeaders(Map.of("wibble", "wobble")); + this.client = new CerbosClientBuilder(target) + .withPlaintext() + .buildBlockingClient() + .withHeaders(Map.of("wibble", "wobble")) + .withRequestAnnotations(Map.of("foo", AttributeValue.stringValue("bar"))); } } diff --git a/src/test/java/dev/cerbos/sdk/CerbosClientTests.java b/src/test/java/dev/cerbos/sdk/CerbosClientTests.java index b84c2e6..fe13f94 100644 --- a/src/test/java/dev/cerbos/sdk/CerbosClientTests.java +++ b/src/test/java/dev/cerbos/sdk/CerbosClientTests.java @@ -288,5 +288,4 @@ public void planResourcesValidation() { Assertions.assertFalse(have.isAlwaysAllowed()); Assertions.assertFalse(have.isConditional()); } - } From e800117fa7f2ceb43706adf57b8a7dcaf9c2839c Mon Sep 17 00:00:00 2001 From: Charith Ellawala Date: Wed, 28 Jan 2026 15:52:10 +0000 Subject: [PATCH 2/2] DeletePolicy and PurgeStoreRevisions Signed-off-by: Charith Ellawala --- .../cerbos/sdk/CerbosBlockingAdminClient.java | 42 +++++++++++++++++++ .../sdk/CerbosBlockingAdminClientTest.java | 19 +++++++++ .../dev/cerbos/sdk/CerbosClientTests.java | 28 +++++++++++++ .../policies/resource_policies/policy_07.yaml | 13 ++++++ 4 files changed, 102 insertions(+) create mode 100644 src/test/resources/policies/resource_policies/policy_07.yaml diff --git a/src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java b/src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java index 2f494ab..feb82f3 100644 --- a/src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java +++ b/src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java @@ -175,6 +175,48 @@ public long disablePolicy(String... ids) { } } + /** + * Delete policies by ID. Note that this is a permanent operation and cannot be rolled back. + * + * @param ids IDs of policies to delete + * @return Number of deleted policies + * @throws CerbosException if an RPC error occurrs + */ + public long deletePolicy(String... ids) { + Request.DeletePolicyRequest.Builder requestBuilder = Request.DeletePolicyRequest.newBuilder(); + requestBuilder.addAllId(List.of(ids)); + + try { + Response.DeletePolicyResponse resp = withClient().deletePolicy(requestBuilder.build()); + return resp.getDeletedPolicies(); + } catch (StatusRuntimeException sre) { + throw new CerbosException(sre.getStatus(), sre.getCause()); + } + } + + /** + * Purge the version history table of policies. + * Note that this permanently deletes history of changes made to policies. + * If the keepLast parameter is greater than 0, everything but the most recent `keepLast` number of revisions will be deleted. + * + * @param keepLast How many revisions of each policy to preserve. 0 deletes everything. + * @return Number of deleted records + * @throws CerbosException if an RPC error occurrs + */ + public long purgeStoreRevisions(int keepLast) { + Request.PurgeStoreRevisionsRequest.Builder requestBuilder = Request.PurgeStoreRevisionsRequest.newBuilder(); + if (keepLast > 0) { + requestBuilder.setKeepLast(keepLast); + } + + try { + Response.PurgeStoreRevisionsResponse resp = withClient().purgeStoreRevisions(requestBuilder.build()); + return resp.getAffectedRows(); + } catch (StatusRuntimeException sre) { + throw new CerbosException(sre.getStatus(), sre.getCause()); + } + } + /** * Add or update schemas * diff --git a/src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java b/src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java index 2201bfe..c1563b5 100644 --- a/src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java +++ b/src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java @@ -159,6 +159,25 @@ void enableAndDisablePolicy() { Assertions.assertEquals(1, enabled, "Enabled count does not match"); } + @Test + void deletePolicyWithDependents() { + Assertions.assertThrows(CerbosException.class, () -> { + this.adminClient.deletePolicy("derived_roles.alpha"); + }); + } + + @Test + void deletePolicyWithoutDependents() { + long deleted = this.adminClient.deletePolicy("resource.foo.vdefault"); + Assertions.assertEquals(1, deleted); + } + + @Test + void purgeStoreRevisions() { + long deleted = this.adminClient.purgeStoreRevisions(0); + Assertions.assertTrue(deleted > 1); + } + @Test void listSchemas() { List have = this.adminClient.listSchemas(); diff --git a/src/test/java/dev/cerbos/sdk/CerbosClientTests.java b/src/test/java/dev/cerbos/sdk/CerbosClientTests.java index fe13f94..86d2e87 100644 --- a/src/test/java/dev/cerbos/sdk/CerbosClientTests.java +++ b/src/test/java/dev/cerbos/sdk/CerbosClientTests.java @@ -15,6 +15,7 @@ import dev.cerbos.sdk.builders.Principal; import dev.cerbos.sdk.builders.Resource; import dev.cerbos.sdk.builders.ResourceAction; +import io.grpc.Status; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -288,4 +289,31 @@ public void planResourcesValidation() { Assertions.assertFalse(have.isAlwaysAllowed()); Assertions.assertFalse(have.isConditional()); } + + @Test + public void partialCheckRequest() { + CerbosException have = Assertions.assertThrows(CerbosException.class, () -> { + this.client.check( + Principal.newInstance("john") + .withPolicyVersion("20210210"), + Resource.newInstance("leave_request", "") + .withPolicyVersion("20210210"), + "view:public", + "approve"); + }); + Assertions.assertEquals(Status.INVALID_ARGUMENT.getCode().value(), have.getStatusCode()); + } + + @Test + public void partialPlanRequest() { + CerbosException have = Assertions.assertThrows(CerbosException.class, () -> { + this.client.plan( + Principal.newInstance("john") + .withPolicyVersion("20210210"), + Resource.newInstance("leave_request", "") + .withPolicyVersion("20210210"), + "view:public"); + }); + Assertions.assertEquals(Status.INVALID_ARGUMENT.getCode().value(), have.getStatusCode()); + } } diff --git a/src/test/resources/policies/resource_policies/policy_07.yaml b/src/test/resources/policies/resource_policies/policy_07.yaml new file mode 100644 index 0000000..9fdeeb6 --- /dev/null +++ b/src/test/resources/policies/resource_policies/policy_07.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: api.cerbos.dev/v1 +resourcePolicy: + version: "default" + resource: foo + rules: + - actions: ["*"] + effect: EFFECT_ALLOW + roles: ["*"] + + - actions: ["create"] + roles: ["user"] + effect: EFFECT_DENY