diff --git a/app/services/auth/dependency_proxy_authentication_service.rb b/app/services/auth/dependency_proxy_authentication_service.rb index 1224a86ce6df9b588fc61127d7f4313651ddcd19..d090a62229f2ddaf4a813e47712830ecf0886c8b 100644 --- a/app/services/auth/dependency_proxy_authentication_service.rb +++ b/app/services/auth/dependency_proxy_authentication_service.rb @@ -5,17 +5,19 @@ class DependencyProxyAuthenticationService < BaseService AUDIENCE = 'dependency_proxy' HMAC_KEY = 'gitlab-dependency-proxy' DEFAULT_EXPIRE_TIME = 1.minute - REQUIRED_ABILITIES = %i[read_container_image create_container_image].freeze + REQUIRED_CI_ABILITIES = %i[build_read_container_image build_create_container_image].freeze + REQUIRED_USER_ABILITIES = %i[read_container_image create_container_image].freeze MISSING_ABILITIES_MESSAGE = 'Dependency proxy missing authentication abilities' def execute(authentication_abilities:) + @authentication_abilities = authentication_abilities + return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled - return error('access forbidden', 403) unless valid_user_actor?(authentication_abilities) + return error('access forbidden', 403) unless valid_user_actor? - unless deploy_token || has_required_abilities?(authentication_abilities) - log_missing_authentication_abilities(authentication_abilities) - end + # TODO: Remove this when enforce_abilities_check_for_dependency_proxy is permanently enabled + log_missing_authentication_abilities unless deploy_token || has_required_abilities? { token: authorized_token.encoded } end @@ -40,23 +42,35 @@ def token_expire_at private - def valid_user_actor?(authentication_abilities) + attr_reader :authentication_abilities + + def valid_user_actor? feature_user = deploy_token&.user || current_user + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 if Feature.enabled?(:packages_dependency_proxy_containers_scope_check, feature_user) - if deploy_token - deploy_token.valid_for_dependency_proxy? - elsif current_user&.project_bot? - group_access_token&.active? && has_required_abilities?(authentication_abilities) - else - current_user - end + dependency_proxy_containers_scope_check + elsif Feature.enabled?(:enforce_abilities_check_for_dependency_proxy, feature_user) + has_required_abilities? else current_user || valid_deploy_token? end end - def has_required_abilities?(authentication_abilities) - (REQUIRED_ABILITIES & authentication_abilities).size == REQUIRED_ABILITIES.size + def dependency_proxy_containers_scope_check + if deploy_token + deploy_token.valid_for_dependency_proxy? + elsif current_user&.project_bot? + group_access_token&.active? && has_required_abilities? + else + current_user + end + end + + def has_required_abilities? + [REQUIRED_CI_ABILITIES, REQUIRED_USER_ABILITIES].any? do |required_abilities| + (required_abilities & authentication_abilities).size == required_abilities.size + end end def group_access_token @@ -95,7 +109,7 @@ def personal_access_token_user? raw_token && current_user && (current_user.human? || current_user.service_account?) end - def log_missing_authentication_abilities(authentication_abilities) + def log_missing_authentication_abilities log_info = { message: MISSING_ABILITIES_MESSAGE, authentication_abilities: authentication_abilities, diff --git a/config/feature_flags/wip/enforce_abilities_check_for_dependency_proxy.yml b/config/feature_flags/wip/enforce_abilities_check_for_dependency_proxy.yml new file mode 100644 index 0000000000000000000000000000000000000000..ce3b0254b09bf170bebd5c23fcac08e48b37105c --- /dev/null +++ b/config/feature_flags/wip/enforce_abilities_check_for_dependency_proxy.yml @@ -0,0 +1,9 @@ +--- +name: enforce_abilities_check_for_dependency_proxy +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/520313 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182559 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/521193 +milestone: '17.10' +group: group::container registry +type: wip +default_enabled: false 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 9c81e5cb1b5d1a8891147edb5b463b652478ca1e..7dd970edc91dbadba299a4c0fa848cae1b279d88 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -86,6 +86,8 @@ context 'with insufficient scopes' do it { is_expected.to have_gitlab_http_status(:not_found) } + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 context 'packages_dependency_proxy_containers_scope_check disabled' do before do stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index bd4d3d8055becb08eda8f450e13f3ca4b8204b42..20a218cd1f9f52a4ae36db56677bbdba67fe59b7 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -304,6 +304,10 @@ def credentials(login, password) context 'authenticating against dependency proxy' do let_it_be(:user) { create(:user) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:personal_access_token_without_required_scopes) do + create(:personal_access_token, user: user, scopes: ['read_user']) + end + let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :private, group: group) } let_it_be(:bot_user) { create(:user, :project_bot) } @@ -332,12 +336,49 @@ def credentials(login, password) end end + shared_examples 'enforce_abilities_check_for_dependency_proxy disabled' do + before do + stub_feature_flags(enforce_abilities_check_for_dependency_proxy: false) + end + + it_behaves_like 'with valid credentials' + end + context 'with personal access token' do let(:credential_user) { nil } let(:credential_password) { personal_access_token.token } it_behaves_like 'with valid credentials' it_behaves_like 'a token that expires today' + it_behaves_like 'enforce_abilities_check_for_dependency_proxy disabled' + + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 + context 'packages_dependency_proxy_containers_scope_check disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + it_behaves_like 'with valid credentials' + it_behaves_like 'enforce_abilities_check_for_dependency_proxy disabled' + end + + context 'without the required scopes' do + let(:credential_password) { personal_access_token_without_required_scopes.token } + + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 + context '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 response status', :forbidden + it_behaves_like 'enforce_abilities_check_for_dependency_proxy disabled' + end + + it_behaves_like 'enforce_abilities_check_for_dependency_proxy disabled' + end end context 'with user credentials token' do @@ -355,6 +396,17 @@ def credentials(login, password) it_behaves_like 'with valid credentials' it_behaves_like 'a token that expires today' + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 + context 'packages_dependency_proxy_containers_scope_check disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + it_behaves_like 'with valid credentials' + it_behaves_like 'enforce_abilities_check_for_dependency_proxy disabled' + end + context 'revoked' do before do group_access_token.update!(revoked: true) @@ -370,6 +422,8 @@ def credentials(login, password) it_behaves_like 'returning response status', :unauthorized end + + it_behaves_like 'enforce_abilities_check_for_dependency_proxy disabled' end context 'without the required scopes' do @@ -379,6 +433,8 @@ def credentials(login, password) it_behaves_like 'returning response status', :forbidden + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 context 'packages_dependency_proxy_containers_scope_check disabled' do before do stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) @@ -386,6 +442,8 @@ def credentials(login, password) it_behaves_like 'with valid credentials' end + + it_behaves_like 'enforce_abilities_check_for_dependency_proxy disabled' end end @@ -403,6 +461,17 @@ def credentials(login, password) let(:credential_password) { job.token } it_behaves_like 'with valid credentials' + + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 + context 'packages_dependency_proxy_containers_scope_check disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + it_behaves_like 'with valid credentials' + it_behaves_like 'enforce_abilities_check_for_dependency_proxy disabled' + end end context 'with project deploy token' do diff --git a/spec/services/auth/dependency_proxy_authentication_service_spec.rb b/spec/services/auth/dependency_proxy_authentication_service_spec.rb index c2d37b537fb12ea7f4f0d4d8d3466ba6df6b064c..7084da48c4e75324c30ba69e09cf2b7dfa52e9c2 100644 --- a/spec/services/auth/dependency_proxy_authentication_service_spec.rb +++ b/spec/services/auth/dependency_proxy_authentication_service_spec.rb @@ -4,8 +4,8 @@ RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :virtual_registry do let_it_be(:user) { create(:user) } - let_it_be(:params) { {} } + let(:params) { {} } let(:authentication_abilities) { [] } let(:service) { described_class.new(nil, user, params) } @@ -73,20 +73,23 @@ 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 } } + let_it_be(:deploy_token_with_dependency_proxy_scopes) { create(:deploy_token, :group, :dependency_proxy_scopes) } + let_it_be(:deploy_token_with_missing_scopes) { create(:deploy_token, :group, read_registry: false) } - it_behaves_like 'returning a token with an encoded field', 'deploy_token' + let(:user) { nil } - context 'with packages_dependency_proxy_containers_scope_check disabled' do - before do - stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) - end + context 'with sufficient scopes' do + let(:params) { { deploy_token: deploy_token_with_dependency_proxy_scopes } } it_behaves_like 'returning a token with an encoded field', 'deploy_token' end + context 'with insufficient scopes' do + let(:params) { { deploy_token: deploy_token_with_missing_scopes } } + + it_behaves_like 'returning', status: 403, message: 'access forbidden' + end + context 'when the the deploy token is restricted with external_authorization' do before do allow(Gitlab::ExternalAuthorization).to receive(:allow_deploy_tokens_and_deploy_keys?).and_return(false) @@ -110,18 +113,49 @@ 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 } } + let(:params) { { raw_token: token.token } } it_behaves_like 'returning a token with an encoded field', 'personal_access_token' context 'with insufficient authentication abilities' do it_behaves_like 'logs missing authentication abilities' + + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 + 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', status: 403, message: 'access forbidden' + + context 'with enforce_abilities_check_for_dependency_proxy disabled' do + before do + stub_feature_flags(enforce_abilities_check_for_dependency_proxy: false) + end + + it_behaves_like 'returning a token with an encoded field', 'personal_access_token' + end + end end context 'with sufficient authentication abilities' do - let_it_be(:authentication_abilities) { Auth::DependencyProxyAuthenticationService::REQUIRED_ABILITIES } + [described_class::REQUIRED_USER_ABILITIES, + described_class::REQUIRED_CI_ABILITIES].each do |abilities| + context "with #{abilities}" do + let(:authentication_abilities) { abilities } + + it_behaves_like 'does not log missing authentication abilities' - it_behaves_like 'does not log missing authentication abilities' + context 'with enforce_abilities_check_for_dependency_proxy disabled' do + before do + stub_feature_flags(enforce_abilities_check_for_dependency_proxy: false) + end + + it_behaves_like 'returning a token with an encoded field', 'personal_access_token' + end + end + end end end @@ -129,7 +163,8 @@ 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 } } + + let(:params) { { raw_token: token.token } } before_all do group.add_guest(user) @@ -138,39 +173,61 @@ context 'with insufficient authentication abilities' do it_behaves_like 'returning', status: 403, message: 'access forbidden' + # TODO: Cleanup code related to packages_dependency_proxy_containers_scope_check + # https://gitlab.com/gitlab-org/gitlab/-/issues/520321 context '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', 'group_access_token' - it_behaves_like 'logs missing authentication abilities' + it_behaves_like 'returning', status: 403, message: 'access forbidden' + + context 'with enforce_abilities_check_for_dependency_proxy disabled' do + before do + stub_feature_flags(enforce_abilities_check_for_dependency_proxy: false) + end + + it_behaves_like 'returning a token with an encoded field', 'group_access_token' + end end end context 'with sufficient authentication abilities' do - let_it_be(:authentication_abilities) { Auth::DependencyProxyAuthenticationService::REQUIRED_ABILITIES } - let_it_be(:params) { { raw_token: token.token } } + [described_class::REQUIRED_USER_ABILITIES, + described_class::REQUIRED_CI_ABILITIES].each do |abilities| + context "with #{abilities}" do + let(:authentication_abilities) { abilities } + let(:params) { { raw_token: token.token } } - subject { service.execute(authentication_abilities: authentication_abilities) } + subject { service.execute(authentication_abilities: authentication_abilities) } - it_behaves_like 'returning a token with an encoded field', 'group_access_token' - it_behaves_like 'does not log missing authentication abilities' + it_behaves_like 'returning a token with an encoded field', 'group_access_token' + it_behaves_like 'does not log missing authentication abilities' - context 'revoked' do - before do - token.revoke! - end + context 'with enforce_abilities_check_for_dependency_proxy disabled' do + before do + stub_feature_flags(enforce_abilities_check_for_dependency_proxy: false) + end - it_behaves_like 'returning', status: 403, message: 'access forbidden' - end + it_behaves_like 'returning a token with an encoded field', 'group_access_token' + end - context 'expired' do - before do - token.update_column(:expires_at, 1.day.ago) - end + context 'revoked' do + before do + token.revoke! + end - it_behaves_like 'returning', status: 403, message: 'access forbidden' + it_behaves_like 'returning', status: 403, message: 'access forbidden' + end + + context 'expired' do + before do + token.update_column(:expires_at, 1.day.ago) + end + + it_behaves_like 'returning', status: 403, message: 'access forbidden' + end + end end end end @@ -183,6 +240,20 @@ end it_behaves_like 'returning a token with an encoded field', 'user_id' + + context 'packages_dependency_proxy_containers_scope_check disabled' do + before do + stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) + end + + context 'with enforce_abilities_check_for_dependency_proxy disabled' do + before do + stub_feature_flags(enforce_abilities_check_for_dependency_proxy: false) + end + + it_behaves_like 'returning a token with an encoded field', 'user_id' + end + end end end end