Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-11-23 00:14:56 +00:00
parent 815e559b62
commit 1b8305975e
39 changed files with 798 additions and 35 deletions

View File

@ -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'

View File

@ -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"},

View File

@ -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)

View File

@ -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"},

View File

@ -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)

View File

@ -182,14 +182,14 @@ export default {
<div
v-if="isVisible"
v-show="!searchValue"
class="award-list gl-flex gl-border-b-1 gl-border-gray-100 gl-border-b-solid"
class="gl-flex gl-border-b-1 gl-border-gray-100 gl-border-b-solid"
>
<gl-button
v-for="(category, index) in categoryNames"
:key="category.name"
category="tertiary"
:class="{ 'emoji-picker-category-active': index === currentCategory }"
class="emoji-picker-category-tab gl-grow !gl-rounded-none !gl-border-b-2 !gl-px-3 !gl-border-b-solid"
class="emoji-picker-category-tab gl-grow !gl-rounded-none !gl-border-b-2 !gl-px-3 !gl-border-b-solid focus:!gl-shadow-none focus:!gl-outline focus:!gl-outline-2 focus:-gl-outline-offset-2 focus:!gl-outline-focus"
:icon="category.icon"
:aria-label="category.name"
@click="scrollToCategory(category.name)"

View File

@ -34,9 +34,8 @@ module MergeRequests
attr_reader :destination, :merge_request, :stored_result
def observe_result(name, result)
return unless result.respond_to?(:success?)
observe("mergeability.#{name}.successful", result.success?)
observe("mergeability.#{name}.successful", result.success?) if result.respond_to?(:success?)
observe("mergeability.#{name}.status", result.status.to_s) if result.respond_to?(:status)
end
def observe(name, value)
@ -69,10 +68,9 @@ module MergeRequests
end
def observations
strong_memoize(:observations) do
Hash.new { |hash, key| hash[key] = [] }
end
Hash.new { |hash, key| hash[key] = [] }
end
strong_memoize_attr :observations
def observe_sql_counters(name, start_db_counters, end_db_counters)
end_db_counters.each do |key, value|

View File

@ -26,7 +26,7 @@ module MergeRequests
logger.commit
return ServiceResponse.success(payload: { results: results }) if all_results_success?
return ServiceResponse.success(payload: { results: results }) if no_result_unsuccessful?
ServiceResponse.error(
message: 'Checks were not successful',
@ -53,18 +53,18 @@ module MergeRequests
end
def cached_results
strong_memoize(:cached_results) do
Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request)
end
Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request)
end
strong_memoize_attr :cached_results
def logger
strong_memoize(:logger) do
MergeRequests::Mergeability::Logger.new(merge_request: merge_request)
end
MergeRequests::Mergeability::Logger.new(merge_request: merge_request)
end
strong_memoize_attr :logger
def all_results_success?
# This name may seem like a double-negative, but it is meaningful because
# #success? is _not_ the inverse of #unsuccessful?
def no_result_unsuccessful?
results.none?(&:unsuccessful?)
end

View File

@ -14,6 +14,7 @@ DETAILS:
> - [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

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -3,6 +3,8 @@ inherit_from:
AllCops:
NewCops: enable
Exclude:
- lib/gitlab/secret_detection/grpc/generated/*.rb
Style/HashSyntax:
EnforcedShorthandSyntax: consistent

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -2,6 +2,6 @@
module Gitlab
module SecretDetection
VERSION = "0.1.0"
VERSION = "0.1.1"
end
end

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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?

View File

@ -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?

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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