Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
815e559b62
commit
1b8305975e
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ inherit_from:
|
|||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
- lib/gitlab/secret_detection/grpc/generated/*.rb
|
||||
|
||||
Style/HashSyntax:
|
||||
EnforcedShorthandSyntax: consistent
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
module Gitlab
|
||||
module SecretDetection
|
||||
VERSION = "0.1.0"
|
||||
VERSION = "0.1.1"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue