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
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
module Hub
|
5
|
+
# @private
|
6
|
+
def self.with_circuit_breaker(&)
|
7
|
+
CIRCUIT_BREAKER.run(&)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @private
|
11
|
+
class CircuitBreaker
|
12
|
+
def initialize(error_rate_threshold: 0.6, volume_threshold: 5, reset_timeout: 60, window_duration: 15, ignore_errors: [
|
13
|
+
GRPC::Aborted,
|
14
|
+
GRPC::AlreadyExists,
|
15
|
+
GRPC::Cancelled,
|
16
|
+
GRPC::DeadlineExceeded,
|
17
|
+
GRPC::FailedPrecondition
|
18
|
+
])
|
19
|
+
@error_rate_threshold = error_rate_threshold
|
20
|
+
@volume_threshold = volume_threshold
|
21
|
+
@reset_timeout = reset_timeout
|
22
|
+
@ignore_errors = ignore_errors
|
23
|
+
@counter = Counter.new(window_duration:)
|
24
|
+
@state = Concurrent::AtomicReference.new(:closed)
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
start
|
29
|
+
yield.tap do
|
30
|
+
succeeded
|
31
|
+
end
|
32
|
+
rescue => error
|
33
|
+
failed error
|
34
|
+
raise error
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def start
|
40
|
+
loop do
|
41
|
+
case state = @state.get
|
42
|
+
when Numeric
|
43
|
+
raise circuit_open if now < state + @reset_timeout
|
44
|
+
return if @state.compare_and_set(state, :half_open)
|
45
|
+
|
46
|
+
when :half_open
|
47
|
+
return
|
48
|
+
|
49
|
+
when :closed
|
50
|
+
succeeded, failed = @counter.stats
|
51
|
+
volume = succeeded + failed
|
52
|
+
return if volume < @volume_threshold
|
53
|
+
|
54
|
+
error_rate = failed.fdiv(volume)
|
55
|
+
return if error_rate < @error_rate_threshold
|
56
|
+
|
57
|
+
@state.compare_and_set(state, now)
|
58
|
+
raise circuit_open
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def succeeded
|
64
|
+
@counter.add_success
|
65
|
+
@state.compare_and_set(:half_open, :closed)
|
66
|
+
end
|
67
|
+
|
68
|
+
def failed(error)
|
69
|
+
return if @ignore_errors.any? { |ignored_error| error.is_a?(ignored_error) }
|
70
|
+
|
71
|
+
@counter.add_failure
|
72
|
+
@state.compare_and_set(:half_open, now)
|
73
|
+
end
|
74
|
+
|
75
|
+
def now
|
76
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
77
|
+
end
|
78
|
+
|
79
|
+
def circuit_open
|
80
|
+
Error::Cancelled.new(details: "Too many failures")
|
81
|
+
end
|
82
|
+
|
83
|
+
# @private
|
84
|
+
class Counter
|
85
|
+
def initialize(window_duration:)
|
86
|
+
@lock = Concurrent::ReadWriteLock.new
|
87
|
+
@window_duration = window_duration
|
88
|
+
@buckets = []
|
89
|
+
@current_bucket = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_success
|
93
|
+
current_bucket.add_success
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_failure
|
97
|
+
current_bucket.add_failure
|
98
|
+
end
|
99
|
+
|
100
|
+
def stats
|
101
|
+
@lock.with_read_lock do
|
102
|
+
time = now
|
103
|
+
|
104
|
+
index = 0
|
105
|
+
|
106
|
+
loop do
|
107
|
+
return [0, 0] if index == @buckets.size
|
108
|
+
break if valid?(@buckets[index], time)
|
109
|
+
|
110
|
+
index += 1
|
111
|
+
end
|
112
|
+
|
113
|
+
successes = 0
|
114
|
+
failures = 0
|
115
|
+
|
116
|
+
loop do
|
117
|
+
bucket = @buckets[index]
|
118
|
+
successes += bucket.successes
|
119
|
+
failures += bucket.failures
|
120
|
+
|
121
|
+
index += 1
|
122
|
+
return [successes, failures] if index == @buckets.size
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def current_bucket
|
130
|
+
time = now
|
131
|
+
bucket = @lock.with_read_lock { @current_bucket }
|
132
|
+
return bucket if bucket&.time == time
|
133
|
+
|
134
|
+
@lock.with_write_lock do
|
135
|
+
return @current_bucket if @current_bucket&.time == time
|
136
|
+
|
137
|
+
@current_bucket = Bucket.new(time:)
|
138
|
+
@buckets.push @current_bucket
|
139
|
+
@buckets.shift until valid?(@buckets.first, time)
|
140
|
+
|
141
|
+
@current_bucket
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def valid?(bucket, time)
|
146
|
+
bucket.time >= time - @window_duration
|
147
|
+
end
|
148
|
+
|
149
|
+
def now
|
150
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
151
|
+
end
|
152
|
+
|
153
|
+
# @private
|
154
|
+
class Bucket
|
155
|
+
attr_reader :time
|
156
|
+
|
157
|
+
def initialize(time:)
|
158
|
+
@time = time
|
159
|
+
@successes = Concurrent::AtomicFixnum.new
|
160
|
+
@failures = Concurrent::AtomicFixnum.new
|
161
|
+
end
|
162
|
+
|
163
|
+
def add_success
|
164
|
+
@successes.increment
|
165
|
+
end
|
166
|
+
|
167
|
+
def add_failure
|
168
|
+
@failures.increment
|
169
|
+
end
|
170
|
+
|
171
|
+
def successes
|
172
|
+
@successes.value
|
173
|
+
end
|
174
|
+
|
175
|
+
def failures
|
176
|
+
@failures.value
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
CIRCUIT_BREAKER = CircuitBreaker.new
|
183
|
+
private_constant :CIRCUIT_BREAKER
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
module Hub
|
5
|
+
# @private
|
6
|
+
class Service
|
7
|
+
def initialize(client_id:, client_secret:, stub:, credentials: GRPC::Core::ChannelCredentials.new, **options)
|
8
|
+
@access_token = AccessToken.new(client_id:, client_secret:, **options)
|
9
|
+
@service = Cerbos::Service.new(stub:, credentials:, **options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(rpc, request, metadata)
|
13
|
+
access_token = @access_token.fetch
|
14
|
+
Hub.with_circuit_breaker { @service.call(rpc, request, metadata.merge({"x-cerbos-auth": access_token})) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
module Hub
|
5
|
+
module Stores
|
6
|
+
# A client for interacting with policy stores in Cerbos Hub.
|
7
|
+
class Client
|
8
|
+
# Create a client for interacting with policy stores in Cerbos Hub.
|
9
|
+
#
|
10
|
+
# @param client_id [String] ID of the client credential to authenticate with Cerbos Hub.
|
11
|
+
# @param client_secret [String] secret of the client credential to authenticate with Cerbos Hub.
|
12
|
+
# @param target [String] address of the Cerbos Hub server.
|
13
|
+
# @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)).
|
14
|
+
# @param grpc_metadata [Hash{String, Symbol => String, Array<String>}] gRPC metadata (a.k.a. HTTP headers) to add to every request to the PDP.
|
15
|
+
# @param timeout [Numeric, nil] timeout for gRPC calls, in seconds (`nil` to never time out).
|
16
|
+
def initialize(client_id:, client_secret:, target: "api.cerbos.cloud:443", grpc_channel_args: {}, grpc_metadata: {}, timeout: nil)
|
17
|
+
@service = Service.new(
|
18
|
+
client_id:,
|
19
|
+
client_secret:,
|
20
|
+
stub: Protobuf::Cerbos::Cloud::Store::V1::CerbosStoreService::Stub,
|
21
|
+
target:,
|
22
|
+
grpc_channel_args:,
|
23
|
+
grpc_metadata:,
|
24
|
+
timeout:
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get file contents from a policy store.
|
29
|
+
#
|
30
|
+
# @param store_id [String] ID of the store from which to get files.
|
31
|
+
# @param files [Array<String>] paths of the files to retrieve.
|
32
|
+
# @param grpc_metadata [Hash{String, Symbol => String, Array<String>}] gRPC metadata (a.k.a. HTTP headers) to add to the request.
|
33
|
+
#
|
34
|
+
# @return [Output::GetFiles]
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# client.get_files(store_id: "MWPKEMFX3CK1", files: ["path/to/policy.yaml"])
|
38
|
+
def get_files(store_id:, files:, grpc_metadata: {})
|
39
|
+
Error.handle do
|
40
|
+
request = Protobuf::Cerbos::Cloud::Store::V1::GetFilesRequest.new(store_id:, files:)
|
41
|
+
|
42
|
+
response = @service.call(:get_files, request, grpc_metadata)
|
43
|
+
|
44
|
+
Output::GetFiles.from_protobuf(response)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# List file paths in a policy store.
|
49
|
+
#
|
50
|
+
# @param store_id [String] ID of the store from which to list files.
|
51
|
+
# @param filter [Input::FileFilter, Hash, nil] filter to limit which files are listed.
|
52
|
+
# @param grpc_metadata [Hash{String, Symbol => String, Array<String>}] gRPC metadata (a.k.a. HTTP headers) to add to the request.
|
53
|
+
#
|
54
|
+
# @return [Output::ListFiles]
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# client.list_files(store_id: "MWPKEMFX3CK1")
|
58
|
+
def list_files(store_id:, filter: nil, grpc_metadata: {})
|
59
|
+
Error.handle do
|
60
|
+
request = Protobuf::Cerbos::Cloud::Store::V1::ListFilesRequest.new(
|
61
|
+
store_id:,
|
62
|
+
filter: Cerbos::Input.coerce_optional(filter, Input::FileFilter)&.to_protobuf
|
63
|
+
)
|
64
|
+
|
65
|
+
response = @service.call(:list_files, request, grpc_metadata)
|
66
|
+
|
67
|
+
Output::ListFiles.from_protobuf(response)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Modify files in a policy store.
|
72
|
+
#
|
73
|
+
# This is a "patch" operation; files that aren't included in the request won't be modified.
|
74
|
+
#
|
75
|
+
# @param store_id [String] ID of the store in which to modify files.
|
76
|
+
# @param operations [Array<Input::FileOperation, Hash>] modifications to make.
|
77
|
+
# @param condition [Input::FileModificationCondition, Hash, nil] a condition that must be met for the modifications to be made.
|
78
|
+
# @param change_details [Input::ChangeDetails, Hash, nil] metadata describing the change being made.
|
79
|
+
# @param allow_unchanged [Boolean] allow modifications that do not change the state of the store. If `false` (the default), an {Error::OperationDiscarded} will be thrown if the modifications leave the store unchanged. If `true`, no error will be thrown and the current store version will be returned.
|
80
|
+
# @param grpc_metadata [Hash{String, Symbol => String, Array<String>}] gRPC metadata (a.k.a. HTTP headers) to add to the request.
|
81
|
+
#
|
82
|
+
# @return [Output::ModifyFiles]
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# client.modify_files(
|
86
|
+
# store_id: "MWPKEMFX3CK1",
|
87
|
+
# operations: [{add_or_update: {path: "policy.yaml", contents: ::File.binread("path/to/policy.yaml")}}]
|
88
|
+
# )
|
89
|
+
def modify_files(store_id:, operations:, condition: nil, change_details: nil, allow_unchanged: false, grpc_metadata: {})
|
90
|
+
Error.handle do
|
91
|
+
request = Protobuf::Cerbos::Cloud::Store::V1::ModifyFilesRequest.new(
|
92
|
+
store_id:,
|
93
|
+
operations: Cerbos::Input.coerce_array(operations, Input::FileOperation).map(&:to_protobuf),
|
94
|
+
condition: Cerbos::Input.coerce_optional(condition, Input::FileModificationCondition)&.to_protobuf_modify_files,
|
95
|
+
change_details: Cerbos::Input.coerce_optional(change_details, Input::ChangeDetails)&.to_protobuf
|
96
|
+
)
|
97
|
+
|
98
|
+
response = @service.call(:modify_files, request, grpc_metadata)
|
99
|
+
|
100
|
+
Output::ModifyFiles.from_protobuf(response)
|
101
|
+
end
|
102
|
+
rescue Error::OperationDiscarded => error
|
103
|
+
raise unless allow_unchanged
|
104
|
+
|
105
|
+
Output::ModifyFiles.new(
|
106
|
+
new_store_version: error.current_store_version,
|
107
|
+
changed: false
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Replace files in a policy store.
|
112
|
+
#
|
113
|
+
# This is a "put" operation; files that aren't included in the request will be removed from the store.
|
114
|
+
#
|
115
|
+
# @param store_id [String] ID of the store in which to replace files.
|
116
|
+
# @param files [Array<File, Hash>, nil] files to upload to the store. Mutually exclusive with `zipped_contents`.
|
117
|
+
# @param zipped_contents [String, nil] binary-encoded string containing zipped files to upload to the store.
|
118
|
+
# @param condition [Input::FileModificationCondition, Hash, nil] a condition that must be met for the replacement to be made.
|
119
|
+
# @param change_details [Input::ChangeDetails, Hash, nil] metadata describing the change being made.
|
120
|
+
# @param allow_unchanged [Boolean] allow replacements that do not change the state of the store. If `false` (the default), an {Error::OperationDiscarded} will be thrown if the contents match those of the store. If `true`, no error will be thrown and the current store version will be returned.
|
121
|
+
# @param grpc_metadata [Hash{String, Symbol => String, Array<String>}] gRPC metadata (a.k.a. HTTP headers) to add to the request.
|
122
|
+
#
|
123
|
+
# @return [Output::ReplaceFiles]
|
124
|
+
#
|
125
|
+
# @example Upload individual files
|
126
|
+
# client.replace_files(
|
127
|
+
# store_id: "MWPKEMFX3CK1",
|
128
|
+
# files: [{path: "policy.yaml", contents: ::File.binread("path/to/policy.yaml")}]
|
129
|
+
# )
|
130
|
+
#
|
131
|
+
# @example Upload zipped files
|
132
|
+
# client.replace_files(
|
133
|
+
# store_id: "MWPKEMFX3CK1",
|
134
|
+
# zipped_contents: ::File.binread("path/to/policies.zip")
|
135
|
+
# )
|
136
|
+
def replace_files(store_id:, files: nil, zipped_contents: nil, condition: nil, change_details: nil, allow_unchanged: false, grpc_metadata: {})
|
137
|
+
Error.handle do
|
138
|
+
request = Protobuf::Cerbos::Cloud::Store::V1::ReplaceFilesRequest.new(
|
139
|
+
store_id:,
|
140
|
+
files: files && Protobuf::Cerbos::Cloud::Store::V1::ReplaceFilesRequest::Files.new(files: files.map { |file| Cerbos::Input.coerce_required(file, File).to_protobuf }),
|
141
|
+
zipped_contents:,
|
142
|
+
condition: Cerbos::Input.coerce_optional(condition, Input::FileModificationCondition)&.to_protobuf_replace_files,
|
143
|
+
change_details: Cerbos::Input.coerce_optional(change_details, Input::ChangeDetails)&.to_protobuf
|
144
|
+
)
|
145
|
+
|
146
|
+
response = @service.call(:replace_files, request, grpc_metadata)
|
147
|
+
|
148
|
+
Output::ReplaceFiles.from_protobuf(response)
|
149
|
+
end
|
150
|
+
rescue Error::OperationDiscarded => error
|
151
|
+
raise unless allow_unchanged
|
152
|
+
|
153
|
+
Output::ReplaceFiles.new(
|
154
|
+
new_store_version: error.current_store_version,
|
155
|
+
ignored_files: error.ignored_files,
|
156
|
+
changed: false
|
157
|
+
)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
module Hub
|
5
|
+
module Stores
|
6
|
+
# Namespace for errors specific to Cerbos Hub stores.
|
7
|
+
module Error
|
8
|
+
# Error thrown when attempting to modify a store that is connected to a Git repository.
|
9
|
+
class CannotModifyGitConnectedStore < Cerbos::Error::NotOK
|
10
|
+
# @private
|
11
|
+
def initialize(error, _detail)
|
12
|
+
super(code: error.code, details: error.details, metadata: error.metadata)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Error thrown when a store modification is rejected because the condition specified in the request wasn't met.
|
17
|
+
class ConditionUnsatisfied < Cerbos::Error::NotOK
|
18
|
+
# The current version of the store.
|
19
|
+
attr_reader :current_store_version
|
20
|
+
|
21
|
+
# @private
|
22
|
+
def initialize(error, detail)
|
23
|
+
super(code: error.code, details: error.details, metadata: error.metadata)
|
24
|
+
@current_store_version = detail.current_store_version
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Error thrown when {Client#replace_files} fails because the request didn't contain any usable files.
|
29
|
+
class NoUsableFiles < Cerbos::Error::NotOK
|
30
|
+
# Paths of files that were provided in the request but were ignored.
|
31
|
+
#
|
32
|
+
# Files with unexpected paths, for example hidden files, will be ignored.
|
33
|
+
attr_reader :ignored_files
|
34
|
+
|
35
|
+
# @private
|
36
|
+
def initialize(error, detail)
|
37
|
+
super(code: error.code, details: error.details, metadata: error.metadata)
|
38
|
+
@ignored_files = detail.ignored_files
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Error thrown when a store modification is aborted because it doesn't change the state of the store.
|
43
|
+
#
|
44
|
+
# Use the `allow_unchanged` request parameter to avoid throwing an error and return the current store version instead.
|
45
|
+
class OperationDiscarded < Cerbos::Error::NotOK
|
46
|
+
# The current version of the store.
|
47
|
+
attr_reader :current_store_version
|
48
|
+
|
49
|
+
# Paths of files that were provided in the request but were ignored.
|
50
|
+
#
|
51
|
+
# Files with unexpected paths, for example hidden files, will be ignored.
|
52
|
+
attr_reader :ignored_files
|
53
|
+
|
54
|
+
# @private
|
55
|
+
def initialize(error, detail)
|
56
|
+
super(code: error.code, details: error.details, metadata: error.metadata)
|
57
|
+
@current_store_version = detail.current_store_version
|
58
|
+
@ignored_files = detail.ignored_files
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Error thrown when a store modification is rejected because it contains invalid files.
|
63
|
+
class ValidationFailure < Cerbos::Error::NotOK
|
64
|
+
# The validation failures.
|
65
|
+
attr_reader :errors
|
66
|
+
|
67
|
+
# @private
|
68
|
+
def initialize(error, detail)
|
69
|
+
super(code: error.code, details: error.details, metadata: error.metadata)
|
70
|
+
@errors = detail.errors.map { |file_error| Output::FileError.from_protobuf(file_error) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @private
|
75
|
+
def self.handle
|
76
|
+
Cerbos::Error.handle do
|
77
|
+
yield
|
78
|
+
rescue GRPC::BadStatus => error
|
79
|
+
raise from_grpc_bad_status(error)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# @private
|
84
|
+
def self.from_grpc_bad_status(error)
|
85
|
+
status = error.to_rpc_status
|
86
|
+
return error if status.nil?
|
87
|
+
|
88
|
+
status.details.each do |detail|
|
89
|
+
ERROR_FROM_DETAILS.each do |detail_class, error_class|
|
90
|
+
return error_class.new(error, detail.unpack(detail_class)) if detail.is(detail_class)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
error
|
95
|
+
end
|
96
|
+
|
97
|
+
ERROR_FROM_DETAILS = {
|
98
|
+
Protobuf::Cerbos::Cloud::Store::V1::ErrDetailCannotModifyGitConnectedStore => CannotModifyGitConnectedStore,
|
99
|
+
Protobuf::Cerbos::Cloud::Store::V1::ErrDetailConditionUnsatisfied => ConditionUnsatisfied,
|
100
|
+
Protobuf::Cerbos::Cloud::Store::V1::ErrDetailNoUsableFiles => NoUsableFiles,
|
101
|
+
Protobuf::Cerbos::Cloud::Store::V1::ErrDetailOperationDiscarded => OperationDiscarded,
|
102
|
+
Protobuf::Cerbos::Cloud::Store::V1::ErrDetailValidationFailure => ValidationFailure
|
103
|
+
}.freeze
|
104
|
+
private_constant :ERROR_FROM_DETAILS
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
module Hub
|
5
|
+
module Stores
|
6
|
+
# A file in a store.
|
7
|
+
File = Output.new_class(:path, :contents) do
|
8
|
+
# @!attribute [r] path
|
9
|
+
# The path of the file.
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
|
13
|
+
# @!attribute [r] contents
|
14
|
+
# The contents of the file (with binary encoding).
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
def self.from_protobuf(file)
|
18
|
+
new(path: file.path, contents: file.contents)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @private
|
22
|
+
def to_protobuf
|
23
|
+
Protobuf::Cerbos::Cloud::Store::V1::File.new(path:, contents:)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
module Hub
|
5
|
+
module Stores
|
6
|
+
module Input
|
7
|
+
class ChangeDetails
|
8
|
+
# Origin of a change that was made to a store.
|
9
|
+
#
|
10
|
+
# @abstract
|
11
|
+
class Origin
|
12
|
+
include AbstractClass
|
13
|
+
|
14
|
+
# Details of a change made to a store when syncing it with a Git repository.
|
15
|
+
class Git < Origin
|
16
|
+
# The Git repository with which the store was synced.
|
17
|
+
#
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :repo
|
20
|
+
|
21
|
+
# The Git ref with which the store was synced.
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
attr_reader :ref
|
25
|
+
|
26
|
+
# The hash of the commit with which the store was synced.
|
27
|
+
#
|
28
|
+
# @return [String]
|
29
|
+
attr_reader :hash
|
30
|
+
|
31
|
+
# The message of the commit with which the store was synced.
|
32
|
+
#
|
33
|
+
# @return [String]
|
34
|
+
attr_reader :message
|
35
|
+
|
36
|
+
# The committer of the commit with which the store was synced.
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
attr_reader :committer
|
40
|
+
|
41
|
+
# The commit date of the commit with which the store was synced.
|
42
|
+
#
|
43
|
+
# @return [Time]
|
44
|
+
# @return [nil] if not provided
|
45
|
+
attr_reader :commit_date
|
46
|
+
|
47
|
+
# The author of the commit with which the store was synced.
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
attr_reader :author
|
51
|
+
|
52
|
+
# The author date of the commit with which the store was synced.
|
53
|
+
#
|
54
|
+
# @return [Time]
|
55
|
+
# @return [nil] if not provided
|
56
|
+
attr_reader :author_date
|
57
|
+
|
58
|
+
# Specify details of a change made to a store when syncing it with a Git repository.
|
59
|
+
#
|
60
|
+
# @param repo [String] the Git repository with which the store was synced.
|
61
|
+
# @param ref [String] the Git ref with which the store was synced.
|
62
|
+
# @param hash [String] the hash of the commit with which the store was synced.
|
63
|
+
# @param message [String] the message of the commit with which the store was synced.
|
64
|
+
# @param committer [String] the committer of the commit with which the store was synced.
|
65
|
+
# @param commit_date [Time, nil] the commit date of the commit with which the store was synced.
|
66
|
+
# @param author [String] the author of the commit with which the store was synced.
|
67
|
+
# @param author_date [Time, nil] the author date of the commit with which the store was synced.
|
68
|
+
def initialize(
|
69
|
+
repo: "",
|
70
|
+
ref: "",
|
71
|
+
hash: "",
|
72
|
+
message: "",
|
73
|
+
committer: "",
|
74
|
+
commit_date: nil,
|
75
|
+
author: "",
|
76
|
+
author_date: nil
|
77
|
+
)
|
78
|
+
@repo = repo
|
79
|
+
@ref = ref
|
80
|
+
@hash = hash
|
81
|
+
@message = message
|
82
|
+
@committer = committer
|
83
|
+
@commit_date = commit_date
|
84
|
+
@author = author
|
85
|
+
@author_date = author_date
|
86
|
+
end
|
87
|
+
|
88
|
+
# @private
|
89
|
+
def to_protobuf
|
90
|
+
Protobuf::Cerbos::Cloud::Store::V1::ChangeDetails::Git.new(
|
91
|
+
repo:,
|
92
|
+
ref:,
|
93
|
+
hash:,
|
94
|
+
message:,
|
95
|
+
committer:,
|
96
|
+
commit_date: commit_date && Google::Protobuf::Timestamp.new.from_time(commit_date),
|
97
|
+
author:,
|
98
|
+
author_date: author_date && Google::Protobuf::Timestamp.new.from_time(author_date)
|
99
|
+
)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Details of a change made to a store by an internal application.
|
104
|
+
class Internal < Origin
|
105
|
+
# The source of the change.
|
106
|
+
#
|
107
|
+
# @return [String]
|
108
|
+
attr_reader :source
|
109
|
+
|
110
|
+
# User-defined metadata about the origin of the change.
|
111
|
+
#
|
112
|
+
# @return [Cerbos::Input::Attributes]
|
113
|
+
attr_reader :metadata
|
114
|
+
|
115
|
+
# Specify details of a change made to a store by an internal application.
|
116
|
+
#
|
117
|
+
# @param source [String] the source of the change.
|
118
|
+
# @param metadata [Cerbos::Input::Attributes, Hash] user-defined metadata about the origin of the change.
|
119
|
+
def initialize(source: "", metadata: {})
|
120
|
+
@source = source
|
121
|
+
@metadata = Cerbos::Input.coerce_required(metadata, Cerbos::Input::Attributes)
|
122
|
+
end
|
123
|
+
|
124
|
+
# @private
|
125
|
+
def to_protobuf
|
126
|
+
Protobuf::Cerbos::Cloud::Store::V1::ChangeDetails::Internal.new(source:, metadata: metadata.to_protobuf)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# @private
|
131
|
+
def self.from_h(**origin)
|
132
|
+
case origin
|
133
|
+
in git:, **nil
|
134
|
+
Git.new(**git)
|
135
|
+
in internal:, **nil
|
136
|
+
Internal.new(**internal)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerbos
|
4
|
+
module Hub
|
5
|
+
module Stores
|
6
|
+
module Input
|
7
|
+
class ChangeDetails
|
8
|
+
# Metadata describing the uploader who made a change to a store.
|
9
|
+
class Uploader
|
10
|
+
# The name of the uploader.
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :name
|
14
|
+
|
15
|
+
# User-defined metadata about the origin of the change.
|
16
|
+
#
|
17
|
+
# @return [Cerbos::Input::Attributes]
|
18
|
+
attr_reader :metadata
|
19
|
+
|
20
|
+
# Specify metadata describing the uploader who made a change to a store.
|
21
|
+
#
|
22
|
+
# @param name [String] the name of the uploader.
|
23
|
+
# @param metadata [Cerbos::Input::Attributes, Hash] user-defined metadata about the origin of the change.
|
24
|
+
def initialize(name: "", metadata: {})
|
25
|
+
@name = name
|
26
|
+
@metadata = Cerbos::Input.coerce_required(metadata, Cerbos::Input::Attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @private
|
30
|
+
def to_protobuf
|
31
|
+
Protobuf::Cerbos::Cloud::Store::V1::ChangeDetails::Uploader.new(name:, metadata: metadata.to_protobuf)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|