From 1b8305975ebffaaa5cb454c07414aa6a6c4e6cb4 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 23 Nov 2024 00:14:56 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo/gitlab/strong_memoize_attr.yml | 2 - Gemfile.checksum | 1 + Gemfile.lock | 5 +- Gemfile.next.checksum | 1 + Gemfile.next.lock | 5 +- .../javascripts/emoji/components/picker.vue | 4 +- .../merge_requests/mergeability/logger.rb | 10 +- .../mergeability/run_checks_service.rb | 16 +- doc/api/admin/token.md | 2 + doc/ci/pipelines/merge_trains.md | 4 + doc/development/event_store.md | 9 ++ doc/integration/oauth_provider.md | 5 +- gems/gitlab-secret_detection/.rubocop.yml | 2 + gems/gitlab-secret_detection/Gemfile.lock | 13 +- .../gitlab-secret_detection.gemspec | 2 + .../lib/gitlab/secret_detection.rb | 2 + .../lib/gitlab/secret_detection/grpc.rb | 13 ++ .../grpc/client/grpc_client.rb | 142 ++++++++++++++++ .../grpc/client/stream_request_enumerator.rb | 24 +++ .../secret_detection/grpc/generated/.gitkeep | 0 .../grpc/generated/secret_detection_pb.rb | 28 ++++ .../generated/secret_detection_services_pb.rb | 30 ++++ .../lib/gitlab/secret_detection/status.rb | 5 +- .../lib/gitlab/secret_detection/utils.rb | 11 ++ .../secret_detection/utils/certificate.rb | 108 +++++++++++++ .../gitlab/secret_detection/utils/memoize.rb | 151 ++++++++++++++++++ .../lib/gitlab/secret_detection/version.rb | 2 +- .../secret_detection/grpc/client_spec.rb | 119 ++++++++++++++ lib/api/admin/token.rb | 7 +- lib/authn/agnostic_token_identifier.rb | 4 +- lib/authn/tokens/deploy_token.rb | 4 + lib/authn/tokens/feed_token.rb | 4 + lib/authn/tokens/oauth_application_secret.rb | 32 ++++ lib/authn/tokens/personal_access_token.rb | 4 + .../authn/agnostic_token_identifier_spec.rb | 2 + .../tokens/oauth_application_secret_spec.rb | 29 ++++ spec/requests/api/admin/token_spec.rb | 5 +- .../mergeability/logger_spec.rb | 20 +++ .../authn/token_shared_examples.rb | 6 + 39 files changed, 798 insertions(+), 35 deletions(-) create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc.rb create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/client/grpc_client.rb create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/client/stream_request_enumerator.rb create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/.gitkeep create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/secret_detection_pb.rb create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/grpc/generated/secret_detection_services_pb.rb create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils.rb create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils/certificate.rb create mode 100644 gems/gitlab-secret_detection/lib/gitlab/secret_detection/utils/memoize.rb create mode 100644 gems/gitlab-secret_detection/spec/lib/gitlab/secret_detection/grpc/client_spec.rb create mode 100644 lib/authn/tokens/oauth_application_secret.rb create mode 100644 spec/lib/authn/tokens/oauth_application_secret_spec.rb 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