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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/README.md +31 -3
  4. data/cerbos.gemspec +3 -2
  5. data/lib/cerbos/abstract_class.rb +10 -0
  6. data/lib/cerbos/client.rb +29 -44
  7. data/lib/cerbos/error.rb +79 -10
  8. data/lib/cerbos/hub/access_token.rb +137 -0
  9. data/lib/cerbos/hub/circuit_breaker.rb +185 -0
  10. data/lib/cerbos/hub/service.rb +18 -0
  11. data/lib/cerbos/hub/stores/client.rb +162 -0
  12. data/lib/cerbos/hub/stores/error.rb +108 -0
  13. data/lib/cerbos/hub/stores/file.rb +28 -0
  14. data/lib/cerbos/hub/stores/input/change_details/origin.rb +144 -0
  15. data/lib/cerbos/hub/stores/input/change_details/uploader.rb +38 -0
  16. data/lib/cerbos/hub/stores/input/change_details.rb +52 -0
  17. data/lib/cerbos/hub/stores/input/file_filter.rb +30 -0
  18. data/lib/cerbos/hub/stores/input/file_modification_condition.rb +34 -0
  19. data/lib/cerbos/hub/stores/input/file_operation.rb +66 -0
  20. data/lib/cerbos/hub/stores/input/string_match.rb +88 -0
  21. data/lib/cerbos/hub/stores/input.rb +17 -0
  22. data/lib/cerbos/hub/stores/output/get_files.rb +31 -0
  23. data/lib/cerbos/hub/stores/output/list_files.rb +31 -0
  24. data/lib/cerbos/hub/stores/output/modify_files.rb +35 -0
  25. data/lib/cerbos/hub/stores/output/replace_files.rb +43 -0
  26. data/lib/cerbos/hub/stores/output.rb +16 -0
  27. data/lib/cerbos/hub/stores.rb +15 -0
  28. data/lib/cerbos/hub.rb +12 -0
  29. data/lib/cerbos/input.rb +2 -1
  30. data/lib/cerbos/output/plan_resources.rb +10 -2
  31. data/lib/cerbos/protobuf/buf/validate/validate_pb.rb +6 -5
  32. data/lib/cerbos/protobuf/cerbos/cloud/apikey/v1/apikey_pb.rb +26 -0
  33. data/lib/cerbos/protobuf/cerbos/cloud/apikey/v1/apikey_services_pb.rb +32 -0
  34. data/lib/cerbos/protobuf/cerbos/cloud/store/v1/store_pb.rb +52 -0
  35. data/lib/cerbos/protobuf/cerbos/cloud/store/v1/store_services_pb.rb +35 -0
  36. data/lib/cerbos/protobuf/cerbos/effect/v1/effect_pb.rb +1 -1
  37. data/lib/cerbos/protobuf/cerbos/engine/v1/engine_pb.rb +4 -2
  38. data/lib/cerbos/protobuf/cerbos/request/v1/request_pb.rb +2 -2
  39. data/lib/cerbos/protobuf/cerbos/response/v1/response_pb.rb +2 -2
  40. data/lib/cerbos/protobuf/cerbos/schema/v1/schema_pb.rb +1 -1
  41. data/lib/cerbos/protobuf/cerbos/svc/v1/svc_pb.rb +2 -4
  42. data/lib/cerbos/protobuf/google/api/annotations_pb.rb +1 -1
  43. data/lib/cerbos/protobuf/google/api/field_behavior_pb.rb +1 -1
  44. data/lib/cerbos/protobuf/google/api/http_pb.rb +2 -2
  45. data/lib/cerbos/protobuf/google/api/visibility_pb.rb +19 -0
  46. data/lib/cerbos/protobuf/grpc/health/v1/health_pb.rb +4 -2
  47. data/lib/cerbos/protobuf/grpc/health/v1/health_services_pb.rb +12 -2
  48. data/lib/cerbos/protobuf/protoc-gen-openapiv2/options/annotations_pb.rb +1 -1
  49. data/lib/cerbos/protobuf/protoc-gen-openapiv2/options/openapiv2_pb.rb +1 -1
  50. data/lib/cerbos/protobuf.rb +2 -0
  51. data/lib/cerbos/service.rb +33 -0
  52. data/lib/cerbos/version.rb +1 -1
  53. data/lib/cerbos.rb +7 -1
  54. data/yard_extensions.rb +8 -1
  55. 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