cerbos 0.11.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 +8 -1
- data/README.md +30 -2
- data/cerbos.gemspec +1 -0
- data/lib/cerbos/abstract_class.rb +10 -0
- data/lib/cerbos/client.rb +24 -41
- 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/protobuf/buf/validate/validate_pb.rb +6 -6
- 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 +2 -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 +1 -1
- 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 +1 -1
- 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 +45 -3
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,12 @@
|
|
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
|
+
|
5
11
|
## [0.11.0] - 2025-06-03
|
6
12
|
|
7
13
|
### Added
|
@@ -113,7 +119,8 @@ No notable changes.
|
|
113
119
|
|
114
120
|
- Initial implementation of `Cerbos::Client` ([#2](https://github.com/cerbos/cerbos-sdk-ruby/pull/2))
|
115
121
|
|
116
|
-
[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
|
117
124
|
[0.11.0]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.10.0...v0.11.0
|
118
125
|
[0.10.0]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.9.1...v0.10.0
|
119
126
|
[0.9.1]: https://github.com/cerbos/cerbos-sdk-ruby/compare/v0.9.0...v0.9.1
|
data/README.md
CHANGED
@@ -7,7 +7,7 @@
|
|
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
|
|
@@ -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
@@ -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
|
@@ -225,7 +224,7 @@ module Cerbos
|
|
225
224
|
# plan.conditional? # => true
|
226
225
|
# plan.condition # => #<Cerbos::Output::PlanResources::Expression ...>
|
227
226
|
def plan_resources(principal:, resource:, action: "", actions: [], aux_data: nil, include_metadata: false, request_id: SecureRandom.uuid, grpc_metadata: {})
|
228
|
-
|
227
|
+
Error.handle do
|
229
228
|
request = Protobuf::Cerbos::Request::V1::PlanResourcesRequest.new(
|
230
229
|
principal: Input.coerce_required(principal, Input::Principal).to_protobuf,
|
231
230
|
resource: Input.coerce_required(resource, Input::ResourceQuery).to_protobuf,
|
@@ -236,7 +235,7 @@ module Cerbos
|
|
236
235
|
request_id: request_id
|
237
236
|
)
|
238
237
|
|
239
|
-
response =
|
238
|
+
response = @cerbos_service.call(:plan_resources, request, grpc_metadata)
|
240
239
|
|
241
240
|
Output::PlanResources.from_protobuf(response).tap do |output|
|
242
241
|
handle_validation_errors output
|
@@ -250,10 +249,10 @@ module Cerbos
|
|
250
249
|
#
|
251
250
|
# @return [Output::ServerInfo]
|
252
251
|
def server_info(grpc_metadata: {})
|
253
|
-
|
252
|
+
Error.handle do
|
254
253
|
request = Protobuf::Cerbos::Request::V1::ServerInfoRequest.new
|
255
254
|
|
256
|
-
response =
|
255
|
+
response = @cerbos_service.call(:server_info, request, grpc_metadata)
|
257
256
|
|
258
257
|
Output::ServerInfo.from_protobuf(response)
|
259
258
|
end
|
@@ -261,18 +260,6 @@ module Cerbos
|
|
261
260
|
|
262
261
|
private
|
263
262
|
|
264
|
-
def handle_errors
|
265
|
-
yield
|
266
|
-
rescue Error
|
267
|
-
raise
|
268
|
-
rescue ArgumentError, TypeError => error
|
269
|
-
raise Error::InvalidArgument.new(details: error.message)
|
270
|
-
rescue GRPC::BadStatus => error
|
271
|
-
raise Error::NotOK.from_grpc_bad_status(error)
|
272
|
-
rescue => error
|
273
|
-
raise Error, error.message
|
274
|
-
end
|
275
|
-
|
276
263
|
def handle_validation_errors(output)
|
277
264
|
return if @on_validation_error == :return
|
278
265
|
|
@@ -283,9 +270,5 @@ module Cerbos
|
|
283
270
|
|
284
271
|
@on_validation_error.call validation_errors
|
285
272
|
end
|
286
|
-
|
287
|
-
def perform_request(service, rpc, request, metadata)
|
288
|
-
service.public_send(rpc, request, metadata: @grpc_metadata.merge(metadata.transform_keys(&:to_sym)))
|
289
|
-
end
|
290
273
|
end
|
291
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
|