Skip to content

Commit

Permalink
feat: Add on_validation_error option to Cerbos::Client#initialize (
Browse files Browse the repository at this point in the history
…#22)

Signed-off-by: Andrew Haines <[email protected]>
  • Loading branch information
haines authored Jun 3, 2022
1 parent 2b74d0c commit c66182a
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 4 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
## [Unreleased]
### Added
- `on_validation_error` option to `Cerbos::Client#initialize` ([#22](https://github.com/cerbos/cerbos-sdk-ruby/pull/22))

### Changed
- Minor documentation fixes ([#21](https://github.com/cerbos/cerbos-sdk-ruby/pull/21))

Expand Down
26 changes: 24 additions & 2 deletions lib/cerbos/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Client
# @param target [String] Cerbos PDP server address (`"host"`, `"host:port"`, or `"unix:/path/to/socket"`).
# @param tls [TLS, MutualTLS, false] gRPC connection encryption settings (`false` for plaintext).
# @param grpc_channel_args [Hash{String, Symbol => String, Integer}] low-level settings for the gRPC channel (see [available keys in the gRPC documentation](https://grpc.github.io/grpc/core/group__grpc__arg__keys.html)).
# @param on_validation_error [:return, :raise, #call] action to take when input fails schema validation (`:return` to return the validation errors in the response, `:raise` to raise {Error::ValidationFailed}, or a callback to invoke).
# @param playground_instance [String, nil] identifier of the playground instance to use when prototyping against the hosted demo PDP.
# @param timeout [Numeric, nil] timeout for gRPC calls, in seconds (`nil` to never time out).
#
Expand All @@ -23,7 +24,15 @@ class Client
#
# @example Connect to the hosted demo PDP to experiment [in the playground](https://play.cerbos.dev)
# client = Cerbos::Client.new("demo-pdp.cerbos.cloud", tls: Cerbos::TLS.new, playground_instance: "gE623b0180QlsG5a4QIN6UOZ6f3iSFW2")
def initialize(target, tls:, grpc_channel_args: {}, playground_instance: nil, timeout: nil)
#
# @example Raise an error when input fails schema validation
# client = Cerbos::Client.new("localhost:3593", tls: false, on_validation_error: :raise)
#
# @example Invoke a callback when input fails schema validation
# client = Cerbos::Client.new("localhost:3593", tls: false, on_validation_error: ->(validation_errors) { do_something_with validation_errors })
def initialize(target, tls:, grpc_channel_args: {}, on_validation_error: :return, playground_instance: nil, timeout: nil)
@on_validation_error = on_validation_error

handle_errors do
credentials = tls ? tls.to_channel_credentials : :this_channel_is_insecure

Expand Down Expand Up @@ -139,7 +148,9 @@ def check_resources(principal:, resources:, aux_data: nil, include_metadata: fal

response = perform_request(@cerbos_service, :check_resources, request)

Output::CheckResources.from_protobuf(response)
Output::CheckResources.from_protobuf(response).tap do |output|
handle_validation_errors output
end
end
end

Expand Down Expand Up @@ -207,6 +218,17 @@ def handle_errors
raise Error, error.message
end

def handle_validation_errors(output)
return if @on_validation_error == :return

validation_errors = output.results.flat_map(&:validation_errors)
return if validation_errors.empty?

raise Error::ValidationFailed.new(validation_errors) if @on_validation_error == :raise

@on_validation_error.call validation_errors
end

def perform_request(service, rpc, request)
service.public_send(rpc, request)
end
Expand Down
17 changes: 16 additions & 1 deletion lib/cerbos/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
module Cerbos
# Base type for errors thrown by the `cerbos` gem.
class Error < StandardError
# Input failed schema validation.
class ValidationFailed < Error
# The validation errors that occurred.
#
# @return [Array<Output::CheckResources::Result::ValidationError>]
attr_reader :validation_errors

# @private
def initialize(validation_errors)
super "Input failed schema validation"

@validation_errors = validation_errors
end
end

# An error indicating an unsuccessful gRPC operation.
class NotOK < Error
# The gRPC status code.
Expand Down Expand Up @@ -33,7 +48,7 @@ def self.from_grpc_bad_status(error)

# @private
def initialize(code:, details:, metadata: {})
super("gRPC error #{code}: #{details}")
super "gRPC error #{code}: #{details}"

@code = code
@details = details
Expand Down
79 changes: 78 additions & 1 deletion spec/cerbos/client_spec.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# frozen_string_literal: true

RSpec.describe Cerbos::Client do
subject(:client) { described_class.new(target, tls: tls) }
subject(:client) { described_class.new(target, on_validation_error: on_validation_error, tls: tls) }

let(:target) { "#{host}:#{port}" }
let(:host) { "localhost" }
let(:on_validation_error) { :return }

shared_examples "client" do
describe "#allow?" do
Expand Down Expand Up @@ -337,6 +338,82 @@
))
end
end

context "when configured to raise on validation error" do
let(:on_validation_error) { :raise }

it "raises an error when validation fails", :aggregate_failures do
expect {
client.allow?(
principal: {
id: "[email protected]",
policy_version: "1",
scope: "test",
roles: ["USER"],
attributes: {
country: "NZ"
}
},
resource: {
kind: "document",
id: "invalid",
policy_version: "1",
scope: "test",
attributes: {
owner: 123
}
},
action: "view"
)
}.to raise_error { |error|
expect(error).to be_a(Cerbos::Error::ValidationFailed).and(have_attributes(
validation_errors: [
Cerbos::Output::CheckResources::Result::ValidationError.new(
path: "/owner",
message: "expected string, but got number",
source: :SOURCE_RESOURCE
)
]
))
}
end
end

context "when configured with a callback on validation error" do
let(:on_validation_error) { instance_double(Proc, call: nil) }

it "raises an error when validation fails", :aggregate_failures do
client.allow?(
principal: {
id: "[email protected]",
policy_version: "1",
scope: "test",
roles: ["USER"],
attributes: {
country: "NZ"
}
},
resource: {
kind: "document",
id: "invalid",
policy_version: "1",
scope: "test",
attributes: {
owner: 123
}
},
action: "view"
)

expect(on_validation_error).to have_received(:call).with([
Cerbos::Output::CheckResources::Result::ValidationError.new(
path: "/owner",
message: "expected string, but got number",
source: :SOURCE_RESOURCE
)
])
end
end
end

context "with plaintext" do
Expand Down

0 comments on commit c66182a

Please sign in to comment.