cerbos 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +31 -3
- data/cerbos.gemspec +3 -2
- data/lib/cerbos/abstract_class.rb +10 -0
- data/lib/cerbos/client.rb +29 -44
- data/lib/cerbos/error.rb +79 -10
- data/lib/cerbos/hub/access_token.rb +137 -0
- data/lib/cerbos/hub/circuit_breaker.rb +185 -0
- data/lib/cerbos/hub/service.rb +18 -0
- data/lib/cerbos/hub/stores/client.rb +162 -0
- data/lib/cerbos/hub/stores/error.rb +108 -0
- data/lib/cerbos/hub/stores/file.rb +28 -0
- data/lib/cerbos/hub/stores/input/change_details/origin.rb +144 -0
- data/lib/cerbos/hub/stores/input/change_details/uploader.rb +38 -0
- data/lib/cerbos/hub/stores/input/change_details.rb +52 -0
- data/lib/cerbos/hub/stores/input/file_filter.rb +30 -0
- data/lib/cerbos/hub/stores/input/file_modification_condition.rb +34 -0
- data/lib/cerbos/hub/stores/input/file_operation.rb +66 -0
- data/lib/cerbos/hub/stores/input/string_match.rb +88 -0
- data/lib/cerbos/hub/stores/input.rb +17 -0
- data/lib/cerbos/hub/stores/output/get_files.rb +31 -0
- data/lib/cerbos/hub/stores/output/list_files.rb +31 -0
- data/lib/cerbos/hub/stores/output/modify_files.rb +35 -0
- data/lib/cerbos/hub/stores/output/replace_files.rb +43 -0
- data/lib/cerbos/hub/stores/output.rb +16 -0
- data/lib/cerbos/hub/stores.rb +15 -0
- data/lib/cerbos/hub.rb +12 -0
- data/lib/cerbos/input.rb +2 -1
- data/lib/cerbos/output/plan_resources.rb +10 -2
- data/lib/cerbos/protobuf/buf/validate/validate_pb.rb +6 -5
- data/lib/cerbos/protobuf/cerbos/cloud/apikey/v1/apikey_pb.rb +26 -0
- data/lib/cerbos/protobuf/cerbos/cloud/apikey/v1/apikey_services_pb.rb +32 -0
- data/lib/cerbos/protobuf/cerbos/cloud/store/v1/store_pb.rb +52 -0
- data/lib/cerbos/protobuf/cerbos/cloud/store/v1/store_services_pb.rb +35 -0
- data/lib/cerbos/protobuf/cerbos/effect/v1/effect_pb.rb +1 -1
- data/lib/cerbos/protobuf/cerbos/engine/v1/engine_pb.rb +4 -2
- data/lib/cerbos/protobuf/cerbos/request/v1/request_pb.rb +2 -2
- data/lib/cerbos/protobuf/cerbos/response/v1/response_pb.rb +2 -2
- data/lib/cerbos/protobuf/cerbos/schema/v1/schema_pb.rb +1 -1
- data/lib/cerbos/protobuf/cerbos/svc/v1/svc_pb.rb +2 -4
- data/lib/cerbos/protobuf/google/api/annotations_pb.rb +1 -1
- data/lib/cerbos/protobuf/google/api/field_behavior_pb.rb +1 -1
- data/lib/cerbos/protobuf/google/api/http_pb.rb +2 -2
- data/lib/cerbos/protobuf/google/api/visibility_pb.rb +19 -0
- data/lib/cerbos/protobuf/grpc/health/v1/health_pb.rb +4 -2
- data/lib/cerbos/protobuf/grpc/health/v1/health_services_pb.rb +12 -2
- data/lib/cerbos/protobuf/protoc-gen-openapiv2/options/annotations_pb.rb +1 -1
- data/lib/cerbos/protobuf/protoc-gen-openapiv2/options/openapiv2_pb.rb +1 -1
- data/lib/cerbos/protobuf.rb +2 -0
- data/lib/cerbos/service.rb +33 -0
- data/lib/cerbos/version.rb +1 -1
- data/lib/cerbos.rb +7 -1
- data/yard_extensions.rb +8 -1
- metadata +49 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1f96bbfba8e309d992b65cff1615eed19a8731c1b16c69f7cd9eb578e8f3f11
|
4
|
+
data.tar.gz: 934e07596201c3638201af7546ac1bd428ff2c4779cb8b25916a66fbb2d58fc4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97b55e3a336f6389b899e62c3f19bf5411d85c319866b46ccc736c08d3869b89dcf930887fe650c6d151841047009b8ceadca315b83ac5214b5a3059cc2f77e5
|
7
|
+
data.tar.gz: fc2c68df0275eb67f3f2f6000f3f452d045f1de5dab1d56e593e3e5cb631e16052d59dbf1af293b5466adffcc0f7518cd5c210a3b0edec832b5524b3b3712f6e
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,24 @@
|
|
2
2
|
|
3
3
|
No notable changes.
|
4
4
|
|
5
|
+
## [0.12.0] - 2025-08-12
|
6
|
+
|
7
|
+
### Added
|
8
|
+
|
9
|
+
- `Cerbos::Hub::Stores::Client` for interacting with policy stores in Cerbos Hub ([#251](https://github.com/cerbos/cerbos-sdk-ruby/pull/251))
|
10
|
+
|
11
|
+
## [0.11.0] - 2025-06-03
|
12
|
+
|
13
|
+
### Added
|
14
|
+
|
15
|
+
- Support for multiple actions in `Cerbos::Client#plan_resources` ([#239](https://github.com/cerbos/cerbos-sdk-ruby/pull/239))
|
16
|
+
|
17
|
+
Requires a policy decision point server running Cerbos 0.44+.
|
18
|
+
|
19
|
+
### Removed
|
20
|
+
|
21
|
+
- Support for Ruby 3.1 ([#234](https://github.com/cerbos/cerbos-sdk-ruby/pull/234))
|
22
|
+
|
5
23
|
## [0.10.0] - 2025-02-06
|
6
24
|
|
7
25
|
### Added
|
@@ -101,7 +119,9 @@ No notable changes.
|
|
101
119
|
|
102
120
|
- Initial implementation of `Cerbos::Client` ([#2](https://github.com/cerbos/cerbos-sdk-ruby/pull/2))
|
103
121
|
|
104
|
-
[Unreleased]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.
|
122
|
+
[Unreleased]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.12.0...HEAD
|
123
|
+
[0.12.0]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.11.0...v0.12.0
|
124
|
+
[0.11.0]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.10.0...v0.11.0
|
105
125
|
[0.10.0]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.9.1...v0.10.0
|
106
126
|
[0.9.1]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.9.0...v0.9.1
|
107
127
|
[0.9.0]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.8.0...v0.9.0
|
data/README.md
CHANGED
@@ -7,12 +7,12 @@
|
|
7
7
|
[Cerbos](https://cerbos.dev) helps you super-charge your authorization implementation by writing context-aware access control policies for your application resources.
|
8
8
|
Author access rules using an intuitive YAML configuration language, use your Git-ops infrastructure to test and deploy them, and make simple API requests to the Cerbos policy decision point (PDP) server to evaluate the policies and make dynamic access decisions.
|
9
9
|
|
10
|
-
The Cerbos Ruby SDK makes it easy to interact with the Cerbos PDP from your Ruby applications.
|
10
|
+
The Cerbos Ruby SDK makes it easy to interact with the [Cerbos PDP](https://www.cerbos.dev/product-cerbos-pdp) and [Cerbos Hub](https://www.cerbos.dev/product-cerbos-hub) from your Ruby applications.
|
11
11
|
|
12
12
|
## Prerequisites
|
13
13
|
|
14
14
|
- Cerbos 0.16+
|
15
|
-
- Ruby 3.
|
15
|
+
- Ruby 3.2+
|
16
16
|
|
17
17
|
## Installation
|
18
18
|
|
@@ -30,7 +30,11 @@ $ gem install cerbos
|
|
30
30
|
|
31
31
|
## Example usage
|
32
32
|
|
33
|
+
### Cerbos PDP
|
34
|
+
|
33
35
|
```ruby
|
36
|
+
require "cerbos"
|
37
|
+
|
34
38
|
client = Cerbos::Client.new("localhost:3593", tls: false)
|
35
39
|
|
36
40
|
decision = client.check_resource(
|
@@ -52,12 +56,36 @@ decision.allow?("view") # => true
|
|
52
56
|
decision.allow?("edit") # => false
|
53
57
|
```
|
54
58
|
|
55
|
-
For more details, [see the `Client` documentation](https://www.rubydoc.info/gems/cerbos/Cerbos/Client).
|
59
|
+
For more details, [see the `Cerbos::Client` documentation](https://www.rubydoc.info/gems/cerbos/Cerbos/Client).
|
60
|
+
|
61
|
+
### Cerbos Hub [policy stores](https://docs.cerbos.dev/cerbos-hub/policy-stores)
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require "cerbos"
|
65
|
+
|
66
|
+
client = Cerbos::Hub::Stores::Client.new(
|
67
|
+
client_id: ENV.fetch("CERBOS_HUB_CLIENT_ID"),
|
68
|
+
client_secret: ENV.fetch("CERBOS_HUB_CLIENT_SECRET")
|
69
|
+
)
|
70
|
+
|
71
|
+
response = client.modify_files(
|
72
|
+
store_id: ENV.fetch("CERBOS_HUB_STORE_ID"),
|
73
|
+
operations: [
|
74
|
+
{add_or_update: {path: "foo.yaml", contents: File.binread("path/to/foo.yaml")}},
|
75
|
+
{delete: "bar.yaml"}
|
76
|
+
]
|
77
|
+
)
|
78
|
+
|
79
|
+
puts response.new_store_version
|
80
|
+
```
|
81
|
+
|
82
|
+
For more details, [see the `Cerbos::Hub::Stores::Client` documentation](https://www.rubydoc.info/gems/cerbos/Cerbos/Hub/Stores/Client).
|
56
83
|
|
57
84
|
## Further reading
|
58
85
|
|
59
86
|
- [API reference](https://www.rubydoc.info/gems/cerbos/Cerbos)
|
60
87
|
- [Cerbos documentation](https://docs.cerbos.dev)
|
88
|
+
- [Cerbos Hub documentation](https://docs.cerbos.dev/cerbos-hub/)
|
61
89
|
|
62
90
|
## Get help
|
63
91
|
|
data/cerbos.gemspec
CHANGED
@@ -31,7 +31,8 @@ Gem::Specification.new do |spec|
|
|
31
31
|
"yard_extensions.rb"
|
32
32
|
]
|
33
33
|
|
34
|
-
spec.required_ruby_version = ">= 3.
|
35
|
-
spec.add_dependency "
|
34
|
+
spec.required_ruby_version = ">= 3.2.0"
|
35
|
+
spec.add_dependency "concurrent-ruby", "~> 1.2"
|
36
|
+
spec.add_dependency "grpc", "~> 1.52"
|
36
37
|
spec.add_dependency "google-protobuf", ">= 3.21.12", "< 5.0"
|
37
38
|
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
# @private
|
5
|
+
module AbstractClass
|
6
|
+
def initialize
|
7
|
+
raise NoMethodError, "Can't initialize #{self.class.name} directly, initialize a subclass instead (#{self.class.subclasses.map(&:name).join(", ")})"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/lib/cerbos/client.rb
CHANGED
@@ -39,32 +39,31 @@ module Cerbos
|
|
39
39
|
# @example Invoke a callback when input fails schema validation
|
40
40
|
# client = Cerbos::Client.new("localhost:3593", tls: false, on_validation_error: ->(validation_errors) { do_something_with validation_errors })
|
41
41
|
def initialize(target, tls:, grpc_channel_args: {}, grpc_metadata: {}, on_validation_error: :return, playground_instance: nil, timeout: nil)
|
42
|
-
@grpc_metadata = grpc_metadata.transform_keys(&:to_sym)
|
43
42
|
@on_validation_error = on_validation_error
|
44
43
|
|
45
|
-
|
44
|
+
Error.handle do
|
46
45
|
credentials = tls ? tls.to_channel_credentials : :this_channel_is_insecure
|
47
46
|
|
48
47
|
unless playground_instance.nil?
|
49
48
|
credentials = credentials.compose(GRPC::Core::CallCredentials.new(->(*) { {"playground-instance" => playground_instance} }))
|
50
49
|
end
|
51
50
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
channel_args: channel_args,
|
60
|
-
timeout: timeout
|
51
|
+
@cerbos_service = Service.new(
|
52
|
+
stub: Protobuf::Cerbos::Svc::V1::CerbosService::Stub,
|
53
|
+
target:,
|
54
|
+
credentials:,
|
55
|
+
grpc_channel_args:,
|
56
|
+
grpc_metadata:,
|
57
|
+
timeout:
|
61
58
|
)
|
62
59
|
|
63
|
-
@health_service =
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
60
|
+
@health_service = Service.new(
|
61
|
+
stub: Protobuf::Grpc::Health::V1::Health::Stub,
|
62
|
+
target:,
|
63
|
+
credentials:,
|
64
|
+
grpc_channel_args:,
|
65
|
+
grpc_metadata:,
|
66
|
+
timeout:
|
68
67
|
)
|
69
68
|
end
|
70
69
|
end
|
@@ -111,10 +110,10 @@ module Cerbos
|
|
111
110
|
# admin_api = client.check_health(service: "cerbos.svc.v1.CerbosAdminService")
|
112
111
|
# admin_api.status # => :DISABLED
|
113
112
|
def check_health(service: "cerbos.svc.v1.CerbosService", grpc_metadata: {})
|
114
|
-
|
113
|
+
Error.handle do
|
115
114
|
request = Protobuf::Grpc::Health::V1::HealthCheckRequest.new(service: service)
|
116
115
|
|
117
|
-
response =
|
116
|
+
response = @health_service.call(:check, request, grpc_metadata)
|
118
117
|
|
119
118
|
Output::HealthCheck.from_protobuf(response)
|
120
119
|
end
|
@@ -145,7 +144,7 @@ module Cerbos
|
|
145
144
|
#
|
146
145
|
# decision.allow?("view") # => true
|
147
146
|
def check_resource(principal:, resource:, actions:, aux_data: nil, include_metadata: false, request_id: SecureRandom.uuid, grpc_metadata: {})
|
148
|
-
|
147
|
+
Error.handle do
|
149
148
|
check_resources(
|
150
149
|
principal: principal,
|
151
150
|
resources: [Input::ResourceCheck.new(resource: resource, actions: actions)],
|
@@ -185,7 +184,7 @@ module Cerbos
|
|
185
184
|
#
|
186
185
|
# decision.allow?(resource: {kind: "document", id: "1"}, action: "view") # => true
|
187
186
|
def check_resources(principal:, resources:, aux_data: nil, include_metadata: false, request_id: SecureRandom.uuid, grpc_metadata: {})
|
188
|
-
|
187
|
+
Error.handle do
|
189
188
|
request = Protobuf::Cerbos::Request::V1::CheckResourcesRequest.new(
|
190
189
|
principal: Input.coerce_required(principal, Input::Principal).to_protobuf,
|
191
190
|
resources: Input.coerce_array(resources, Input::ResourceCheck).map(&:to_protobuf),
|
@@ -194,7 +193,7 @@ module Cerbos
|
|
194
193
|
request_id: request_id
|
195
194
|
)
|
196
195
|
|
197
|
-
response =
|
196
|
+
response = @cerbos_service.call(:check_resources, request, grpc_metadata)
|
198
197
|
|
199
198
|
Output::CheckResources.from_protobuf(response).tap do |output|
|
200
199
|
handle_validation_errors output
|
@@ -206,7 +205,8 @@ module Cerbos
|
|
206
205
|
#
|
207
206
|
# @param principal [Input::Principal, Hash] the principal for whom to plan.
|
208
207
|
# @param resource [Input::ResourceQuery, Hash] partial details of the resources for which to plan.
|
209
|
-
# @param action [String]
|
208
|
+
# @param action [String] deprecated (use `actions` instead).
|
209
|
+
# @param actions [Array<String>] the actions for which to plan (requires a policy decision point server running Cerbos v0.44+).
|
210
210
|
# @param aux_data [Input::AuxData, Hash, nil] auxiliary data.
|
211
211
|
# @param include_metadata [Boolean] `true` to include additional metadata ({Output::CheckResources::Result::Metadata}) in the results.
|
212
212
|
# @param request_id [String] identifier for tracing the request.
|
@@ -218,23 +218,24 @@ module Cerbos
|
|
218
218
|
# plan = client.plan_resources(
|
219
219
|
# principal: {id: "user@example.com", roles: ["USER"]},
|
220
220
|
# resource: {kind: "document"},
|
221
|
-
#
|
221
|
+
# actions: ["view"]
|
222
222
|
# )
|
223
223
|
#
|
224
224
|
# plan.conditional? # => true
|
225
225
|
# plan.condition # => #<Cerbos::Output::PlanResources::Expression ...>
|
226
|
-
def plan_resources(principal:, resource:, action
|
227
|
-
|
226
|
+
def plan_resources(principal:, resource:, action: "", actions: [], aux_data: nil, include_metadata: false, request_id: SecureRandom.uuid, grpc_metadata: {})
|
227
|
+
Error.handle do
|
228
228
|
request = Protobuf::Cerbos::Request::V1::PlanResourcesRequest.new(
|
229
229
|
principal: Input.coerce_required(principal, Input::Principal).to_protobuf,
|
230
230
|
resource: Input.coerce_required(resource, Input::ResourceQuery).to_protobuf,
|
231
231
|
action: action,
|
232
|
+
actions: actions,
|
232
233
|
aux_data: Input.coerce_optional(aux_data, Input::AuxData)&.to_protobuf,
|
233
234
|
include_meta: include_metadata,
|
234
235
|
request_id: request_id
|
235
236
|
)
|
236
237
|
|
237
|
-
response =
|
238
|
+
response = @cerbos_service.call(:plan_resources, request, grpc_metadata)
|
238
239
|
|
239
240
|
Output::PlanResources.from_protobuf(response).tap do |output|
|
240
241
|
handle_validation_errors output
|
@@ -248,10 +249,10 @@ module Cerbos
|
|
248
249
|
#
|
249
250
|
# @return [Output::ServerInfo]
|
250
251
|
def server_info(grpc_metadata: {})
|
251
|
-
|
252
|
+
Error.handle do
|
252
253
|
request = Protobuf::Cerbos::Request::V1::ServerInfoRequest.new
|
253
254
|
|
254
|
-
response =
|
255
|
+
response = @cerbos_service.call(:server_info, request, grpc_metadata)
|
255
256
|
|
256
257
|
Output::ServerInfo.from_protobuf(response)
|
257
258
|
end
|
@@ -259,18 +260,6 @@ module Cerbos
|
|
259
260
|
|
260
261
|
private
|
261
262
|
|
262
|
-
def handle_errors
|
263
|
-
yield
|
264
|
-
rescue Error
|
265
|
-
raise
|
266
|
-
rescue ArgumentError, TypeError => error
|
267
|
-
raise Error::InvalidArgument.new(details: error.message)
|
268
|
-
rescue GRPC::BadStatus => error
|
269
|
-
raise Error::NotOK.from_grpc_bad_status(error)
|
270
|
-
rescue => error
|
271
|
-
raise Error, error.message
|
272
|
-
end
|
273
|
-
|
274
263
|
def handle_validation_errors(output)
|
275
264
|
return if @on_validation_error == :return
|
276
265
|
|
@@ -281,9 +270,5 @@ module Cerbos
|
|
281
270
|
|
282
271
|
@on_validation_error.call validation_errors
|
283
272
|
end
|
284
|
-
|
285
|
-
def perform_request(service, rpc, request, metadata)
|
286
|
-
service.public_send(rpc, request, metadata: @grpc_metadata.merge(metadata.transform_keys(&:to_sym)))
|
287
|
-
end
|
288
273
|
end
|
289
274
|
end
|
data/lib/cerbos/error.rb
CHANGED
@@ -56,80 +56,149 @@ module Cerbos
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
-
# The
|
59
|
+
# The operation was aborted.
|
60
|
+
class Aborted < NotOK
|
61
|
+
def initialize(code: GRPC::Core::StatusCodes::ABORTED, **args)
|
62
|
+
super
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# The entity that the client attempted to create already exists.
|
67
|
+
class AlreadyExists < NotOK
|
68
|
+
def initialize(code: GRPC::Core::StatusCodes::ALREADY_EXISTS, **args)
|
69
|
+
super
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# The operation was cancelled.
|
60
74
|
class Cancelled < NotOK
|
61
75
|
def initialize(code: GRPC::Core::StatusCodes::CANCELLED, **args)
|
62
76
|
super
|
63
77
|
end
|
64
78
|
end
|
65
79
|
|
66
|
-
# The
|
80
|
+
# The operation resulted in unrecoverable data loss or corruption.
|
81
|
+
class DataLoss < NotOK
|
82
|
+
def initialize(code: GRPC::Core::StatusCodes::DATA_LOSS, **args)
|
83
|
+
super
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# The operation timed out.
|
67
88
|
class DeadlineExceeded < NotOK
|
68
89
|
def initialize(code: GRPC::Core::StatusCodes::DEADLINE_EXCEEDED, **args)
|
69
90
|
super
|
70
91
|
end
|
71
92
|
end
|
72
93
|
|
73
|
-
# The
|
94
|
+
# The operation was rejected because the system is not in a state required for the operation's execution.
|
95
|
+
class FailedPrecondition < NotOK
|
96
|
+
def initialize(code: GRPC::Core::StatusCodes::FAILED_PRECONDITION, **args)
|
97
|
+
super
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# The operation failed due to an internal error.
|
74
102
|
class InternalError < NotOK
|
75
103
|
def initialize(code: GRPC::Core::StatusCodes::INTERNAL, **args)
|
76
104
|
super
|
77
105
|
end
|
78
106
|
end
|
79
107
|
|
80
|
-
# The
|
108
|
+
# The operation was rejected because an argument was invalid.
|
81
109
|
class InvalidArgument < NotOK
|
82
110
|
def initialize(code: GRPC::Core::StatusCodes::INVALID_ARGUMENT, **args)
|
83
111
|
super
|
84
112
|
end
|
85
113
|
end
|
86
114
|
|
87
|
-
# The
|
115
|
+
# The requested entity was not found.
|
88
116
|
class NotFound < NotOK
|
89
117
|
def initialize(code: GRPC::Core::StatusCodes::NOT_FOUND, **args)
|
90
118
|
super
|
91
119
|
end
|
92
120
|
end
|
93
121
|
|
94
|
-
# The
|
122
|
+
# The operation was attempted past the valid range.
|
123
|
+
class OutOfRange < NotOK
|
124
|
+
def initialize(code: GRPC::Core::StatusCodes::OUT_OF_RANGE, **args)
|
125
|
+
super
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# The caller does not have permission to execute the specified operation.
|
130
|
+
class PermissionDenied < NotOK
|
131
|
+
def initialize(code: GRPC::Core::StatusCodes::PERMISSION_DENIED, **args)
|
132
|
+
super
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# The operation failed because a resource has been exhausted.
|
95
137
|
class ResourceExhausted < NotOK
|
96
138
|
def initialize(code: GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED, **args)
|
97
139
|
super
|
98
140
|
end
|
99
141
|
end
|
100
142
|
|
101
|
-
# The
|
143
|
+
# The operation was rejected because it did not have valid authentication credentials.
|
102
144
|
class Unauthenticated < NotOK
|
103
145
|
def initialize(code: GRPC::Core::StatusCodes::UNAUTHENTICATED, **args)
|
104
146
|
super
|
105
147
|
end
|
106
148
|
end
|
107
149
|
|
108
|
-
# The
|
150
|
+
# The operation failed because the service is unavailable.
|
109
151
|
class Unavailable < NotOK
|
110
152
|
def initialize(code: GRPC::Core::StatusCodes::UNAVAILABLE, **args)
|
111
153
|
super
|
112
154
|
end
|
113
155
|
end
|
114
156
|
|
115
|
-
# The
|
157
|
+
# The operation is not supported.
|
116
158
|
class Unimplemented < NotOK
|
117
159
|
def initialize(code: GRPC::Core::StatusCodes::UNIMPLEMENTED, **args)
|
118
160
|
super
|
119
161
|
end
|
120
162
|
end
|
121
163
|
|
164
|
+
# An unknown error occurred.
|
165
|
+
class Unknown < NotOK
|
166
|
+
def initialize(code: GRPC::Core::StatusCodes::UNKNOWN, **args)
|
167
|
+
super
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
122
171
|
GRPC_BAD_STATUS_ERROR_CLASS = {
|
172
|
+
GRPC::Aborted => Aborted,
|
173
|
+
GRPC::AlreadyExists => AlreadyExists,
|
123
174
|
GRPC::Cancelled => Cancelled,
|
175
|
+
GRPC::DataLoss => DataLoss,
|
124
176
|
GRPC::DeadlineExceeded => DeadlineExceeded,
|
177
|
+
GRPC::FailedPrecondition => FailedPrecondition,
|
125
178
|
GRPC::Internal => InternalError,
|
126
179
|
GRPC::InvalidArgument => InvalidArgument,
|
127
180
|
GRPC::NotFound => NotFound,
|
181
|
+
GRPC::OutOfRange => OutOfRange,
|
182
|
+
GRPC::PermissionDenied => PermissionDenied,
|
128
183
|
GRPC::ResourceExhausted => ResourceExhausted,
|
129
184
|
GRPC::Unauthenticated => Unauthenticated,
|
130
185
|
GRPC::Unavailable => Unavailable,
|
131
|
-
GRPC::Unimplemented => Unimplemented
|
186
|
+
GRPC::Unimplemented => Unimplemented,
|
187
|
+
GRPC::Unknown => Unknown
|
132
188
|
}.freeze
|
133
189
|
private_constant :GRPC_BAD_STATUS_ERROR_CLASS
|
190
|
+
|
191
|
+
# @private
|
192
|
+
def self.handle
|
193
|
+
yield
|
194
|
+
rescue Error
|
195
|
+
raise
|
196
|
+
rescue ArgumentError, TypeError => error
|
197
|
+
raise InvalidArgument.new(details: error.message)
|
198
|
+
rescue GRPC::BadStatus => error
|
199
|
+
raise NotOK.from_grpc_bad_status(error)
|
200
|
+
rescue => error
|
201
|
+
raise Error, error.message
|
202
|
+
end
|
134
203
|
end
|
135
204
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
module Hub
|
5
|
+
# @private
|
6
|
+
class AccessToken
|
7
|
+
def initialize(client_id:, client_secret:, **options)
|
8
|
+
@client_id = client_id
|
9
|
+
@client_secret = client_secret
|
10
|
+
|
11
|
+
@lock = Concurrent::ReadWriteLock.new
|
12
|
+
@result = None.new
|
13
|
+
|
14
|
+
@service = Cerbos::Service.new(
|
15
|
+
stub: Protobuf::Cerbos::Cloud::Apikey::V1::ApiKeyService::Stub,
|
16
|
+
credentials: GRPC::Core::ChannelCredentials.new,
|
17
|
+
**options
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch
|
22
|
+
token = @lock.with_read_lock { @result.token }
|
23
|
+
return token unless token.nil?
|
24
|
+
|
25
|
+
@lock.with_write_lock do
|
26
|
+
token = @result.token
|
27
|
+
return token unless token.nil?
|
28
|
+
|
29
|
+
attempt = @result.next_attempt
|
30
|
+
attempted_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
31
|
+
|
32
|
+
Error.handle do
|
33
|
+
request = Protobuf::Cerbos::Cloud::Apikey::V1::IssueAccessTokenRequest.new(
|
34
|
+
client_id: @client_id,
|
35
|
+
client_secret: @client_secret
|
36
|
+
)
|
37
|
+
|
38
|
+
response = Hub.with_circuit_breaker { @service.call(:issue_access_token, request, {}) }
|
39
|
+
|
40
|
+
@result = Success.new(response:, attempted_at:)
|
41
|
+
end
|
42
|
+
|
43
|
+
@result.token
|
44
|
+
rescue Error => error
|
45
|
+
@result = Failure.new(error:, attempt:, attempted_at:)
|
46
|
+
raise
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @private
|
53
|
+
class None
|
54
|
+
def token
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def next_attempt
|
59
|
+
1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
private_constant :None
|
63
|
+
|
64
|
+
# @private
|
65
|
+
class Success
|
66
|
+
def initialize(response:, attempted_at:)
|
67
|
+
@token = response.access_token
|
68
|
+
@refresh_at = attempted_at + response.expires_in.seconds - 300
|
69
|
+
end
|
70
|
+
|
71
|
+
def token
|
72
|
+
@token if Process.clock_gettime(Process::CLOCK_MONOTONIC) < @refresh_at
|
73
|
+
end
|
74
|
+
|
75
|
+
def next_attempt
|
76
|
+
1
|
77
|
+
end
|
78
|
+
end
|
79
|
+
private_constant :Success
|
80
|
+
|
81
|
+
# @private
|
82
|
+
class Failure
|
83
|
+
def initialize(error:, attempt:, attempted_at:)
|
84
|
+
@error = error
|
85
|
+
@attempt = attempt
|
86
|
+
@retry_at = attempted_at + retry_in
|
87
|
+
end
|
88
|
+
|
89
|
+
def token
|
90
|
+
remaining = @retry_at - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
91
|
+
return nil if remaining <= 0
|
92
|
+
raise @error if remaining.infinite?
|
93
|
+
|
94
|
+
begin
|
95
|
+
raise @error
|
96
|
+
rescue
|
97
|
+
raise Error::Cancelled.new(details: "Previous authentication attempt failed, backing off for %.3gs" % remaining)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def next_attempt
|
102
|
+
@attempt + 1
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def retry_in
|
108
|
+
case @error
|
109
|
+
when Error::Aborted, Error::Cancelled
|
110
|
+
-Float::INFINITY # immediately
|
111
|
+
when Error::Unauthenticated
|
112
|
+
Float::INFINITY # never
|
113
|
+
else
|
114
|
+
backoff
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
MIN_INTERVAL = 0.5
|
119
|
+
MAX_INTERVAL = 60
|
120
|
+
MULTIPLIER = 1.5
|
121
|
+
RANDOMIZATION_FACTOR = 0.5
|
122
|
+
USE_MAX_INTERVAL_AFTER_ATTEMPT = (Math.log(MAX_INTERVAL / MIN_INTERVAL) / Math.log(MULTIPLIER)).ceil
|
123
|
+
|
124
|
+
def backoff
|
125
|
+
interval = if @attempt > USE_MAX_INTERVAL_AFTER_ATTEMPT
|
126
|
+
MAX_INTERVAL
|
127
|
+
else
|
128
|
+
MULTIPLIER**(@attempt - 1) * MIN_INTERVAL
|
129
|
+
end
|
130
|
+
|
131
|
+
interval * (1 + (2 * rand - 1) * RANDOMIZATION_FACTOR)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
private_constant :Failure
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|