diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml
index bd0dbda439b..d5477a47faf 100644
--- a/.rubocop_todo/gitlab/strong_memoize_attr.yml
+++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml
@@ -174,8 +174,6 @@ Gitlab/StrongMemoizeAttr:
- 'app/services/members/invitation_reminder_email_service.rb'
- 'app/services/merge_requests/build_service.rb'
- 'app/services/merge_requests/mergeability/detailed_merge_status_service.rb'
- - 'app/services/merge_requests/mergeability/logger.rb'
- - 'app/services/merge_requests/mergeability/run_checks_service.rb'
- 'app/services/merge_requests/mergeability_check_service.rb'
- 'app/services/merge_requests/outdated_discussion_diff_lines_service.rb'
- 'app/services/merge_requests/pushed_branches_service.rb'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index dd60bd05a19..d2ab6768fa3 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -304,6 +304,7 @@
{"name":"grpc","version":"1.63.0","platform":"x86_64-darwin","checksum":"a814414ff178e89ee3ad0cc2a826ce1ca96c68063effb81affe3e5ceff7b44cc"},
{"name":"grpc","version":"1.63.0","platform":"x86_64-linux","checksum":"41a90a597f44959c8dbb94619db2b0c0939a768569a5dfad41fffa227eb1287d"},
{"name":"grpc-google-iam-v1","version":"1.5.0","platform":"ruby","checksum":"cea356d150dac69751f6a4c71f1571c8022c69d9f4ce9c18139200932c19374e"},
+{"name":"grpc-tools","version":"1.63.0","platform":"ruby","checksum":"133de88d6e8dbcbf846c22a5c693c1704092d9613c9ade6f749053e6a25bea40"},
{"name":"gssapi","version":"1.3.1","platform":"ruby","checksum":"c51cf30842ee39bd93ce7fc33e20405ff8a04cda9dec6092071b61258284aee1"},
{"name":"guard","version":"2.16.2","platform":"ruby","checksum":"71ba7abaddecc8be91ab77bbaf78f767246603652ebbc7b976fda497ebdc8fbb"},
{"name":"guard-compat","version":"1.2.1","platform":"ruby","checksum":"3ad21ab0070107f92edfd82610b5cdc2fb8e368851e72362ada9703443d646fe"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 2ff6b98235e..936e3315eb7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -97,7 +97,9 @@ PATH
PATH
remote: gems/gitlab-secret_detection
specs:
- gitlab-secret_detection (0.1.0)
+ gitlab-secret_detection (0.1.1)
+ grpc (= 1.63.0)
+ grpc-tools (= 1.63.0)
parallel (~> 1.22)
re2 (~> 2.4)
toml-rb (~> 2.2)
@@ -942,6 +944,7 @@ GEM
google-protobuf (~> 3.18)
googleapis-common-protos (~> 1.4)
grpc (~> 1.41)
+ grpc-tools (1.63.0)
gssapi (1.3.1)
ffi (>= 1.0.1)
guard (2.16.2)
diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum
index fbd433050f6..4102eb6f693 100644
--- a/Gemfile.next.checksum
+++ b/Gemfile.next.checksum
@@ -305,6 +305,7 @@
{"name":"grpc","version":"1.63.0","platform":"x86_64-darwin","checksum":"a814414ff178e89ee3ad0cc2a826ce1ca96c68063effb81affe3e5ceff7b44cc"},
{"name":"grpc","version":"1.63.0","platform":"x86_64-linux","checksum":"41a90a597f44959c8dbb94619db2b0c0939a768569a5dfad41fffa227eb1287d"},
{"name":"grpc-google-iam-v1","version":"1.5.0","platform":"ruby","checksum":"cea356d150dac69751f6a4c71f1571c8022c69d9f4ce9c18139200932c19374e"},
+{"name":"grpc-tools","version":"1.63.0","platform":"ruby","checksum":"133de88d6e8dbcbf846c22a5c693c1704092d9613c9ade6f749053e6a25bea40"},
{"name":"gssapi","version":"1.3.1","platform":"ruby","checksum":"c51cf30842ee39bd93ce7fc33e20405ff8a04cda9dec6092071b61258284aee1"},
{"name":"guard","version":"2.16.2","platform":"ruby","checksum":"71ba7abaddecc8be91ab77bbaf78f767246603652ebbc7b976fda497ebdc8fbb"},
{"name":"guard-compat","version":"1.2.1","platform":"ruby","checksum":"3ad21ab0070107f92edfd82610b5cdc2fb8e368851e72362ada9703443d646fe"},
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index aeeeeaba95f..1b814327380 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -97,7 +97,9 @@ PATH
PATH
remote: gems/gitlab-secret_detection
specs:
- gitlab-secret_detection (0.1.0)
+ gitlab-secret_detection (0.1.1)
+ grpc (= 1.63.0)
+ grpc-tools (= 1.63.0)
parallel (~> 1.22)
re2 (~> 2.4)
toml-rb (~> 2.2)
@@ -952,6 +954,7 @@ GEM
google-protobuf (~> 3.18)
googleapis-common-protos (~> 1.4)
grpc (~> 1.41)
+ grpc-tools (1.63.0)
gssapi (1.3.1)
ffi (>= 1.0.1)
guard (2.16.2)
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index 92f7c8d6137..2bb70dd55d0 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -182,14 +182,14 @@ export default {
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165157) in GitLab 17.5 [with a flag](../../administration/feature_flags.md) named `admin_agnostic_token_finder`. Disabled by default.
> - [Feed tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169821) in GitLab 17.6.
+> - [OAuth application secrets added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172985) in GitLab 17.7.
FLAG:
The availability of this feature is controlled by a feature flag.
@@ -37,6 +38,7 @@ Supported tokens:
- [Personal access tokens](../../user/profile/personal_access_tokens.md)
- [Deploy tokens](../../user/project/deploy_tokens/index.md)
- [Feed tokens](../../security/tokens/index.md#feed-token)
+- [OAuth application secrets](../../integration/oauth_provider.md)
```plaintext
POST /api/v4/admin/token
diff --git a/doc/ci/pipelines/merge_trains.md b/doc/ci/pipelines/merge_trains.md
index 6f030fc412d..664e5fbcb22 100644
--- a/doc/ci/pipelines/merge_trains.md
+++ b/doc/ci/pipelines/merge_trains.md
@@ -156,6 +156,10 @@ You can also remove (**{close}**) a merge request from the merge train details v
> - Auto-merge for merge trains [enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/470667) on GitLab.com in GitLab 17.2.
> - Auto-merge for merge trains [enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/470667) by default in GitLab 17.4.
+FLAG:
+The availability of this feature is controlled by a feature flag.
+For more information, see the history.
+
Prerequisites:
- You must have [permissions](../../user/permissions.md) to merge or push to the target branch.
diff --git a/doc/development/event_store.md b/doc/development/event_store.md
index b25357183f3..325d41674c1 100644
--- a/doc/development/event_store.md
+++ b/doc/development/event_store.md
@@ -388,6 +388,15 @@ store.subscribe ::Security::RefreshProjectPoliciesWorker,
The `handle_event` method in the subscriber worker is called for each of the events in the group.
+## Remove a subscriber
+
+As `Gitlab::EventStore` is backed by Sidekiq we follow the same guides for
+[removing Sidekiq workers](sidekiq/compatibility_across_updates.md#removing-worker-classes) starting
+with:
+
+- Removing the subscription in order to remove any code that enqueues the job
+- Making the subscriber worker no-op. For this we need to remove the `Gitlab::EventStore::Subscriber` module from the worker.
+
## Testing
### Testing the publisher
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index 7c3deb678de..acc857695c4 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -140,10 +140,7 @@ application are also deleted.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/374588) in GitLab 15.4 [with a flag](../administration/feature_flags.md) named `hash_oauth_secrets`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/374588) in GitLab 15.8.
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/374588) in GitLab 15.9.
-
-FLAG:
-On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../administration/feature_flags.md) named `hash_oauth_secrets`.
-On GitLab.com, this feature is available.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113892) in GitLab 15.10. Feature flag `hash_oauth_secrets` removed.
By default, GitLab stores OAuth application secrets in the database in hashed format. These secrets are only available to users immediately after creating OAuth applications. In
earlier versions of GitLab, application secrets are stored as plain text in the database.
diff --git a/gems/gitlab-secret_detection/.rubocop.yml b/gems/gitlab-secret_detection/.rubocop.yml
index f591ab360a9..c4b822538de 100644
--- a/gems/gitlab-secret_detection/.rubocop.yml
+++ b/gems/gitlab-secret_detection/.rubocop.yml
@@ -3,6 +3,8 @@ inherit_from:
AllCops:
NewCops: enable
+ Exclude:
+ - lib/gitlab/secret_detection/grpc/generated/*.rb
Style/HashSyntax:
EnforcedShorthandSyntax: consistent
diff --git a/gems/gitlab-secret_detection/Gemfile.lock b/gems/gitlab-secret_detection/Gemfile.lock
index 71f3da8daa5..20e75c64fa7 100644
--- a/gems/gitlab-secret_detection/Gemfile.lock
+++ b/gems/gitlab-secret_detection/Gemfile.lock
@@ -1,7 +1,9 @@
PATH
remote: .
specs:
- gitlab-secret_detection (0.1.0)
+ gitlab-secret_detection (0.1.1)
+ grpc (= 1.63.0)
+ grpc-tools (= 1.63.0)
parallel (~> 1.22)
re2 (~> 2.4)
toml-rb (~> 2.2)
@@ -45,6 +47,13 @@ GEM
rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17)
rubocop-rspec (~> 2.22)
+ google-protobuf (3.25.5)
+ googleapis-common-protos-types (1.16.0)
+ google-protobuf (>= 3.18, < 5.a)
+ grpc (1.63.0)
+ google-protobuf (~> 3.25)
+ googleapis-common-protos-types (~> 1.0)
+ grpc-tools (1.63.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
@@ -154,4 +163,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.22
+ 2.5.23
diff --git a/gems/gitlab-secret_detection/gitlab-secret_detection.gemspec b/gems/gitlab-secret_detection/gitlab-secret_detection.gemspec
index 203f0f11fa5..c3869fcacfd 100644
--- a/gems/gitlab-secret_detection/gitlab-secret_detection.gemspec
+++ b/gems/gitlab-secret_detection/gitlab-secret_detection.gemspec
@@ -24,6 +24,8 @@ Gem::Specification.new do |spec|
spec.files = Dir['lib/**/*.rb']
spec.require_paths = ["lib"]
+ spec.add_runtime_dependency "grpc", "= 1.63.0"
+ spec.add_runtime_dependency "grpc-tools", "= 1.63.0"
spec.add_runtime_dependency "parallel", "~> 1.22"
spec.add_runtime_dependency "re2", "~> 2.4"
spec.add_runtime_dependency "toml-rb", "~> 2.2"
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb
index aae3343aa6f..62c73a35b7d 100644
--- a/gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection.rb
@@ -5,6 +5,8 @@ require_relative 'secret_detection/finding'
require_relative 'secret_detection/response'
require_relative 'secret_detection/scan'
require_relative 'secret_detection/scan_diffs'
+require_relative 'secret_detection/grpc'
+require_relative 'secret_detection/utils'
module Gitlab
module SecretDetection
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc.rb
new file mode 100644
index 00000000000..8afa8ea5365
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative 'grpc/client/stream_request_enumerator'
+require_relative 'grpc/client/grpc_client'
+require_relative 'grpc/generated/secret_detection_pb'
+require_relative 'grpc/generated/secret_detection_services_pb'
+
+module Gitlab
+ module SecretDetection
+ module GRPC
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/client/grpc_client.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/client/grpc_client.rb
new file mode 100644
index 00000000000..708aee46330
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/client/grpc_client.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require 'grpc'
+require 'logger'
+require_relative '../../utils'
+require_relative 'stream_request_enumerator'
+
+module Gitlab
+ module SecretDetection
+ module GRPC
+ class Client
+ include Gitlab::SecretDetection::Utils::StrongMemoize
+
+ # Time to wait for the response from the service
+ REQUEST_TIMEOUT_SECONDS = 10 # 10 seconds
+
+ def initialize(host, secure: false, compression: true, logger: Logger.new($stdout))
+ @host = host
+ @secure = secure
+ @compression = compression
+ @logger = logger
+ end
+
+ # Triggers Secret Detection service's `/Scan` gRPC endpoint. To keep it consistent with SDS gem interface,
+ # this method transforms the gRPC response to +Gitlab::SecretDetection::Response+.
+ # Furthermore, any errors that are raised by the service will be translated to
+ # +Gitlab::SecretDetection::Response+ type by assiging a appropriate +status+ value to it.
+ def run_scan(request:, auth_token:, extra_headers: {})
+ with_rescued_errors do
+ grpc_response = stub.scan(
+ request,
+ metadata: build_metadata(auth_token, extra_headers),
+ deadline: request_deadline
+ )
+
+ convert_to_core_response(grpc_response)
+ end
+ end
+
+ # Triggers Secret Detection service's `/ScanStream` gRPC endpoint.
+ #
+ # To keep it consistent with SDS gem interface, this method transforms the gRPC response to
+ # +Gitlab::SecretDetection::Response+ type. Furthermore, any errors that are raised by the service will be
+ # translated to +Gitlab::SecretDetection::Response+ type by assiging a appropriate +status+ value to it.
+ #
+ # Note: If one of the stream requests result in an error, the stream will end immediately without processing the
+ # remaining requests.
+ def run_scan_stream(requests:, auth_token:, extra_headers: {})
+ request_stream = Gitlab::SecretDetection::GRPC::StreamRequestEnumerator.new(requests)
+ results = []
+ with_rescued_errors do
+ stub.scan_stream(
+ request_stream.each_item,
+ metadata: build_metadata(auth_token, extra_headers),
+ deadline: request_deadline
+ ).each do |grpc_response|
+ response = convert_to_core_response(grpc_response)
+ if block_given?
+ yield response
+ else
+ results << response
+ end
+ end
+ results
+ end
+ end
+
+ private
+
+ attr_reader :secure, :host, :compression, :logger
+
+ def stub
+ Gitlab::SecretDetection::GRPC::Scanner::Stub.new(
+ host,
+ channel_credentials,
+ channel_args:
+ )
+ end
+
+ strong_memoize_attr :stub
+
+ def channel_args
+ default_options = {
+ 'grpc.keepalive_permit_without_calls' => 1,
+ 'grpc.keepalive_time_ms' => 30000, # 30 seconds
+ 'grpc.keepalive_timeout_ms' => 10000 # 10 seconds timeout for keepalive response
+ }
+
+ compression_options = ::GRPC::Core::CompressionOptions
+ .new(default_algorithm: :gzip)
+ .to_channel_arg_hash
+
+ default_options.merge!(compression_options) if compression
+
+ default_options.freeze
+ end
+
+ def channel_credentials
+ return :this_channel_is_insecure unless secure
+
+ certs = Gitlab::SecretDetection::Utils::X509::Certificate.ca_certs_bundle
+
+ ::GRPC::Core::ChannelCredentials.new(certs)
+ end
+
+ def build_metadata(token, extra_headers = {})
+ { 'x-sd-auth' => token }.merge!(extra_headers).freeze
+ end
+
+ def request_deadline
+ Time.now + REQUEST_TIMEOUT_SECONDS
+ end
+
+ def with_rescued_errors
+ yield
+ rescue ::GRPC::Unauthenticated
+ SecretDetection::Response.new(SecretDetection::Status::AUTH_ERROR)
+ rescue ::GRPC::InvalidArgument => e
+ SecretDetection::Response.new(
+ SecretDetection::Status::INPUT_ERROR, nil, { message: e.details, **e.metadata }
+ )
+ rescue ::GRPC::Unknown, ::GRPC::BadStatus => e
+ SecretDetection::Response.new(
+ SecretDetection::Status::SCAN_ERROR, nil, { message: e.details }
+ )
+ end
+
+ def convert_to_core_response(grpc_response)
+ response = grpc_response.to_h
+
+ SecretDetection::Response.new(
+ response[:status],
+ response[:results]
+ )
+ rescue StandardError => e
+ logger.error("Failed to convert to core response: #{e}")
+ SecretDetection::Response.new(SecretDetection::Status::SCAN_ERROR)
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/client/stream_request_enumerator.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/client/stream_request_enumerator.rb
new file mode 100644
index 00000000000..305a675f93f
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/client/stream_request_enumerator.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SecretDetection
+ module GRPC
+ class StreamRequestEnumerator
+ def initialize(requests = [])
+ @requests = requests
+ end
+
+ # yields a request, waiting between 0 and 1 seconds between requests
+ #
+ # @return an Enumerable that yields a request input
+ def each_item
+ return enum_for(:each_item) unless block_given?
+
+ @requests.each do |request|
+ yield request
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/.gitkeep b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/secret_detection_pb.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/secret_detection_pb.rb
new file mode 100644
index 00000000000..93a86f1811e
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/secret_detection_pb.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: secret_detection.proto
+
+require 'google/protobuf'
+
+
+descriptor_data = "\n\x16secret_detection.proto\x12\x17gitlab.secret_detection\"\xfc\x03\n\x0bScanRequest\x12>\n\x08payloads\x18\x01 \x03(\x0b\x32,.gitlab.secret_detection.ScanRequest.Payload\x12\x19\n\x0ctimeout_secs\x18\x02 \x01(\x02H\x00\x88\x01\x01\x12!\n\x14payload_timeout_secs\x18\x03 \x01(\x02H\x01\x88\x01\x01\x12\x42\n\nexclusions\x18\x04 \x03(\x0b\x32..gitlab.secret_detection.ScanRequest.Exclusion\x12\x0c\n\x04tags\x18\x05 \x03(\t\x1a#\n\x07Payload\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\x1a\x66\n\tExclusion\x12J\n\x0e\x65xclusion_type\x18\x01 \x01(\x0e\x32\x32.gitlab.secret_detection.ScanRequest.ExclusionType\x12\r\n\x05value\x18\x02 \x01(\t\"f\n\rExclusionType\x12\x1e\n\x1a\x45XCLUSION_TYPE_UNSPECIFIED\x10\x00\x12\x17\n\x13\x45XCLUSION_TYPE_RULE\x10\x01\x12\x1c\n\x18\x45XCLUSION_TYPE_RAW_VALUE\x10\x02\x42\x0f\n\r_timeout_secsB\x17\n\x15_payload_timeout_secs\"\xe2\x03\n\x0cScanResponse\x12>\n\x07results\x18\x01 \x03(\x0b\x32-.gitlab.secret_detection.ScanResponse.Finding\x12\x0e\n\x06status\x18\x02 \x01(\x05\x1a\x9d\x01\n\x07\x46inding\x12\x12\n\npayload_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\x05\x12\x11\n\x04type\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0bline_number\x18\x05 \x01(\x05H\x02\x88\x01\x01\x42\x07\n\x05_typeB\x0e\n\x0c_descriptionB\x0e\n\x0c_line_number\"\xe1\x01\n\x06Status\x12\x16\n\x12STATUS_UNSPECIFIED\x10\x00\x12\x10\n\x0cSTATUS_FOUND\x10\x01\x12\x1c\n\x18STATUS_FOUND_WITH_ERRORS\x10\x02\x12\x17\n\x13STATUS_SCAN_TIMEOUT\x10\x03\x12\x1a\n\x16STATUS_PAYLOAD_TIMEOUT\x10\x04\x12\x15\n\x11STATUS_SCAN_ERROR\x10\x05\x12\x16\n\x12STATUS_INPUT_ERROR\x10\x06\x12\x14\n\x10STATUS_NOT_FOUND\x10\x07\x12\x15\n\x11STATUS_AUTH_ERROR\x10\x08\x32\xc1\x01\n\x07Scanner\x12U\n\x04Scan\x12$.gitlab.secret_detection.ScanRequest\x1a%.gitlab.secret_detection.ScanResponse\"\x00\x12_\n\nScanStream\x12$.gitlab.secret_detection.ScanRequest\x1a%.gitlab.secret_detection.ScanResponse\"\x00(\x01\x30\x01\x42 \xea\x02\x1dGitlab::SecretDetection::GRPCb\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Gitlab
+ module SecretDetection
+ module GRPC
+ # rubocop:disable Layout/LineLength -- generated file
+ ScanRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.secret_detection.ScanRequest").msgclass
+ ScanRequest::Payload = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.secret_detection.ScanRequest.Payload").msgclass
+ ScanRequest::Exclusion = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.secret_detection.ScanRequest.Exclusion").msgclass
+ ScanRequest::ExclusionType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.secret_detection.ScanRequest.ExclusionType").enummodule
+ ScanResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.secret_detection.ScanResponse").msgclass
+ ScanResponse::Finding = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.secret_detection.ScanResponse.Finding").msgclass
+ ScanResponse::Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.secret_detection.ScanResponse.Status").enummodule
+ # rubocop:enable Layout/LineLength
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/secret_detection_services_pb.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/secret_detection_services_pb.rb
new file mode 100644
index 00000000000..a4593a29a3c
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/secret_detection_services_pb.rb
@@ -0,0 +1,30 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# Source: secret_detection.proto for package 'Gitlab.SecretDetection.GRPC'
+
+require 'grpc'
+require_relative 'secret_detection_pb'
+
+module Gitlab
+ module SecretDetection
+ module GRPC
+ module Scanner
+ # Scanner service that scans given payloads and returns findings
+ class Service
+
+ include ::GRPC::GenericService
+
+ self.marshal_class_method = :encode
+ self.unmarshal_class_method = :decode
+ self.service_name = 'gitlab.secret_detection.Scanner'
+
+ # Runs secret detection scan for the given request
+ rpc :Scan, ::Gitlab::SecretDetection::GRPC::ScanRequest, ::Gitlab::SecretDetection::GRPC::ScanResponse
+ # Runs bi-directional streaming of scans for the given stream of requests with a stream of responses
+ rpc :ScanStream, stream(::Gitlab::SecretDetection::GRPC::ScanRequest), stream(::Gitlab::SecretDetection::GRPC::ScanResponse)
+ end
+
+ Stub = Service.rpc_stub_class
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/status.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/status.rb
index e4644a0cda7..ac6d0bbca97 100644
--- a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/status.rb
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/status.rb
@@ -4,13 +4,14 @@ module Gitlab
module SecretDetection
# All the possible statuses emitted by the scan operation
class Status
- NOT_FOUND = 0 # When scan operation completes with zero findings
FOUND = 1 # When scan operation completes with one or more findings
FOUND_WITH_ERRORS = 2 # When scan operation completes with one or more findings along with some errors
SCAN_TIMEOUT = 3 # When the scan operation runs beyond given time out
- PAYLOAD_TIMEOUT = 4 # When the scan operation on a diff runs beyond given time out
+ PAYLOAD_TIMEOUT = 4 # When the scan operation on a payload runs beyond given time out
SCAN_ERROR = 5 # When the scan operation fails due to regex error
INPUT_ERROR = 6 # When the scan operation fails due to invalid input
+ NOT_FOUND = 7 # When scan operation completes with zero findings
+ AUTH_ERROR = 8 # When authentication fails
end
end
end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils.rb
new file mode 100644
index 00000000000..5dc75707f98
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative 'utils/certificate'
+require_relative 'utils/memoize'
+
+module Gitlab
+ module SecretDetection
+ module Utils
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils/certificate.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils/certificate.rb
new file mode 100644
index 00000000000..65debcaf55d
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils/certificate.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'openssl'
+require_relative 'memoize'
+
+module Gitlab
+ module SecretDetection
+ module Utils
+ module X509
+ # Pulled from Gitlab.com source
+ # Link: https://gitlab.com/gitlab-org/gitlab/-/blob/4713a798f997389f04e442db3d1d8349a39d5d46/lib/gitlab/x509/certificate.rb
+ class Certificate
+ CERT_REGEX = /-----BEGIN CERTIFICATE-----(?:.|\n)+?-----END CERTIFICATE-----/
+
+ attr_reader :key, :cert, :ca_certs
+
+ def self.default_cert_dir
+ strong_memoize(:default_cert_dir) do
+ ENV.fetch('SSL_CERT_DIR', OpenSSL::X509::DEFAULT_CERT_DIR)
+ end
+ end
+
+ def self.default_cert_file
+ strong_memoize(:default_cert_file) do
+ ENV.fetch('SSL_CERT_FILE', OpenSSL::X509::DEFAULT_CERT_FILE)
+ end
+ end
+
+ def self.from_strings(key_string, cert_string, ca_certs_string = nil)
+ key = OpenSSL::PKey::RSA.new(key_string)
+ cert = OpenSSL::X509::Certificate.new(cert_string)
+ ca_certs = load_ca_certs_bundle(ca_certs_string)
+
+ new(key, cert, ca_certs)
+ end
+
+ def self.from_files(key_path, cert_path, ca_certs_path = nil)
+ ca_certs_string = File.read(ca_certs_path) if ca_certs_path
+
+ from_strings(File.read(key_path), File.read(cert_path), ca_certs_string)
+ end
+
+ # Returns all top-level, readable files in the default CA cert directory
+ def self.ca_certs_paths
+ cert_paths = Dir["#{default_cert_dir}/*"].select do |path|
+ !File.directory?(path) && File.readable?(path)
+ end
+ cert_paths << default_cert_file if File.exist? default_cert_file
+ cert_paths
+ end
+
+ # Returns a concatenated array of Strings, each being a PEM-coded CA certificate.
+ def self.ca_certs_bundle
+ strong_memoize(:ca_certs_bundle) do
+ ca_certs_paths.flat_map do |cert_file|
+ load_ca_certs_bundle(File.read(cert_file))
+ end.uniq.join("\n")
+ end
+ end
+
+ def self.reset_ca_certs_bundle
+ clear_memoization(:ca_certs_bundle)
+ end
+
+ def self.reset_default_cert_paths
+ clear_memoization(:default_cert_dir)
+ clear_memoization(:default_cert_file)
+ end
+
+ # Returns an array of OpenSSL::X509::Certificate objects, empty array if none found
+ #
+ # Ruby OpenSSL::X509::Certificate.new will only load the first
+ # certificate if a bundle is presented, this allows to parse multiple certs
+ # in the same file
+ def self.load_ca_certs_bundle(ca_certs_string)
+ return [] unless ca_certs_string
+
+ ca_certs_string.scan(CERT_REGEX).map do |ca_cert_string|
+ OpenSSL::X509::Certificate.new(ca_cert_string)
+ end
+ end
+
+ def initialize(key, cert, ca_certs = nil)
+ @key = key
+ @cert = cert
+ @ca_certs = ca_certs
+ end
+
+ def key_string
+ key.to_s
+ end
+
+ def cert_string
+ cert.to_pem
+ end
+
+ def ca_certs_string
+ ca_certs&.map(&:to_pem)&.join('\n') unless ca_certs.blank?
+ end
+
+ class << self
+ include ::Gitlab::SecretDetection::Utils::StrongMemoize
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils/memoize.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils/memoize.rb
new file mode 100644
index 00000000000..38a0277fc92
--- /dev/null
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils/memoize.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SecretDetection
+ module Utils
+ # Pulled from GitLab.com source
+ # Link: https://gitlab.com/gitlab-org/gitlab/-/blob/4713a798f997389f04e442db3d1d8349a39d5d46/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
+ module StrongMemoize
+ # Instead of writing patterns like this:
+ #
+ # def trigger_from_token
+ # return @trigger if defined?(@trigger)
+ #
+ # @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ # end
+ #
+ # We could write it like:
+ #
+ # include Gitlab::SecretDetection::Utils::StrongMemoize
+ #
+ # def trigger_from_token
+ # Ci::Trigger.find_by_token(params[:token].to_s)
+ # end
+ # strong_memoize_attr :trigger_from_token
+ #
+ # def enabled?
+ # Feature.enabled?(:some_feature)
+ # end
+ # strong_memoize_attr :enabled?
+ #
+ def strong_memoize(name)
+ key = ivar(name)
+
+ if instance_variable_defined?(key)
+ instance_variable_get(key)
+ else
+ instance_variable_set(key, yield)
+ end
+ end
+
+ # Works the same way as "strong_memoize" but takes
+ # a second argument - expire_in. This allows invalidate
+ # the data after specified number of seconds
+ def strong_memoize_with_expiration(name, expire_in)
+ key = ivar(name)
+ expiration_key = "#{key}_expired_at"
+
+ if instance_variable_defined?(expiration_key)
+ expire_at = instance_variable_get(expiration_key)
+ clear_memoization(name) if expire_at.past?
+ end
+
+ if instance_variable_defined?(key)
+ instance_variable_get(key)
+ else
+ value = instance_variable_set(key, yield)
+ instance_variable_set(expiration_key, Time.current + expire_in)
+ value
+ end
+ end
+
+ def strong_memoize_with(name, *args)
+ container = strong_memoize(name) { {} }
+
+ if container.key?(args)
+ container[args]
+ else
+ container[args] = yield
+ end
+ end
+
+ def strong_memoized?(name)
+ key = ivar(StrongMemoize.normalize_key(name))
+ instance_variable_defined?(key)
+ end
+
+ def clear_memoization(name)
+ key = ivar(StrongMemoize.normalize_key(name))
+ remove_instance_variable(key) if instance_variable_defined?(key)
+ end
+
+ module StrongMemoizeClassMethods
+ def strong_memoize_attr(method_name)
+ member_name = StrongMemoize.normalize_key(method_name)
+
+ StrongMemoize.send(:do_strong_memoize, self, method_name, member_name) # rubocop:disable GitlabSecurity/PublicSend -- Same reason as Gitlab;:Utils::StrongMemoize
+ end
+ end
+
+ def self.included(base)
+ base.singleton_class.prepend(StrongMemoizeClassMethods)
+ end
+
+ private
+
+ # Convert `"name"`/`:name` into `:@name`
+ #
+ # Depending on a type ensure that there's a single memory allocation
+ def ivar(name)
+ case name
+ when Symbol
+ name.to_s.prepend("@").to_sym
+ when String
+ :"@#{name}"
+ else
+ raise ArgumentError, "Invalid type of '#{name}'"
+ end
+ end
+
+ class << self
+ def normalize_key(key)
+ return key unless key.end_with?('!', '?')
+
+ # Replace invalid chars like `!` and `?` with allowed Unicode codeparts.
+ key.to_s.tr('!?', "\uFF01\uFF1F")
+ end
+
+ private
+
+ def do_strong_memoize(klass, method_name, member_name)
+ method = klass.instance_method(method_name)
+
+ unless method.arity.zero?
+ raise <<~ERROR
+ Using `strong_memoize_attr` on methods with parameters is not supported.
+
+ Use `strong_memoize_with` instead.
+ See https://docs.gitlab.com/ee/development/utilities.html#strongmemoize
+ ERROR
+ end
+
+ # Methods defined within a class method are already public by default, so we don't need to
+ # explicitly make them public.
+ scope = %i[private protected].find do |scope|
+ klass.send(:"#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend -- For the same reason as Gitlab::Utils::StrongMemoise
+ .include? method_name
+ end
+
+ klass.define_method(method_name) do |&block|
+ strong_memoize(member_name) do
+ method.bind_call(self, &block)
+ end
+ end
+
+ klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend -- For the same reason as Gitlab::Utils::StrongMemoise
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/version.rb b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/version.rb
index 8fc73a02121..befeff12ef7 100644
--- a/gems/gitlab-secret_detection/lib/gitlab/secret_detection/version.rb
+++ b/gems/gitlab-secret_detection/lib/gitlab/secret_detection/version.rb
@@ -2,6 +2,6 @@
module Gitlab
module SecretDetection
- VERSION = "0.1.0"
+ VERSION = "0.1.1"
end
end
diff --git a/gems/gitlab-secret_detection/spec/lib/gitlab/secret_detection/grpc/client_spec.rb b/gems/gitlab-secret_detection/spec/lib/gitlab/secret_detection/grpc/client_spec.rb
new file mode 100644
index 00000000000..797ae2b759d
--- /dev/null
+++ b/gems/gitlab-secret_detection/spec/lib/gitlab/secret_detection/grpc/client_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'time'
+require_relative '../../../../spec_helper'
+
+SDGRPC = Gitlab::SecretDetection::GRPC
+GRPCStatus = SDGRPC::ScanResponse::Status
+SD = Gitlab::SecretDetection
+
+RSpec.describe Gitlab::SecretDetection::GRPC::Client do
+ subject(:client) { described_class.new(host, secure:) }
+
+ let(:secure) { true }
+ let(:host) { 'example.com:443' }
+ let(:auth_token) { '12345' }
+ let(:stub) { instance_double(SDGRPC::Scanner::Stub) }
+
+ let(:payloads) { [SDGRPC::ScanRequest::Payload.new(id: '1', data: 'dummy')] }
+ let(:request) { SDGRPC::ScanRequest.new(payloads:) }
+ let(:requests) { [request, request] }
+
+ let(:stub_scan_response) { SDGRPC::ScanResponse.new(status: GRPCStatus::STATUS_NOT_FOUND) }
+ let(:stub_scan_stream_response) do
+ [
+ SDGRPC::ScanResponse.new(
+ status: GRPCStatus::STATUS_NOT_FOUND,
+ results: []
+ ),
+ SDGRPC::ScanResponse.new(
+ status: GRPCStatus::STATUS_NOT_FOUND,
+ results: []
+ )
+ ].to_enum
+ end
+
+ let(:metadata) { { "x-sd-auth" => auth_token } }
+
+ before do
+ allow(SDGRPC::Scanner::Stub).to receive(:new).and_return(stub)
+ allow(stub).to receive_messages(scan: stub_scan_response, scan_stream: stub_scan_stream_response)
+ end
+
+ describe "#run_scan" do
+ it "sends correct metadata and deadline" do
+ before_test_time = Time.now
+ client.run_scan(request:, auth_token:)
+
+ expect(stub).to have_received(:scan).with(
+ request,
+ deadline: satisfy do |deadline|
+ diff = (deadline - before_test_time)
+ (diff - described_class::REQUEST_TIMEOUT_SECONDS) < 1 # considering buffer of 1 sec
+ end,
+ metadata: { 'x-sd-auth' => '12345' }
+ )
+ end
+
+ it "transforms SD service response to SD response" do
+ result = client.run_scan(request:, auth_token:)
+
+ expect(result).to be_instance_of(SD::Response)
+ expect(result.status).to eq(stub_scan_response.status)
+ expect(result.results).to eq(stub_scan_response.results)
+ end
+
+ context "when an error occurs in the service" do
+ it "returns SD response instead of raising error" do
+ allow(stub).to receive(:scan).and_raise(GRPC::Unauthenticated)
+ result = nil
+ expect { result = client.run_scan(request:, auth_token:) }.not_to raise_error
+ expect(result).to be_instance_of(SD::Response)
+ end
+
+ it "returns SD response with corresponding Status" do
+ [
+ [GRPC::InvalidArgument, SD::Status::INPUT_ERROR],
+ [GRPC::Unauthenticated, SD::Status::AUTH_ERROR],
+ [GRPC::Unknown, SD::Status::SCAN_ERROR],
+ [GRPC::BadStatus, SD::Status::SCAN_ERROR]
+ ].each do |grpc_error, sd_core_status|
+ allow(stub).to receive(:scan).and_raise(grpc_error, "")
+
+ result = nil
+ expect { result = client.run_scan(request:, auth_token:) }.not_to raise_error
+
+ expect(result).to be_instance_of(SD::Response)
+ expect(result&.status).to eq(sd_core_status)
+ end
+ end
+ end
+ end
+
+ describe "#run_scan_stream" do
+ it "sends correct metadata and deadline" do
+ before_test_time = Time.now
+ client.run_scan_stream(requests:, auth_token:)
+
+ expect(stub).to have_received(:scan_stream).with(
+ requests,
+ deadline: satisfy do |deadline|
+ diff = (deadline - before_test_time)
+ (diff - described_class::REQUEST_TIMEOUT_SECONDS) < 1 # considering buffer of 1 sec
+ end,
+ metadata: { 'x-sd-auth' => '12345' }
+ )
+ end
+
+ it "transforms each streamed response to SD response" do
+ result = client.run_scan_stream(requests:, auth_token:)
+
+ expect(result).to be_instance_of(Array)
+ expect(result.length).to eq(requests.length)
+ result.each do |msg|
+ expect(msg).to be_instance_of(SD::Response)
+ expect(msg.status).to eq(SD::Status::NOT_FOUND)
+ end
+ end
+ end
+end
diff --git a/lib/api/admin/token.rb b/lib/api/admin/token.rb
index 9a54c95526f..2100d2104c7 100644
--- a/lib/api/admin/token.rb
+++ b/lib/api/admin/token.rb
@@ -11,7 +11,7 @@ module API
token = ::Authn::AgnosticTokenIdentifier.token_for(plaintext, AUDIT_SOURCE)
raise ArgumentError, 'Token type not supported.' if token.blank?
- token.revocable
+ token
end
end
@@ -45,12 +45,11 @@ module API
end
post 'token' do
identified_token = identify_token(params[:token])
-
- render_api_error!({ error: 'Not found' }, :not_found) if identified_token.nil?
+ render_api_error!({ error: 'Not found' }, :not_found) if identified_token.revocable.nil?
status :ok
- present identified_token, with: "API::Entities::#{identified_token.class.name}".constantize
+ present identified_token.revocable, with: identified_token.present_with
end
end
end
diff --git a/lib/authn/agnostic_token_identifier.rb b/lib/authn/agnostic_token_identifier.rb
index 672f03d6eba..7f04f75e1bc 100644
--- a/lib/authn/agnostic_token_identifier.rb
+++ b/lib/authn/agnostic_token_identifier.rb
@@ -3,10 +3,12 @@
module Authn
class AgnosticTokenIdentifier
NotFoundError = Class.new(StandardError)
+ UnsupportedTokenError = Class.new(StandardError)
TOKEN_TYPES = [
::Authn::Tokens::DeployToken,
::Authn::Tokens::FeedToken,
- ::Authn::Tokens::PersonalAccessToken
+ ::Authn::Tokens::PersonalAccessToken,
+ ::Authn::Tokens::OauthApplicationSecret
].freeze
def self.token_for(plaintext, source)
diff --git a/lib/authn/tokens/deploy_token.rb b/lib/authn/tokens/deploy_token.rb
index 43b694adf17..7ebaf1d98d4 100644
--- a/lib/authn/tokens/deploy_token.rb
+++ b/lib/authn/tokens/deploy_token.rb
@@ -14,6 +14,10 @@ module Authn
@source = source
end
+ def present_with
+ ::API::Entities::DeployToken
+ end
+
def revoke!(current_user)
raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank?
diff --git a/lib/authn/tokens/feed_token.rb b/lib/authn/tokens/feed_token.rb
index 1b425c76403..b46c35632c9 100644
--- a/lib/authn/tokens/feed_token.rb
+++ b/lib/authn/tokens/feed_token.rb
@@ -14,6 +14,10 @@ module Authn
@source = source
end
+ def present_with
+ ::API::Entities::User
+ end
+
def revoke!(current_user)
raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank?
diff --git a/lib/authn/tokens/oauth_application_secret.rb b/lib/authn/tokens/oauth_application_secret.rb
new file mode 100644
index 00000000000..84432afeaf7
--- /dev/null
+++ b/lib/authn/tokens/oauth_application_secret.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal:true
+
+module Authn
+ module Tokens
+ class OauthApplicationSecret
+ def self.prefix?(plaintext)
+ prefix =
+ ::Gitlab::DoorkeeperSecretStoring::Token::UniqueApplicationToken::OAUTH_APPLICATION_SECRET_PREFIX_FORMAT
+ .split('-').first
+
+ plaintext.start_with?(prefix)
+ end
+
+ attr_reader :revocable, :source
+
+ def initialize(plaintext, source)
+ @revocable = ::Doorkeeper::Application.find_by_plaintext_token(:secret, plaintext)
+ @source = source
+ end
+
+ def present_with
+ ::API::Entities::Application
+ end
+
+ def revoke!(_current_user)
+ raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank?
+
+ raise ::Authn::AgnosticTokenIdentifier::UnsupportedTokenError, 'Revocation not supported for this token type'
+ end
+ end
+ end
+end
diff --git a/lib/authn/tokens/personal_access_token.rb b/lib/authn/tokens/personal_access_token.rb
index 669debaf2b4..67ef8681d7d 100644
--- a/lib/authn/tokens/personal_access_token.rb
+++ b/lib/authn/tokens/personal_access_token.rb
@@ -17,6 +17,10 @@ module Authn
@source = source
end
+ def present_with
+ ::API::Entities::PersonalAccessToken
+ end
+
def revoke!(current_user)
raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank?
diff --git a/spec/lib/authn/agnostic_token_identifier_spec.rb b/spec/lib/authn/agnostic_token_identifier_spec.rb
index b3d96cb6a46..386b91894c2 100644
--- a/spec/lib/authn/agnostic_token_identifier_spec.rb
+++ b/spec/lib/authn/agnostic_token_identifier_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Authn::AgnosticTokenIdentifier, feature_category: :system_access
let_it_be(:deploy_token) { create(:deploy_token).token }
let_it_be(:feed_token) { user.feed_token }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user).token }
+ let_it_be(:oauth_application_secret) { create(:oauth_application).plaintext_secret }
subject(:token) { described_class.token_for(plaintext, :group_token_revocation_service) }
@@ -17,6 +18,7 @@ RSpec.describe Authn::AgnosticTokenIdentifier, feature_category: :system_access
ref(:personal_access_token) | ::Authn::Tokens::PersonalAccessToken
ref(:feed_token) | ::Authn::Tokens::FeedToken
ref(:deploy_token) | ::Authn::Tokens::DeployToken
+ ref(:oauth_application_secret) | ::Authn::Tokens::OauthApplicationSecret
'unsupported' | NilClass
end
diff --git a/spec/lib/authn/tokens/oauth_application_secret_spec.rb b/spec/lib/authn/tokens/oauth_application_secret_spec.rb
new file mode 100644
index 00000000000..b0857f27088
--- /dev/null
+++ b/spec/lib/authn/tokens/oauth_application_secret_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Authn::Tokens::OauthApplicationSecret, feature_category: :system_access do
+ let_it_be(:user) { create(:user) }
+
+ let(:oauth_application_secret) { create(:oauth_application) }
+
+ subject(:token) { described_class.new(plaintext, :api_admin_token) }
+
+ context 'with valid oauth application secret' do
+ let(:plaintext) { oauth_application_secret.plaintext_secret }
+ let(:valid_revocable) { oauth_application_secret }
+
+ it_behaves_like 'finding the valid revocable'
+
+ describe '#revoke!' do
+ it 'does not support revocation yet' do
+ expect do
+ token.revoke!(user)
+ end.to raise_error(::Authn::AgnosticTokenIdentifier::UnsupportedTokenError,
+ 'Revocation not supported for this token type')
+ end
+ end
+ end
+
+ it_behaves_like 'token handling with unsupported token type'
+end
diff --git a/spec/requests/api/admin/token_spec.rb b/spec/requests/api/admin/token_spec.rb
index 5cc09953497..2f411f6aab1 100644
--- a/spec/requests/api/admin/token_spec.rb
+++ b/spec/requests/api/admin/token_spec.rb
@@ -10,6 +10,8 @@ RSpec.describe API::Admin::Token, :aggregate_failures, feature_category: :system
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:deploy_token) { create(:deploy_token) }
+ let_it_be(:oauth_application) { create(:oauth_application) }
+
let(:plaintext) { nil }
let(:params) { { token: plaintext } }
@@ -22,7 +24,8 @@ RSpec.describe API::Admin::Token, :aggregate_failures, feature_category: :system
[
[ref(:personal_access_token), lazy { personal_access_token.token }],
[ref(:deploy_token), lazy { deploy_token.token }],
- [ref(:user), lazy { user.feed_token }]
+ [ref(:user), lazy { user.feed_token }],
+ [ref(:oauth_application), lazy { oauth_application.plaintext_secret }]
]
end
diff --git a/spec/services/merge_requests/mergeability/logger_spec.rb b/spec/services/merge_requests/mergeability/logger_spec.rb
index 501532e762a..0212d26fce2 100644
--- a/spec/services/merge_requests/mergeability/logger_spec.rb
+++ b/spec/services/merge_requests/mergeability/logger_spec.rb
@@ -71,6 +71,26 @@ RSpec.describe MergeRequests::Mergeability::Logger, :request_store, feature_cate
end
end
+ context 'when block value responds to #status' do
+ let(:check_result) { instance_double(Gitlab::MergeRequests::Mergeability::CheckResult, status: :inactive) }
+
+ let(:extra_data) do
+ {
+ 'mergeability.expensive_operation.status.values' => ['inactive']
+ }
+ end
+
+ it 'records operation status value' do
+ expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
+ expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data(**extra_data))))
+ end
+
+ expect(logger.instrument(mergeability_name: :expensive_operation) { check_result }).to eq(check_result)
+
+ logger.commit
+ end
+ end
+
context 'with multiple observations' do
let(:operation_count) { 2 }
diff --git a/spec/support/shared_examples/authn/token_shared_examples.rb b/spec/support/shared_examples/authn/token_shared_examples.rb
index a0ff50401de..12c9741151f 100644
--- a/spec/support/shared_examples/authn/token_shared_examples.rb
+++ b/spec/support/shared_examples/authn/token_shared_examples.rb
@@ -27,4 +27,10 @@ RSpec.shared_examples 'finding the valid revocable' do
expect(token.revocable).to eq(valid_revocable)
end
end
+
+ describe '#present_with' do
+ it 'returns a constant that is a subclass of Grape::Entity' do
+ expect(token.present_with).to be <= Grape::Entity
+ end
+ end
end