diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb index e9fb2563e42e1ee02cbd84f54d9fbbdc08c94b59..76d6535a21f261fab6d994c216fbfecaae0f2edd 100644 --- a/app/controllers/concerns/dependency_proxy/group_access.rb +++ b/app/controllers/concerns/dependency_proxy/group_access.rb @@ -11,13 +11,45 @@ module GroupAccess private + def auth_user_or_token + if defined?(personal_access_token) && personal_access_token && auth_user.is_a?(::User) && + ( + (auth_user.project_bot? && auth_user.resource_bot_resource.is_a?(::Group)) || + auth_user.human? || + auth_user.service_account? + ) + personal_access_token + else + auth_user + end + end + def verify_dependency_proxy_available! render_404 unless group&.dependency_proxy_feature_available? end + # TODO: Split the authorization logic into dedicated methods + # https://gitlab.com/gitlab-org/gitlab/-/issues/452145 def authorize_read_dependency_proxy! + if Feature.enabled?(:packages_dependency_proxy_pass_token_to_policy, group) + if auth_user_or_token.is_a?(User) + authorize_read_dependency_proxy_for_users! + else + authorize_read_dependency_proxy_for_tokens! + end + else + authorize_read_dependency_proxy_for_users! + end + end + + def authorize_read_dependency_proxy_for_users! access_denied! unless can?(auth_user, :read_dependency_proxy, group) end + + def authorize_read_dependency_proxy_for_tokens! + access_denied! unless can?(auth_user_or_token, :read_dependency_proxy, + group&.dependency_proxy_for_containers_policy_subject) + end end end diff --git a/app/controllers/groups/dependency_proxy/application_controller.rb b/app/controllers/groups/dependency_proxy/application_controller.rb index a5266d89ddbee076ea91397a05b2aac6faa71451..b1bc53507a0ee0067aee3b42371e0603928594a0 100644 --- a/app/controllers/groups/dependency_proxy/application_controller.rb +++ b/app/controllers/groups/dependency_proxy/application_controller.rb @@ -19,15 +19,18 @@ def authenticate_user_from_jwt_token! authenticate_with_http_token do |token, _| @authentication_result = EMPTY_AUTH_RESULT - user_or_deploy_token = ::DependencyProxy::AuthTokenService.user_or_deploy_token_from_jwt(token) - - case user_or_deploy_token - when User - @authentication_result = Gitlab::Auth::Result.new(user_or_deploy_token, nil, :user, []) - sign_in(user_or_deploy_token) unless user_or_deploy_token.project_bot? || - user_or_deploy_token.service_account? - when DeployToken - @authentication_result = Gitlab::Auth::Result.new(user_or_deploy_token, nil, :deploy_token, []) + if Feature.enabled?(:packages_dependency_proxy_pass_token_to_policy, group_from_params) + user_or_token = ::DependencyProxy::AuthTokenService.user_or_token_from_jwt(token) + sign_in_and_setup_authentication_result(user_or_token) + else + user_or_token = ::DependencyProxy::AuthTokenService.user_or_deploy_token_from_jwt(token) + case user_or_token + when User + @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :user, []) + sign_in(user_or_token) unless user_or_token.project_bot? || user_or_token.service_account? + when DeployToken + @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :deploy_token, []) + end end end @@ -36,11 +39,32 @@ def authenticate_user_from_jwt_token! private + attr_reader :personal_access_token + + def group_from_params + GroupsFinder.new(nil, { search: params[:group_id] }).execute.first + end + def request_bearer_token! # unfortunately, we cannot use https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html#method-i-authentication_request response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header render plain: '', status: :unauthorized end + + # When we rollout packages_dependency_proxy_pass_token_to_policy, + # we can move the body of this method inline, inside authenticate_user_from_jwt_token! + def sign_in_and_setup_authentication_result(user_or_token) + case user_or_token + when User + @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :user, []) + sign_in(user_or_token) + when PersonalAccessToken + @authentication_result = Gitlab::Auth::Result.new(user_or_token.user, nil, :personal_access_token, []) + @personal_access_token = user_or_token + when DeployToken + @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :deploy_token, []) + end + end end end end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index e6e232cfbc38fc93fd8c5791e0297771aac60568..de7fe52e3db5dadca84c889039f88d2e45d8cd2c 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -33,9 +33,10 @@ def authenticate_project_or_user @authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_only_authentication_abilities) authenticate_with_http_basic do |login, password| - @raw_token = password @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, request: request) + @raw_token = password if @authentication_result.type == :personal_access_token + if @authentication_result.failed? log_authentication_failed(login, @authentication_result) render_access_denied diff --git a/app/models/group.rb b/app/models/group.rb index 0668458b8e2b38638c13b4b4ef4051bd064296c4..cac322ea7ebe9e255451ebffc1c4363765503a7c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -971,6 +971,10 @@ def packages_policy_subject ::Packages::Policies::Group.new(self) end + def dependency_proxy_for_containers_policy_subject + ::Packages::Policies::DependencyProxy::Group.new(self) + end + def update_two_factor_requirement_for_members hierarchy_members.find_each(&:update_two_factor_requirement) end diff --git a/app/models/packages/policies/dependency_proxy/group.rb b/app/models/packages/policies/dependency_proxy/group.rb new file mode 100644 index 0000000000000000000000000000000000000000..00986e6f474ba6608a8ca93736c7b55b1b927abc --- /dev/null +++ b/app/models/packages/policies/dependency_proxy/group.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# We use this class, in conjunction with the +# Group#dependency_proxy_for_containers_policy_subject method, +# to specify a custom policy class for DependencyProxy. +# A similar pattern was used in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90963 +module Packages + module Policies + module DependencyProxy + class Group + attr_reader :group + + delegate :dependency_proxy_feature_available?, :full_path, :licensed_feature_available?, + :max_member_access_for_user, :member?, :owned_by?, :public?, :root_ancestor, + :root_ancestor_ip_restrictions, to: :group + + def initialize(group) + @group = group + end + end + end + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 560625f6541db011767692a95018a988819112ee..45f02b438c5fb35b27ac7e63b5c2dec86c4c627d 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -462,6 +462,7 @@ def resource_access_token_creation_allowed? resource_access_token_create_feature_available? && group.root_ancestor.namespace_settings.resource_access_token_creation_allowed? end + # We can remove this when we rollout the feature flag packages_dependency_proxy_pass_token_to_policy def valid_dependency_proxy_deploy_token @user.is_a?(DeployToken) && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject) end diff --git a/app/policies/packages/policies/dependency_proxy/group_policy.rb b/app/policies/packages/policies/dependency_proxy/group_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..11e57a2d33782258b3dbe7ec129f9d281758e1f6 --- /dev/null +++ b/app/policies/packages/policies/dependency_proxy/group_policy.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# The policies defined in GroupPolicy is used in GraphQL requests +# With a GraphQL request, the user is always a human User +# +# With JWT requests, we can be dealing with any of the following: +# - a PrAT for a human +# - a PrAT for a service account +# - a GrAT +# - a Group DeployToken +# +# We use this custom policy class for JWT requests +module Packages + module Policies + module DependencyProxy + class GroupPolicy < ::GroupPolicy + overrides(:read_dependency_proxy) + + desc "Deploy token with read access to dependency proxy" + condition(:read_dependency_proxy_deploy_token) do + @user.is_a?(DeployToken) && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject.group) + end + + desc "Personal access or group access token with read access to dependency proxy" + condition(:read_dependency_proxy_personal_access_token) do + user_is_personal_access_token? && + ( + user.user.human? || + user.user.service_account? || + (user.user.project_bot? && user.user.resource_bot_resource.is_a?(::Group)) + ) && + (access_level(for_any_session: true) >= GroupMember::GUEST) + end + + condition(:dependency_proxy_disabled, scope: :subject) do + !@subject.dependency_proxy_feature_available? + end + + rule { dependency_proxy_disabled }.prevent :read_dependency_proxy + + rule do + read_dependency_proxy_personal_access_token | read_dependency_proxy_deploy_token + end.enable :read_dependency_proxy + + rule do + ~read_dependency_proxy_personal_access_token & ~read_dependency_proxy_deploy_token + end.prevent :read_dependency_proxy + + def access_level(for_any_session: false) + return GroupMember::NO_ACCESS if @user.nil? + + @access_level ||= lookup_access_level!(for_any_session: for_any_session) + end + + def lookup_access_level!(_) + @subject.max_member_access_for_user(@user.user) + end + + def user_is_personal_access_token? + user.is_a?(PersonalAccessToken) + end + end + end + end +end + +Packages::Policies::DependencyProxy::GroupPolicy.prepend_mod_with('Packages::Policies::DependencyProxy::GroupPolicy') diff --git a/app/services/auth/dependency_proxy_authentication_service.rb b/app/services/auth/dependency_proxy_authentication_service.rb index 29f5a50d809ffb5f53a6c5a55dc0fb8f7714dd0d..2ba9da159c939798a6908ebd70aa792c5aa453d5 100644 --- a/app/services/auth/dependency_proxy_authentication_service.rb +++ b/app/services/auth/dependency_proxy_authentication_service.rb @@ -65,6 +65,8 @@ def authorized_token JSONWebToken::HMACToken.new(self.class.secret).tap do |token| token['user_id'] = current_user.id if current_user token['deploy_token'] = deploy_token.token if deploy_token + token['personal_access_token'] = raw_token if personal_access_token_user? + token['group_access_token'] = raw_token if group_access_token_user? token.expire_time = self.class.token_expire_at end end @@ -76,5 +78,13 @@ def deploy_token def raw_token params[:raw_token] end + + def group_access_token_user? + raw_token && current_user&.project_bot? && current_user.resource_bot_resource.is_a?(Group) + end + + def personal_access_token_user? + raw_token && current_user && (current_user.human? || current_user.service_account?) + end end end diff --git a/app/services/dependency_proxy/auth_token_service.rb b/app/services/dependency_proxy/auth_token_service.rb index c6c9eb534bbb26cae28d04886c3f78d0638c3fcd..a4034894e13d614d78246eacc860451e189ba81b 100644 --- a/app/services/dependency_proxy/auth_token_service.rb +++ b/app/services/dependency_proxy/auth_token_service.rb @@ -12,6 +12,10 @@ def execute JSONWebToken::HMACToken.decode(token, ::Auth::DependencyProxyAuthenticationService.secret).first end + # Rename to make it obvious how it's used in Gitlab::Auth::RequestAuthenticator + # which is to return an <object>.<id> that is used as a rack-attack discriminator + # that way it cannot be confused with `.user_or_token_from_jwt` + # https://gitlab.com/gitlab-org/gitlab/-/issues/454518 def self.user_or_deploy_token_from_jwt(raw_jwt) token_payload = self.new(raw_jwt).execute @@ -23,5 +27,34 @@ def self.user_or_deploy_token_from_jwt(raw_jwt) rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature nil end + + def self.user_or_token_from_jwt(raw_jwt) + token_payload = self.new(raw_jwt).execute + + if token_payload['personal_access_token'] + get_personal_access_token(token_payload['personal_access_token']) + elsif token_payload['group_access_token'] + # a group access token is a personal access token in disguise + get_personal_access_token(token_payload['group_access_token']) + elsif token_payload['deploy_token'] + get_deploy_token(token_payload['deploy_token']) + elsif token_payload['user_id'] + get_user(token_payload['user_id']) + end + rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature + nil + end + + def self.get_user(user_id) + User.find(user_id) + end + + def self.get_personal_access_token(raw_token) + PersonalAccessTokensFinder.new(state: 'active').find_by_token(raw_token) + end + + def self.get_deploy_token(raw_token) + DeployToken.active.find_by_token(raw_token) + end end end diff --git a/config/feature_flags/gitlab_com_derisk/packages_dependency_proxy_pass_token_to_policy.yml b/config/feature_flags/gitlab_com_derisk/packages_dependency_proxy_pass_token_to_policy.yml new file mode 100644 index 0000000000000000000000000000000000000000..9131f3f01d791d27fff2939dfd2cd3ca2780f715 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/packages_dependency_proxy_pass_token_to_policy.yml @@ -0,0 +1,9 @@ +--- +name: packages_dependency_proxy_pass_token_to_policy +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434291 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141358 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/441588 +milestone: '17.0' +group: group::container registry +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/app/controllers/concerns/ee/dependency_proxy/group_access.rb b/ee/app/controllers/concerns/ee/dependency_proxy/group_access.rb index 0a832f8988a645d603196f20f86de89e1dc56e04..f07eeed9d0fe9c13d8b1839c3a474213d21b32e6 100644 --- a/ee/app/controllers/concerns/ee/dependency_proxy/group_access.rb +++ b/ee/app/controllers/concerns/ee/dependency_proxy/group_access.rb @@ -8,6 +8,8 @@ module GroupAccess private + # Rename this to authorize_read_dependency_proxy_for_tokens! + # when we rollout FF packages_dependency_proxy_pass_token_to_policy override :authorize_read_dependency_proxy! def authorize_read_dependency_proxy! if ip_restricted?(group) diff --git a/ee/app/policies/ee/packages/policies/dependency_proxy/group_policy.rb b/ee/app/policies/ee/packages/policies/dependency_proxy/group_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..6545e582dcbf8ae06fc45c8c0eb99ff59419eb65 --- /dev/null +++ b/ee/app/policies/ee/packages/policies/dependency_proxy/group_policy.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module EE + module Packages + module Policies + module DependencyProxy + module GroupPolicy + extend ActiveSupport::Concern + extend ::Gitlab::Utils::Override + + prepended do + with_scope :user + condition(:auditor, score: 0) { false } + + condition(:no_active_sso_session, scope: :subject) do + policy_user = user.user + + ::Gitlab::Auth::GroupSaml::SessionEnforcer.new(policy_user, subject).access_restricted? + end + end + + # This is a copy of lookup_access_level! in ee/app/policies/ee/group_policy.rb + override :lookup_access_level! + def lookup_access_level!(for_any_session: false) + if for_any_session + return ::GroupMember::NO_ACCESS if no_active_sso_session? + elsif needs_new_sso_session? + return ::GroupMember::NO_ACCESS + end + + super + end + end + end + end + end +end diff --git a/ee/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/ee/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index 19ce13bd85c133975e9e388d33a97655a71a31f7..1e64b1fefe6eb1795af1c41e3ff11e28d538ad40 100644 --- a/ee/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/ee/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -122,7 +122,10 @@ it_behaves_like 'returning response status', :not_found context 'when user is a deploy token' do - let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + let_it_be(:deploy_token) do + create(:deploy_token, user: user, read_package_registry: true, write_package_registry: true) + end + let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: group) } let(:authenticated_subject) { deploy_token } diff --git a/ee/spec/policies/packages/policies/dependency_proxy/group_policy_spec.rb b/ee/spec/policies/packages/policies/dependency_proxy/group_policy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0aba2fdd8a62185d36e59bb85ec093107a1b64a9 --- /dev/null +++ b/ee/spec/policies/packages/policies/dependency_proxy/group_policy_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Policies::DependencyProxy::GroupPolicy, feature_category: :container_registry do + include_context 'GroupPolicy context' + + subject do + described_class.new(auth_token, group.dependency_proxy_for_containers_policy_subject) + end + + let_it_be(:current_user) { guest } + let_it_be(:auth_token) { create(:personal_access_token, user: current_user) } + + before do + stub_config(dependency_proxy: { enabled: true }, registry: { enabled: true }) + end + + context 'when there is no active sso session' do + before do + allow(::Gitlab::Auth::GroupSaml::SsoEnforcer) + .to receive(:access_restricted?) + .and_return(true) + end + + it { is_expected.to be_disallowed(:guest_access) } + end +end diff --git a/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb b/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb index d9cfc91b33e8dd68450945a3735baecd806d7e75..b8726bbfd5dff0373c47441bc9867aca572e056e 100644 --- a/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Groups::DependencyProxyAuthController do +RSpec.describe Groups::DependencyProxyAuthController, feature_category: :container_registry do include DependencyProxyHelpers describe 'GET #authenticate' do @@ -33,21 +33,57 @@ end context 'group bot user' do - let_it_be(:user) { create(:user, :project_bot) } + context 'with packages_dependency_proxy_pass_token_to_policy disabled' do + let_it_be(:user) { create(:user, :project_bot) } - it { is_expected.to have_gitlab_http_status(:success) } + before do + stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) + end + + it { is_expected.to have_gitlab_http_status(:success) } + end + + context 'with packages_dependency_proxy_pass_token_to_policy enabled' do + let_it_be(:bot_user) { create(:user, :project_bot) } + let_it_be(:user) { create(:personal_access_token, user: bot_user) } + + it { is_expected.to have_gitlab_http_status(:success) } + end end context 'service account user' do - let_it_be(:user) { create(:user, :service_account) } + context 'with packages_dependency_proxy_pass_token_to_policy disabled' do + let_it_be(:user) { create(:user, :service_account) } - it { is_expected.to have_gitlab_http_status(:success) } + before do + stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) + end + + it { is_expected.to have_gitlab_http_status(:success) } + end + + context 'with packages_dependency_proxy_pass_token_to_policy enabled' do + let_it_be(:service_account_user) { create(:user, :service_account) } + let_it_be(:user) { create(:personal_access_token, user: service_account_user) } + + it { is_expected.to have_gitlab_http_status(:success) } + end end context 'deploy token' do let_it_be(:user) { create(:deploy_token) } - it { is_expected.to have_gitlab_http_status(:success) } + context 'with packages_dependency_proxy_pass_token_to_policy disabled' do + before do + stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) + end + + it { is_expected.to have_gitlab_http_status(:success) } + end + + context 'with packages_dependency_proxy_pass_token_to_policy enabled' do + it { is_expected.to have_gitlab_http_status(:success) } + end end end diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index 9b9772484350a088c07a6909fe35b54a3cffbec5..34ca70e17fec5fe1edac868061b2338a971ab1a8 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -62,6 +62,8 @@ context 'with invalid group access token' do let_it_be(:user) { create(:user, :project_bot) } + let_it_be(:token) { create(:personal_access_token, user: user, scopes: [Gitlab::Auth::READ_API_SCOPE]) } + let_it_be(:jwt) { build_jwt(token) } context 'not under the group' do it { is_expected.to have_gitlab_http_status(:not_found) } @@ -82,8 +84,6 @@ end context 'with insufficient scopes' do - let_it_be(:pat) { create(:personal_access_token, user: user, scopes: [Gitlab::Auth::READ_API_SCOPE]) } - it { is_expected.to have_gitlab_http_status(:not_found) } context 'packages_dependency_proxy_containers_scope_check disabled' do @@ -193,7 +193,19 @@ token.update_column(:scopes, Gitlab::Auth::REGISTRY_SCOPES) end - it_behaves_like 'sends Workhorse instructions' + context 'with packages_dependency_proxy_pass_token_to_policy disabled' do + before do + stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) + end + + it_behaves_like 'sends Workhorse instructions' + end + + context 'with packages_dependency_proxy_pass_token_to_policy enabled' do + let_it_be(:jwt) { build_jwt(token) } + + it_behaves_like 'sends Workhorse instructions' + end end context 'with a deploy token' do @@ -293,6 +305,15 @@ it_behaves_like 'a successful manifest pull' it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false + context 'when packages_dependency_proxy_pass_token_to_policy is disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + it_behaves_like 'a successful manifest pull' + it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false + end + context 'with workhorse response' do let(:pull_response) { { status: :success, manifest: nil, from_cache: false } } @@ -324,6 +345,14 @@ it_behaves_like 'a successful manifest pull' + context 'when packages_dependency_proxy_pass_token_to_policy is disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + it_behaves_like 'a successful manifest pull' + end + context 'pulling from a subgroup' do let_it_be_with_reload(:parent_group) { create(:group) } let_it_be_with_reload(:group) { create(:group, parent: parent_group) } @@ -344,8 +373,21 @@ group.add_guest(user) end - it_behaves_like 'a successful manifest pull' - it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false + context 'when packages_dependency_proxy_pass_token_to_policy is disabled' do + before do + stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) + end + + it_behaves_like 'a successful manifest pull' + it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false + end + + context 'when packages_dependency_proxy_pass_token_to_policy is enabled' do + let_it_be(:jwt) { build_jwt(token) } + + it_behaves_like 'a successful manifest pull' + it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false + end end end @@ -367,6 +409,14 @@ def get_manifest(tag) it_behaves_like 'without a token' it_behaves_like 'without permission' + context 'when packages_dependency_proxy_pass_token_to_policy is disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + context 'a valid user' do before do group.add_guest(user) diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 1eae4ae3c15bb7050c2937acb7141b907e2758d2..4862e12f02f9acc06738ef1b7f7d2c8817b96ecf 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -1114,6 +1114,7 @@ end end + # This block can be removed when packages_dependency_proxy_pass_token_to_policy is rolled out describe 'dependency proxy' do RSpec.shared_examples 'disabling admin_package feature flag' do before do diff --git a/spec/policies/packages/policies/dependency_proxy/group_policy_spec.rb b/spec/policies/packages/policies/dependency_proxy/group_policy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..15b14d69ab0df243a8224d4113138900af39e1d8 --- /dev/null +++ b/spec/policies/packages/policies/dependency_proxy/group_policy_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Policies::DependencyProxy::GroupPolicy, feature_category: :system_access do + subject { described_class.new(auth_token, group.dependency_proxy_for_containers_policy_subject) } + + let_it_be(:guest) { create(:user) } + let_it_be(:non_group_member) { create(:user) } + let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only) } + let_it_be(:current_user) { guest } + + before do + group.add_guest(guest) + end + + describe 'dependency proxy' do + shared_examples 'disallows dependency proxy read access' do + it { is_expected.to be_disallowed(:read_dependency_proxy) } + end + + shared_examples 'allows dependency proxy read access' do + it { is_expected.to be_allowed(:read_dependency_proxy) } + end + + context 'with feature disabled' do + let_it_be(:auth_token) { create(:personal_access_token, user: current_user) } + + before do + stub_config(dependency_proxy: { enabled: false }) + end + + it_behaves_like 'disallows dependency proxy read access' + end + + context 'with feature enabled' do + let_it_be(:auth_token) { create(:personal_access_token, user: current_user) } + + before do + stub_config(dependency_proxy: { enabled: true }, registry: { enabled: true }) + end + + context 'with a human user personal access token' do + subject { described_class.new(auth_token, group.dependency_proxy_for_containers_policy_subject) } + + context 'when not a member of the group' do + let_it_be(:current_user) { non_group_member } + let_it_be(:auth_token) { create(:personal_access_token, user: current_user) } + + it_behaves_like 'disallows dependency proxy read access' + end + + context 'when a member of the group' do + let_it_be(:current_user) { guest } + let_it_be(:auth_token) { create(:personal_access_token, user: current_user) } + + it_behaves_like 'allows dependency proxy read access' + end + end + + context 'with a deploy token user' do + before do + create(:group_deploy_token, group: group, deploy_token: auth_token) + end + + context 'with insufficient scopes' do + let_it_be(:auth_token) { create(:deploy_token, :group, user: current_user) } + + it_behaves_like 'disallows dependency proxy read access' + end + + context 'with sufficient scopes' do + let_it_be(:auth_token) { create(:deploy_token, :group, :dependency_proxy_scopes) } + + it_behaves_like 'allows dependency proxy read access' + end + end + + context 'with a group access token user' do + let_it_be(:bot_user) { create(:user, :project_bot) } + let_it_be(:auth_token) do + create(:personal_access_token, user: bot_user, scopes: [Gitlab::Auth::READ_API_SCOPE]) + end + + context 'when not a member of the group' do + it_behaves_like 'disallows dependency proxy read access' + end + + context 'when a member of the group' do + before do + group.add_guest(bot_user) + end + + it_behaves_like 'allows dependency proxy read access' + end + end + end + end +end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 992c5ab0b136199829953f15c19714017ed6efcb..539055d103e5abeaf7f17eaf72fea483f79160c0 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -92,7 +92,7 @@ context 'project with enabled CI' do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build, raw_token: build.token)).permit!) } + it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build)).permit!) } it_behaves_like 'user logging' end @@ -119,7 +119,7 @@ .with( nil, nil, - ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token, raw_token: deploy_token.token)).permit! + ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token)).permit! ) end @@ -160,7 +160,7 @@ subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)).permit!) } + it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap)).permit!) } it_behaves_like 'rejecting a blocked user' @@ -180,7 +180,7 @@ ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit! end - it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)) } + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } it_behaves_like 'user logging' end @@ -197,7 +197,7 @@ ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit! end - it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)) } + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } end context 'when user has 2FA enabled' do diff --git a/spec/services/auth/dependency_proxy_authentication_service_spec.rb b/spec/services/auth/dependency_proxy_authentication_service_spec.rb index e81f59cff39cf30211674ec37f38789200553203..2cd3aad9aa818b5ae5f691d78efe67e34edda48a 100644 --- a/spec/services/auth/dependency_proxy_authentication_service_spec.rb +++ b/spec/services/auth/dependency_proxy_authentication_service_spec.rb @@ -6,7 +6,7 @@ let_it_be(:user) { create(:user) } let_it_be(:params) { {} } - let(:authentication_abilities) { nil } + let(:authentication_abilities) { [] } let(:service) { described_class.new(nil, user, params) } before do @@ -48,31 +48,57 @@ end context 'with a deploy token' do + let(:user) { nil } let_it_be(:deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) } let_it_be(:params) { { deploy_token: deploy_token } } it_behaves_like 'returning a token with an encoded field', 'deploy_token' + + context 'with packages_dependency_proxy_containers_scope_check disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + it_behaves_like 'returning a token with an encoded field', 'deploy_token' + end + + context 'with packages_dependency_proxy_pass_token_to_policy disabled' do + before do + stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) + end + + it_behaves_like 'returning a token with an encoded field', 'deploy_token' + end end context 'with a human user' do it_behaves_like 'returning a token with an encoded field', 'user_id' - end - context 'all other user types' do - User::USER_TYPES.except(:human, :project_bot).each_value do |user_type| - context "with user_type #{user_type}" do - before do - user.update!(user_type: user_type) - end - - it_behaves_like 'returning a token with an encoded field', 'user_id' + context 'with packages_dependency_proxy_pass_token_to_policy disabled' do + before do + stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) end + + it_behaves_like 'returning a token with an encoded field', 'user_id' end end + context 'with a personal access token user' do + let_it_be_with_reload(:token) { create(:personal_access_token, user: user) } + let_it_be(:params) { { raw_token: token.token } } + + it_behaves_like 'returning a token with an encoded field', 'personal_access_token' + end + context 'with a group access token' do let_it_be(:user) { create(:user, :project_bot) } + let_it_be(:group) { create(:group) } let_it_be_with_reload(:token) { create(:personal_access_token, user: user) } + let_it_be(:params) { { raw_token: token.token } } + + before_all do + group.add_guest(user) + end context 'with insufficient authentication abilities' do it_behaves_like 'returning', status: 403, message: 'access forbidden' @@ -82,7 +108,7 @@ stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) end - it_behaves_like 'returning a token with an encoded field', 'user_id' + it_behaves_like 'returning a token with an encoded field', 'group_access_token' end end @@ -92,7 +118,15 @@ subject { service.execute(authentication_abilities: authentication_abilities) } - it_behaves_like 'returning a token with an encoded field', 'user_id' + it_behaves_like 'returning a token with an encoded field', 'group_access_token' + + context 'with packages_dependency_proxy_pass_token_to_policy disabled' do + before do + stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) + end + + it_behaves_like 'returning a token with an encoded field', 'user_id' + end context 'revoked' do before do @@ -112,6 +146,18 @@ end end + context 'all other user types' do + User::USER_TYPES.except(:human, :project_bot).each_value do |user_type| + context "with user_type #{user_type}" do + before do + user.update!(user_type: user_type) + end + + it_behaves_like 'returning a token with an encoded field', 'user_id' + end + end + end + def decode(token) DependencyProxy::AuthTokenService.new(token).execute end diff --git a/spec/services/dependency_proxy/auth_token_service_spec.rb b/spec/services/dependency_proxy/auth_token_service_spec.rb index 2612c5765a49f7d4d860667c9ff19b1d0dcbb0ac..43a672032b93290f428d4ef34ae6d4dd91379754 100644 --- a/spec/services/dependency_proxy/auth_token_service_spec.rb +++ b/spec/services/dependency_proxy/auth_token_service_spec.rb @@ -5,8 +5,36 @@ include DependencyProxyHelpers let_it_be(:user) { create(:user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:group_access_token) { create(:personal_access_token, user: user) } let_it_be(:deploy_token) { create(:deploy_token) } + shared_examples 'handling token errors' do + context 'with a decoding error' do + before do + allow(JWT).to receive(:decode).and_raise(JWT::DecodeError) + end + + it { is_expected.to eq(nil) } + end + + context 'with an immature signature error' do + before do + allow(JWT).to receive(:decode).and_raise(JWT::ImmatureSignature) + end + + it { is_expected.to eq(nil) } + end + + context 'with an expired signature error' do + it 'returns nil' do + travel_to(Time.zone.now + Auth::DependencyProxyAuthenticationService.token_expire_at + 1.minute) do + expect(subject).to eq(nil) + end + end + end + end + describe '.user_or_deploy_token_from_jwt' do subject { described_class.user_or_deploy_token_from_jwt(token.encoded) } @@ -72,4 +100,84 @@ it { is_expected.to eq(nil) } end end + + describe '.user_or_token_from_jwt' do + subject { described_class.user_or_token_from_jwt(token.encoded) } + + context 'with a user' do + let_it_be(:token) { build_jwt(user) } + + it { is_expected.to eq(user) } + + context 'with an invalid user id' do + let_it_be(:token) { build_jwt { |jwt| jwt['user_id'] = 'this_is_not_a_user_id' } } + + it 'raises an not found error' do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + it_behaves_like 'handling token errors' + end + + context 'with a personal access token' do + let_it_be(:token) { build_jwt(personal_access_token) } + + it { is_expected.to eq(personal_access_token) } + + context 'with an inactive token' do + before do + personal_access_token.revoke! + end + + it { is_expected.to eq(nil) } + end + + context 'with an invalid token' do + let_it_be(:token) { build_jwt { |jwt| jwt['personal_access_token'] = 'this_is_not_a_token' } } + + it { is_expected.to eq(nil) } + end + end + + context 'with a group access token' do + let_it_be(:token) { build_jwt(group_access_token) } + + it { is_expected.to eq(group_access_token) } + + context 'with an inactive token' do + before do + group_access_token.revoke! + end + + it { is_expected.to eq(nil) } + end + + context 'with an invalid token' do + let_it_be(:token) { build_jwt { |jwt| jwt['group_access_token'] = 'this_is_not_a_token' } } + + it { is_expected.to eq(nil) } + end + end + + context 'with a deploy token' do + let_it_be(:token) { build_jwt(deploy_token) } + + it { is_expected.to eq(deploy_token) } + + context 'with an invalid token' do + let_it_be(:token) { build_jwt { |jwt| jwt['deploy_token'] = 'this_is_not_a_token' } } + + it { is_expected.to eq(nil) } + end + + it_behaves_like 'handling token errors' + end + + context 'with an empty token payload' do + let_it_be(:token) { build_jwt(nil) } + + it { is_expected.to eq(nil) } + end + end end diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb index 75dc09ec159e32b8fb3dd1064f21d0b1e6422524..ae54c64a4d972684b3d543174787c67990ef22f5 100644 --- a/spec/support/helpers/dependency_proxy_helpers.rb +++ b/spec/support/helpers/dependency_proxy_helpers.rb @@ -32,13 +32,14 @@ def stub_blob_download(image, blob_sha, status = 200, body = '123456') .to_return(status: status, body: body) end - def build_jwt(user = nil, expire_time: nil) + def build_jwt(user_or_token = nil, expire_time: nil) JSONWebToken::HMACToken.new(::Auth::DependencyProxyAuthenticationService.secret).tap do |jwt| if block_given? yield(jwt) else - jwt['user_id'] = user.id if user.is_a?(User) - jwt['deploy_token'] = user.token if user.is_a?(DeployToken) + jwt['user_id'] = user_or_token.id if user_or_token.is_a?(User) + jwt['personal_access_token'] = user_or_token.token if user_or_token.is_a?(PersonalAccessToken) + jwt['deploy_token'] = user_or_token.token if user_or_token.is_a?(DeployToken) jwt.expire_time = expire_time || jwt.issued_at + 1.minute end end