Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
48 changes: 44 additions & 4 deletions src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -30,6 +34,7 @@ public class CerbosBlockingClient {
private final long timeoutMillis;
private final Optional<AuxData> auxData;
private final Optional<Metadata> headerMetadata;
private final Optional<Map<String, Value>> requestAnnotations;


CerbosBlockingClient(
Expand All @@ -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> auxData, Optional<Metadata> headerMetadata) {
CerbosServiceGrpc.CerbosServiceBlockingStub cerbosStub, long timeoutMillis, Optional<AuxData> auxData, Optional<Metadata> headerMetadata, Optional<Map<String, Value>> requestAnnotations) {
this.cerbosStub = cerbosStub;
this.timeoutMillis = timeoutMillis;
this.auxData = auxData;
this.headerMetadata = headerMetadata;
this.requestAnnotations = requestAnnotations;
}

private CerbosServiceGrpc.CerbosServiceBlockingStub withClient() {
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -90,6 +97,23 @@ public CerbosBlockingClient withHeaders(Map<String, String> 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<String, AttributeValue> annotations) {
if (annotations == null) {
return new CerbosBlockingClient(cerbosStub, timeoutMillis, auxData, headerMetadata, Optional.empty());
}

Map<String, Value> 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.
*
Expand All @@ -103,6 +127,7 @@ public CerbosBlockingClient withHeaders(Map<String, String> 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())
Expand All @@ -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) {
Expand All @@ -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());
}

Expand All @@ -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());
}

/**
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -24,13 +28,16 @@ public class CheckResourcesRequestBuilder {
CheckResourcesRequestBuilder(
Supplier<CerbosServiceGrpc.CerbosServiceBlockingStub> clientStub,
Request.AuxData auxData,
Optional<Map<String, Value>> requestAnnotations,
Engine.Principal principal) {
this.clientStub = clientStub;
this.requestBuilder =
Request.CheckResourcesRequest.newBuilder()
.setRequestId(RequestId.generate())
.setPrincipal(principal)
.setAuxData(auxData);
requestAnnotations.map(a -> this.requestBuilder.setRequestContext(Audit.RequestContext.newBuilder().putAllAnnotations(a).build()));

}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/main/proto/authzen/authorization/v1/evaluation.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021-2025 Zenauth Ltd.
// Copyright 2021-2026 Zenauth Ltd.
// SPDX-License-Identifier: Apache-2.0

syntax = "proto3";
Expand Down
2 changes: 1 addition & 1 deletion src/main/proto/authzen/authorization/v1/svc.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021-2025 Zenauth Ltd.
// Copyright 2021-2026 Zenauth Ltd.
// SPDX-License-Identifier: Apache-2.0

syntax = "proto3";
Expand Down
24 changes: 23 additions & 1 deletion src/main/proto/cerbos/audit/v1/audit.proto
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,6 +25,7 @@ message AccessLogEntry {
uint32 status_code = 6;
bool oversized = 7;
PolicySource policy_source = 8;
optional RequestContext request_context = 9;
}

message DecisionLogEntry {
Expand Down Expand Up @@ -54,6 +58,7 @@ message DecisionLogEntry {
AuditTrail audit_trail = 16;
bool oversized = 17;
PolicySource policy_source = 18;
optional RequestContext request_context = 19;
}

message MetaValues {
Expand Down Expand Up @@ -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<string, google.protobuf.Value> 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\"}"
}
];
}
2 changes: 1 addition & 1 deletion src/main/proto/cerbos/effect/v1/effect.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021-2025 Zenauth Ltd.
// Copyright 2021-2026 Zenauth Ltd.
// SPDX-License-Identifier: Apache-2.0

syntax = "proto3";
Expand Down
6 changes: 5 additions & 1 deletion src/main/proto/cerbos/engine/v1/engine.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021-2025 Zenauth Ltd.
// Copyright 2021-2026 Zenauth Ltd.
// SPDX-License-Identifier: Apache-2.0

syntax = "proto3";
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/main/proto/cerbos/policy/v1/policy.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021-2025 Zenauth Ltd.
// Copyright 2021-2026 Zenauth Ltd.
// SPDX-License-Identifier: Apache-2.0

syntax = "proto3";
Expand Down Expand Up @@ -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: {
Expand Down
Loading
Loading