Skip to content
代码片段 群组 项目
未验证 提交 1eb9eb93 编辑于 作者: Drew Blessing's avatar Drew Blessing 提交者: GitLab
浏览文件

Merge branch '390874-add-tracking-to-deploy-token' into 'master'

Add user tracking for deploy tokens from registry events

See merge request https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966



Merged-by: default avatarDrew Blessing <drew@gitlab.com>
Approved-by: default avatarRadamanthus Batnag <rbatnag@gitlab.com>
Approved-by: default avatarDavid Fernandez <dfernandez@gitlab.com>
Approved-by: default avatarDrew Blessing <drew@gitlab.com>
Reviewed-by: default avatarMichał Wielich <mwielich@gitlab.com>
Reviewed-by: default avatarDavid Fernandez <dfernandez@gitlab.com>
Reviewed-by: default avatarRadamanthus Batnag <rbatnag@gitlab.com>
Co-authored-by: default avatarAdie Po <avpfestin@gitlab.com>
No related branches found
No related tags found
无相关合并请求
显示
517 个添加56 个删除
...@@ -14,6 +14,7 @@ class Event ...@@ -14,6 +14,7 @@ class Event
personal_access_token personal_access_token
build build
gitlab_or_ldap gitlab_or_ldap
deploy_token
].freeze ].freeze
TRACKABLE_ACTOR_EVENTS = %w[ TRACKABLE_ACTOR_EVENTS = %w[
...@@ -39,7 +40,6 @@ def handle! ...@@ -39,7 +40,6 @@ def handle!
end end
def track! def track!
tracked_target = target_tag? ? :tag : :repository
tracking_action = "#{action}_#{tracked_target}" tracking_action = "#{action}_#{tracked_target}"
if target_repository? && action_push? && !container_repository_exists? if target_repository? && action_push? && !container_repository_exists?
...@@ -58,6 +58,10 @@ def track! ...@@ -58,6 +58,10 @@ def track!
private private
def tracked_target
target_tag? ? :tag : :repository
end
def target_tag? def target_tag?
# There is no clear indication in the event structure when we delete a top-level manifest # There is no clear indication in the event structure when we delete a top-level manifest
# except existence of "tag" key # except existence of "tag" key
...@@ -108,22 +112,51 @@ def usage_data_event_for(tracking_action) ...@@ -108,22 +112,51 @@ def usage_data_event_for(tracking_action)
return unless originator return unless originator
return unless TRACKABLE_ACTOR_EVENTS.include?(tracking_action) return unless TRACKABLE_ACTOR_EVENTS.include?(tracking_action)
"#{EVENT_PREFIX}_#{tracking_action}_user" "#{EVENT_PREFIX}_#{tracking_action}_#{originator_suffix}"
end
def originator
return unless ALLOWED_ACTOR_TYPES.include?(originator_type)
origin_id = get_origin_id(originator_type)
return unless origin_id
origin_class.find(origin_id)
end
strong_memoize_attr :originator
def originator_suffix
originator.is_a?(DeployToken) ? 'deploy_token' : 'user'
end end
def originator_type def originator_type
event.dig('actor', 'user_type') event.dig('actor', 'user_type')
end end
def originator def origin_class
return unless ALLOWED_ACTOR_TYPES.include?(originator_type) deploy_token?(originator_type) ? DeployToken : User
end
username = event.dig('actor', 'name') def get_origin_id(originator_type)
return unless username encoded_user_jwt = event.dig('actor', 'user')
return unless encoded_user_jwt
strong_memoize(:originator) do key = deploy_token?(originator_type) ? 'deploy_token_id' : 'user_id'
User.find_by_username(username) user_info = decode_user_info(encoded_user_jwt)
end user_info&.dig('user_info', key)
end
def decode_user_info(encoded_user_jwt)
registry_key_file = File.read(Gitlab.config.registry.key)
registry_key = OpenSSL::PKey::RSA.new(registry_key_file)
JSONWebToken::RSAToken.decode(encoded_user_jwt, registry_key).first
rescue Errno::ENOENT, JWT::VerificationError, JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
nil
end
def deploy_token?(originator_type)
originator_type == 'deploy_token'
end end
def manifest_delete_event? def manifest_delete_event?
......
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_push_tag_deploy_token_monthly
description: A monthly count of deploy tokens that have pushed a tag to the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_push_tag_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_delete_tag_deploy_token_monthly
description: A monthly count of deploy tokens that have deleted a tag from the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_delete_tag_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_push_repository_deploy_token_monthly
description: A monthly count of deploy tokens that have pushed a repository to the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_push_repository_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_delete_repository_deploy_token_monthly
description: A monthly count of deploy tokens that have deleted a repository from the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_delete_repository_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_create_repository_deploy_token_monthly
description: A monthly count of deploy tokens that have created a repository from the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_create_repository_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_push_repository_deploy_token_weekly
description: A weekly count of deploy tokens that have pushed a repository to the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_push_repository_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_push_tag_deploy_token_weekly
description: A weekly count of deploy tokens that have pushed a tag to the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_push_tag_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_delete_tag_deploy_token_weekly
description: A weekly count of deploy tokens that have deleted a tag from the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_delete_tag_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_delete_repository_deploy_token_weekly
description: A weekly count of deploy tokens that have deleted a repository from the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_delete_repository_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.user_container_registry.i_container_registry_create_repository_deploy_token_weekly
description: A weekly count of deploy tokens that have created a repository from the registry
product_section: ops
product_stage: package
product_group: container_registry
value_type: number
status: active
milestone: "16.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131966
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_container_registry_create_repository_deploy_token
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module JSONWebToken module JSONWebToken
class RSAToken < Token class RSAToken < Token
ALGORITHM = 'RS256'
attr_reader :key_file attr_reader :key_file
def initialize(key_file) def initialize(key_file)
...@@ -14,7 +16,11 @@ def encoded ...@@ -14,7 +16,11 @@ def encoded
kid: kid, kid: kid,
typ: 'JWT' typ: 'JWT'
} }
JWT.encode(payload, key, 'RS256', headers) JWT.encode(payload, key, ALGORITHM, headers)
end
def self.decode(token, key)
JWT.decode(token, key, true, { algorithm: ALGORITHM })
end end
private private
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe JSONWebToken::RSAToken do RSpec.describe JSONWebToken::RSAToken do
let(:rsa_key) do let_it_be(:rsa_key) do
OpenSSL::PKey::RSA.new <<-eos.strip_heredoc OpenSSL::PKey::RSA.new <<-eos.strip_heredoc
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAMA5sXIBE0HwgIB40iNidN4PGWzOyLQK0bsdOBNgpEXkDlZBvnak MIIBOgIBAAJBAMA5sXIBE0HwgIB40iNidN4PGWzOyLQK0bsdOBNgpEXkDlZBvnak
...@@ -49,4 +49,52 @@ ...@@ -49,4 +49,52 @@
it { expect { subject }.to raise_error(JWT::DecodeError) } it { expect { subject }.to raise_error(JWT::DecodeError) }
end end
end end
describe '.decode' do
let(:decoded_token) { described_class.decode(rsa_encoded, rsa_key) }
context 'with an invalid token' do
context 'that is junk' do
let(:rsa_encoded) { 'junk' }
it "raises exception saying 'Not enough or too many segments'" do
expect { decoded_token }.to raise_error(JWT::DecodeError, 'Not enough or too many segments')
end
end
context 'that has been fiddled with' do
let(:rsa_encoded) { rsa_token.encoded.tap { |token| token[0] = 'E' } }
it "raises exception saying 'Invalid segment encoding'" do
expect { decoded_token }.to raise_error(JWT::DecodeError, 'Invalid segment encoding')
end
end
context 'that was generated using a different key' do
let_it_be(:rsa_key_2) { OpenSSL::PKey::RSA.new 2048 }
before do
# rsa_key is returned for encoding, and rsa_key_2 for decoding
allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key, rsa_key_2)
end
it "raises exception saying 'Signature verification failed" do
expect { decoded_token }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end
context 'that is expired' do
# Needs the ! so freeze_time() is effective
let!(:rsa_encoded) { rsa_token.encoded }
it "raises exception saying 'Signature has expired'" do
# Needs to be 120 seconds, because the default expiry is 60 seconds
# with an additional 60 second leeway.
travel_to(Time.current + 120) do
expect { decoded_token }.to raise_error(JWT::ExpiredSignature, 'Signature has expired')
end
end
end
end
end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ContainerRegistry::Event do RSpec.describe ContainerRegistry::Event, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group, name: 'group') } let_it_be(:group) { create(:group, name: 'group') }
...@@ -112,19 +112,22 @@ ...@@ -112,19 +112,22 @@
describe '#track!' do describe '#track!' do
let_it_be(:container_repository) { create(:container_repository, name: 'container', project: project) } let_it_be(:container_repository) { create(:container_repository, name: 'container', project: project) }
let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(3072) }
let(:raw_event) { { 'action' => action, 'target' => target } } let(:raw_event) { { 'action' => action, 'target' => target } }
let(:key_file) { Tempfile.new('keypath') }
subject { described_class.new(raw_event).track! } before do
allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: key_file.path)
shared_examples 'tracking event is sent to HLLRedisCounter with event and originator ID' do |originator_type| allow(OpenSSL::PKey::RSA).to receive(:new).and_return(rsa_key)
it 'fetches the event originator based on username' do
count.times do
expect(User).to receive(:find_by_username).with(originator.username)
end
subject allow_next_instance_of(JSONWebToken::RSAToken) do |instance|
allow(instance).to receive(:key).and_return(rsa_key)
end end
end
subject { described_class.new(raw_event).track! }
shared_examples 'tracking event is sent to HLLRedisCounter with event and originator ID' do |originator_type|
it 'sends a tracking event to HLLRedisCounter' do it 'sends a tracking event to HLLRedisCounter' do
expect(::Gitlab::UsageDataCounters::HLLRedisCounter) expect(::Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event).with("i_container_registry_#{event}_#{originator_type}", values: originator.id) .to receive(:track_event).with("i_container_registry_#{event}_#{originator_type}", values: originator.id)
...@@ -134,6 +137,16 @@ ...@@ -134,6 +137,16 @@
end end
end end
shared_examples 'event originator is fetched based on ID' do |originator_class|
it 'fetches the event originator based on id' do
count.times do
expect(originator_class).to receive(:find).with(originator.id)
end
subject
end
end
context 'with a respository target' do context 'with a respository target' do
let(:target) do let(:target) do
{ {
...@@ -185,43 +198,165 @@ ...@@ -185,43 +198,165 @@
context 'with a deploy token as the actor' do context 'with a deploy token as the actor' do
let!(:originator) { create(:deploy_token, username: 'username', id: 3) } let!(:originator) { create(:deploy_token, username: 'username', id: 3) }
let(:raw_event) do
{ shared_examples 'no tracking of a deploy token is sent to HLLRedisCounter' do
'action' => 'push', it 'does not send a tracking event to HLLRedisCounter' do
'target' => { 'tag' => 'latest' }, expect(DeployToken).not_to receive(:find)
'actor' => { 'user_type' => 'deploy_token', 'name' => originator.username } expect(DeployToken).not_to receive(:find_by_username)
} expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
expect { subject }.not_to raise_error
end
end end
it 'does not send a tracking event to HLLRedisCounter' do context 'when only username is provided and no deploy_token_id is given' do
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) let(:raw_event) do
{
'action' => 'push',
'target' => { 'tag' => 'latest' },
'actor' => { 'user_type' => 'deploy_token', 'name' => originator.username }
}
end
subject it_behaves_like 'no tracking of a deploy token is sent to HLLRedisCounter'
end
context 'when no username or deploy_token_id is given' do
let(:raw_event) { { 'action' => 'push', 'target' => {}, 'actor' => { 'user_type' => 'deploy_token' } } }
it_behaves_like 'no tracking of a deploy token is sent to HLLRedisCounter'
end
context 'when deploy_token_id is given' do
let(:deploy_token_info) do
{
token_type: 'deploy_token',
username: originator.username,
deploy_token_id: originator.id
}
end
let(:token) do
JSONWebToken::RSAToken.new(rsa_key).tap do |token|
token[:user_info] = deploy_token_info
end
end
let(:raw_event) do
{
'action' => action,
'target' => target,
'actor' => { 'user_type' => 'deploy_token', 'name' => originator.username, 'user' => token.encoded }
}
end
where(:target, :action, :event, :count) do
{ 'tag' => 'latest' } | 'push' | 'push_tag' | 1
{ 'tag' => 'latest' } | 'delete' | 'delete_tag' | 1
{ 'repository' => 'foo/bar' } | 'push' | 'create_repository' | 1
{ 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 1
{ 'tag' => 'latest' } | 'copy' | '' | 0
end
with_them do
it_behaves_like 'event originator is fetched based on ID', DeployToken
it_behaves_like 'tracking event is sent to HLLRedisCounter with event and originator ID', :deploy_token
end
context "when there are errors" do
let(:action) { 'push' }
let(:target) { { 'tag' => 'latest' } }
context 'when the registry key file does not exist' do
before do
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with(key_file.path).and_raise(Errno::ENOENT)
end
it_behaves_like 'no tracking of a deploy token is sent to HLLRedisCounter'
end
[JWT::VerificationError, JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature].each do |error|
context "when JWT decoding encounters #{error}" do
before do
allow(JWT).to receive(:decode)
.with(token.encoded, rsa_key, true, { algorithm: "RS256" })
.and_raise(error)
end
it_behaves_like 'no tracking of a deploy token is sent to HLLRedisCounter'
end
end
end
end end
end end
context 'with a user as the actor' do context 'with a user as the actor' do
let_it_be(:originator) { create(:user, username: 'username') } let_it_be(:originator) { create(:user, username: 'username') }
let(:raw_event) do
{ context 'when user_id is available' do
'action' => action, let(:user_info) do
'target' => target, {
'actor' => { 'user_type' => user_type, 'name' => originator.username } token_type: user_type,
} username: originator.username,
user_id: originator.id
}
end
let(:token) do
JSONWebToken::RSAToken.new(rsa_key).tap do |token|
token[:user_info] = user_info
end
end
let(:raw_event) do
{
'action' => action,
'target' => target,
'actor' => { 'user_type' => user_type, 'name' => originator.username, 'user' => token.encoded }
}
end
where(:target, :action, :event, :user_type, :count) do
{ 'tag' => 'latest' } | 'push' | 'push_tag' | 'personal_access_token' | 1
{ 'tag' => 'latest' } | 'delete' | 'delete_tag' | 'personal_access_token' | 1
{ 'repository' => 'foo/bar' } | 'push' | 'create_repository' | 'build' | 1
{ 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'gitlab_or_ldap' | 1
{ 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'not_a_user' | 0
{ 'tag' => 'latest' } | 'copy' | '' | nil | 0
{ 'repository' => 'foo/bar' } | 'copy' | '' | '' | 0
end
with_them do
it_behaves_like 'event originator is fetched based on ID', User
it_behaves_like 'tracking event is sent to HLLRedisCounter with event and originator ID', :user
end
end end
where(:target, :action, :event, :user_type, :count) do context 'when only username is available and user_id is not' do
{ 'tag' => 'latest' } | 'push' | 'push_tag' | 'personal_access_token' | 1 let(:raw_event) do
{ 'tag' => 'latest' } | 'delete' | 'delete_tag' | 'personal_access_token' | 1 {
{ 'repository' => 'foo/bar' } | 'push' | 'create_repository' | 'build' | 1 'action' => 'push',
{ 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'gitlab_or_ldap' | 1 'target' => { 'tag' => 'latest' },
{ 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'not_a_user' | 0 'actor' => { 'user_type' => 'personal_access_token', 'name' => originator.username }
{ 'tag' => 'latest' } | 'copy' | '' | nil | 0 }
{ 'repository' => 'foo/bar' } | 'copy' | '' | '' | 0 end
it 'does not send a tracking event to HLLRedisCounter' do
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
subject
end
end end
with_them do context 'when no username or id is given' do
it_behaves_like 'tracking event is sent to HLLRedisCounter with event and originator ID', :user let(:raw_event) { { 'action' => 'push', 'target' => {}, 'actor' => { 'user_type' => 'build' } } }
it 'does not send a tracking event to HLLRedisCounter' do
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
subject
end
end end
end end
...@@ -246,16 +381,5 @@ ...@@ -246,16 +381,5 @@
subject subject
end end
end end
context 'without an actor name' do
let(:raw_event) { { 'action' => 'push', 'target' => {}, 'actor' => { 'user_type' => 'personal_access_token' } } }
it 'does not send a tracking event to HLLRedisCounter' do
expect(User).not_to receive(:find_by_username)
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
subject
end
end
end end
end end
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册