diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 3b509368a6b..66e3e6c860b 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -2441,31 +2441,6 @@ Gitlab/FeatureAvailableUsage: - 'lib/api/helpers/related_resources_helpers.rb' - 'spec/models/concerns/featurable_spec.rb' -# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/327490 -Style/RegexpLiteralMixedPreserve: - Exclude: - - 'qa/qa/page/project/settings/advanced.rb' - - 'qa/spec/service/docker_run/gitlab_runner_spec.rb' - - 'rubocop/cop/gitlab/duplicate_spec_location.rb' - - 'spec/features/clusters/cluster_health_dashboard_spec.rb' - - 'spec/features/markdown/metrics_spec.rb' - - 'spec/features/search/user_searches_for_code_spec.rb' - - 'spec/features/snippets/embedded_snippet_spec.rb' - - 'spec/helpers/diff_helper_spec.rb' - - 'spec/helpers/releases_helper_spec.rb' - - 'spec/lib/gitlab/ci/reports/test_case_spec.rb' - - 'spec/lib/gitlab/consul/internal_spec.rb' - - 'spec/lib/gitlab/import_export/shared_spec.rb' - - 'spec/lib/gitlab/utils/usage_data_spec.rb' - - 'spec/presenters/ci/build_runner_presenter_spec.rb' - - 'spec/requests/api/projects_spec.rb' - - 'spec/services/jira/requests/projects/list_service_spec.rb' - - 'spec/support/capybara.rb' - - 'spec/support/helpers/grafana_api_helpers.rb' - - 'spec/support/helpers/query_recorder.rb' - - 'spec/support/helpers/require_migration.rb' - - 'spec/views/layouts/_head.html.haml_spec.rb' - # WIP see: https://gitlab.com/gitlab-org/gitlab/-/issues/335808 Database/MultipleDatabases: Exclude: diff --git a/Gemfile b/Gemfile index d4126b7614f..d89e8f6adec 100644 --- a/Gemfile +++ b/Gemfile @@ -223,7 +223,7 @@ gem 're2', '~> 1.2.0' gem 'version_sorter', '~> 2.2.4' # Export Ruby Regex to Javascript -gem 'js_regex', '~> 3.4' +gem 'js_regex', '~> 3.7' # User agent parsing gem 'device_detector' @@ -523,7 +523,7 @@ gem 'valid_email', '~> 0.1' # JSON gem 'json', '~> 2.3.0' -gem 'json_schemer', '~> 0.2.12' +gem 'json_schemer', '~> 0.2.18' gem 'oj', '~> 3.10.6' gem 'multi_json', '~> 1.14.1' gem 'yajl-ruby', '~> 1.4.1', require: 'yajl' diff --git a/Gemfile.lock b/Gemfile.lock index 9c78186595d..02298bca3c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -179,7 +179,8 @@ GEM mime-types (>= 1.16) ssrf_filter (~> 1.0) cbor (0.5.9.6) - character_set (1.4.0) + character_set (1.4.1) + sorted_set (~> 1.0) charlock_holmes (0.7.7) chef-config (16.10.17) addressable @@ -307,8 +308,8 @@ GEM dry-inflector (~> 0.1, >= 0.1.2) dry-logic (~> 1.0, >= 1.0.2) e2mmap (0.1.0) - ecma-re-validator (0.2.1) - regexp_parser (~> 1.2) + ecma-re-validator (0.3.0) + regexp_parser (~> 2.0) ed25519 (1.2.4) elasticsearch (6.8.2) elasticsearch-api (= 6.8.2) @@ -604,7 +605,7 @@ GEM temple (>= 0.8.2) thor tilt - hana (1.3.6) + hana (1.3.7) hangouts-chat (0.0.5) hashdiff (1.0.1) hashie (4.1.0) @@ -652,19 +653,19 @@ GEM multipart-post oauth (~> 0.5, >= 0.5.0) jmespath (1.4.0) - js_regex (3.4.0) + js_regex (3.7.0) character_set (~> 1.4) - regexp_parser (~> 1.5) - regexp_property_values (~> 0.3) + regexp_parser (~> 2.1) + regexp_property_values (~> 1.0) json (2.3.0) json-jwt (1.13.0) activesupport (>= 4.2) aes_key_wrap bindata - json_schemer (0.2.12) - ecma-re-validator (~> 0.2) + json_schemer (0.2.18) + ecma-re-validator (~> 0.3) hana (~> 1.3) - regexp_parser (~> 1.5) + regexp_parser (~> 2.0) uri_template (~> 0.7) jsonpath (1.1.0) multi_json @@ -1010,6 +1011,7 @@ GEM ffi (>= 1.0.6) msgpack (>= 0.4.3) optimist (>= 3.0.0) + rbtree (0.4.4) rchardet (1.8.0) rdoc (6.3.2) re2 (1.2.0) @@ -1035,8 +1037,8 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.8.1) redis (>= 4, < 5) - regexp_parser (1.8.2) - regexp_property_values (0.3.5) + regexp_parser (2.1.1) + regexp_property_values (1.0.0) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -1174,6 +1176,7 @@ GEM rubyzip (>= 1.2.2) sentry-raven (3.1.2) faraday (>= 1.0) + set (1.0.1) settingslogic (2.0.9) sexp_processor (4.15.1) shellany (0.0.1) @@ -1218,6 +1221,9 @@ GEM thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) + sorted_set (1.0.3) + rbtree + set (~> 1.0) spamcheck (0.1.0) grpc (~> 1.0) spring (2.1.1) @@ -1509,9 +1515,9 @@ DEPENDENCIES invisible_captcha (~> 1.1.0) ipaddress (~> 0.8.3) jira-ruby (~> 2.1.4) - js_regex (~> 3.4) + js_regex (~> 3.7) json (~> 2.3.0) - json_schemer (~> 0.2.12) + json_schemer (~> 0.2.18) jwt (~> 2.1.0) kaminari (~> 1.0) kas-grpc (~> 0.0.2) diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js index a4e5df559ff..01e03ed437d 100644 --- a/app/assets/javascripts/pages/admin/groups/edit/index.js +++ b/app/assets/javascripts/pages/admin/groups/edit/index.js @@ -1,3 +1,3 @@ import initFilePickers from '~/file_pickers'; -document.addEventListener('DOMContentLoaded', initFilePickers); +initFilePickers(); diff --git a/app/assets/javascripts/pages/admin/serverless/domains/index.js b/app/assets/javascripts/pages/admin/serverless/domains/index.js index 5be466886a5..4fab7a1d9cb 100644 --- a/app/assets/javascripts/pages/admin/serverless/domains/index.js +++ b/app/assets/javascripts/pages/admin/serverless/domains/index.js @@ -1,19 +1,17 @@ import initSettingsPanels from '~/settings_panels'; -document.addEventListener('DOMContentLoaded', () => { - // Initialize expandable settings panels - initSettingsPanels(); +// Initialize expandable settings panels +initSettingsPanels(); - const domainCard = document.querySelector('.js-domain-cert-show'); - const domainForm = document.querySelector('.js-domain-cert-inputs'); - const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn'); - const domainSubmitButton = document.querySelector('.js-serverless-domain-submit'); +const domainCard = document.querySelector('.js-domain-cert-show'); +const domainForm = document.querySelector('.js-domain-cert-inputs'); +const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn'); +const domainSubmitButton = document.querySelector('.js-serverless-domain-submit'); - if (domainReplaceButton && domainCard && domainForm) { - domainReplaceButton.addEventListener('click', () => { - domainCard.classList.add('hidden'); - domainForm.classList.remove('hidden'); - domainSubmitButton.removeAttribute('disabled'); - }); - } -}); +if (domainReplaceButton && domainCard && domainForm) { + domainReplaceButton.addEventListener('click', () => { + domainCard.classList.add('hidden'); + domainForm.classList.remove('hidden'); + domainSubmitButton.removeAttribute('disabled'); + }); +} diff --git a/app/assets/javascripts/pages/groups/settings/badges/index.js b/app/assets/javascripts/pages/groups/settings/badges/index.js index 3f48e4f281e..9dcea737d51 100644 --- a/app/assets/javascripts/pages/groups/settings/badges/index.js +++ b/app/assets/javascripts/pages/groups/settings/badges/index.js @@ -5,6 +5,4 @@ import Translate from '~/vue_shared/translate'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => { - mountBadgeSettings(GROUP_BADGE); -}); +mountBadgeSettings(GROUP_BADGE); diff --git a/app/assets/javascripts/pages/import/github/status/index.js b/app/assets/javascripts/pages/import/github/status/index.js index 98ddb8b3aa4..4c427b72372 100644 --- a/app/assets/javascripts/pages/import/github/status/index.js +++ b/app/assets/javascripts/pages/import/github/status/index.js @@ -1,7 +1,5 @@ import mountImportProjectsTable from '~/import_entities/import_projects'; -document.addEventListener('DOMContentLoaded', () => { - const mountElement = document.getElementById('import-projects-mount-element'); +const mountElement = document.getElementById('import-projects-mount-element'); - mountImportProjectsTable(mountElement); -}); +mountImportProjectsTable(mountElement); diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js index 549e596cb8d..5edaa7f7e51 100644 --- a/app/assets/javascripts/pages/projects/compare/show/index.js +++ b/app/assets/javascripts/pages/projects/compare/show/index.js @@ -5,9 +5,7 @@ import initCompareSelector from '~/projects/compare'; initCompareSelector(); -document.addEventListener('DOMContentLoaded', () => { - new Diff(); // eslint-disable-line no-new - const paddingTop = 16; - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); - GpgBadges.fetch(); -}); +new Diff(); // eslint-disable-line no-new +const paddingTop = 16; +initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); +GpgBadges.fetch(); diff --git a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js index 255d05b39be..bef21ef8fdf 100644 --- a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js +++ b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js @@ -1,3 +1,3 @@ import initCycleAnalytics from '~/cycle_analytics'; -document.addEventListener('DOMContentLoaded', initCycleAnalytics); +initCycleAnalytics(); diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js index 09d9c78c446..4f5a5bfe6fe 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/index.js +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -1,3 +1,3 @@ import initContributorsGraphs from '~/contributors'; -document.addEventListener('DOMContentLoaded', initContributorsGraphs); +initContributorsGraphs(); diff --git a/app/assets/javascripts/pages/projects/import/jira/index.js b/app/assets/javascripts/pages/projects/import/jira/index.js index cb7a7bde55d..5876e5283b5 100644 --- a/app/assets/javascripts/pages/projects/import/jira/index.js +++ b/app/assets/javascripts/pages/projects/import/jira/index.js @@ -1,3 +1,3 @@ import mountJiraImportApp from '~/jira_import'; -document.addEventListener('DOMContentLoaded', mountJiraImportApp); +mountJiraImportApp(); diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js index aecc6484b26..48afd2142ee 100644 --- a/app/assets/javascripts/pages/projects/issues/edit/index.js +++ b/app/assets/javascripts/pages/projects/issues/edit/index.js @@ -1,3 +1,3 @@ import initForm from 'ee_else_ce/pages/projects/issues/form'; -document.addEventListener('DOMContentLoaded', initForm); +initForm(); diff --git a/app/assets/javascripts/pages/projects/jobs/terminal/index.js b/app/assets/javascripts/pages/projects/jobs/terminal/index.js index 7129e24cee1..d42c163a41b 100644 --- a/app/assets/javascripts/pages/projects/jobs/terminal/index.js +++ b/app/assets/javascripts/pages/projects/jobs/terminal/index.js @@ -1,3 +1,3 @@ import initTerminal from '~/terminal/'; -document.addEventListener('DOMContentLoaded', initTerminal); +initTerminal(); diff --git a/app/assets/javascripts/pages/projects/pages_domains/new/index.js b/app/assets/javascripts/pages/projects/pages_domains/new/index.js index 27e4433ad4d..17fa49a46e0 100644 --- a/app/assets/javascripts/pages/projects/pages_domains/new/index.js +++ b/app/assets/javascripts/pages/projects/pages_domains/new/index.js @@ -1,3 +1,3 @@ import initForm from '~/pages/projects/pages_domains/form'; -document.addEventListener('DOMContentLoaded', initForm); +initForm(); diff --git a/app/assets/javascripts/pages/projects/pages_domains/show/index.js b/app/assets/javascripts/pages/projects/pages_domains/show/index.js index 27e4433ad4d..17fa49a46e0 100644 --- a/app/assets/javascripts/pages/projects/pages_domains/show/index.js +++ b/app/assets/javascripts/pages/projects/pages_domains/show/index.js @@ -1,3 +1,3 @@ import initForm from '~/pages/projects/pages_domains/form'; -document.addEventListener('DOMContentLoaded', initForm); +initForm(); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js index 40730ec7e60..cd4bc35e74e 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue'; -document.addEventListener('DOMContentLoaded', () => { +function initPipelineSchedules() { const el = document.getElementById('pipeline-schedules-callout'); if (!el) { @@ -21,4 +21,6 @@ document.addEventListener('DOMContentLoaded', () => { return createElement(PipelineSchedulesCallout); }, }); -}); +} + +initPipelineSchedules(); diff --git a/app/controllers/concerns/dependency_proxy/auth.rb b/app/controllers/concerns/dependency_proxy/auth.rb deleted file mode 100644 index 1276feedba6..00000000000 --- a/app/controllers/concerns/dependency_proxy/auth.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module DependencyProxy - module Auth - extend ActiveSupport::Concern - - included do - # We disable `authenticate_user!` since the `DependencyProxy::Auth` performs auth using JWT token - skip_before_action :authenticate_user!, raise: false - prepend_before_action :authenticate_user_from_jwt_token! - end - - def authenticate_user_from_jwt_token! - return unless dependency_proxy_for_private_groups? - - authenticate_with_http_token do |token, _| - user = user_from_token(token) - sign_in(user) if user - end - - request_bearer_token! unless current_user - end - - private - - def dependency_proxy_for_private_groups? - Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) - 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 - - def user_from_token(token) - token_payload = DependencyProxy::AuthTokenService.decoded_token_payload(token) - User.find(token_payload['user_id']) - rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature - nil - end - end -end diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb index 2a923d02752..07aca72b22f 100644 --- a/app/controllers/concerns/dependency_proxy/group_access.rb +++ b/app/controllers/concerns/dependency_proxy/group_access.rb @@ -12,15 +12,15 @@ module DependencyProxy private def verify_dependency_proxy_enabled! - render_404 unless group.dependency_proxy_feature_available? + render_404 unless group&.dependency_proxy_feature_available? end def authorize_read_dependency_proxy! - access_denied! unless can?(current_user, :read_dependency_proxy, group) + access_denied! unless can?(auth_user, :read_dependency_proxy, group) end def authorize_admin_dependency_proxy! - access_denied! unless can?(current_user, :admin_dependency_proxy, group) + access_denied! unless can?(auth_user, :admin_dependency_proxy, group) end end end diff --git a/app/controllers/groups/dependency_proxies_controller.rb b/app/controllers/groups/dependency_proxies_controller.rb index b896b240daf..b037aa52939 100644 --- a/app/controllers/groups/dependency_proxies_controller.rb +++ b/app/controllers/groups/dependency_proxies_controller.rb @@ -2,7 +2,7 @@ module Groups class DependencyProxiesController < Groups::ApplicationController - include DependencyProxy::GroupAccess + include ::DependencyProxy::GroupAccess before_action :authorize_admin_dependency_proxy!, only: :update before_action :dependency_proxy diff --git a/app/controllers/groups/dependency_proxy/application_controller.rb b/app/controllers/groups/dependency_proxy/application_controller.rb new file mode 100644 index 00000000000..c6484ffb5f1 --- /dev/null +++ b/app/controllers/groups/dependency_proxy/application_controller.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Groups + module DependencyProxy + class ApplicationController < ::ApplicationController + EMPTY_AUTH_RESULT = Gitlab::Auth::Result.new(nil, nil, nil, nil).freeze + + delegate :actor, to: :@authentication_result, allow_nil: true + + # This allows auth_user to be set in the base ApplicationController + alias_method :authenticated_user, :actor + + # We disable `authenticate_user!` since the `DependencyProxy::ApplicationController` performs auth using JWT token + skip_before_action :authenticate_user!, raise: false + + prepend_before_action :authenticate_user_from_jwt_token! + + def authenticate_user_from_jwt_token! + return unless dependency_proxy_for_private_groups? + + if Feature.enabled?(:dependency_proxy_deploy_tokens) + authenticate_with_http_token do |token, _| + @authentication_result = EMPTY_AUTH_RESULT + + found_user = user_from_token(token) + sign_in(found_user) if found_user.is_a?(User) + end + + request_bearer_token! unless authenticated_user + else + authenticate_with_http_token do |token, _| + user = user_from_token(token) + sign_in(user) if user + end + + request_bearer_token! unless current_user + end + end + + private + + def dependency_proxy_for_private_groups? + Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) + 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 + + def user_from_token(token) + token_payload = ::DependencyProxy::AuthTokenService.decoded_token_payload(token) + return User.find(token_payload['user_id']) unless Feature.enabled?(:dependency_proxy_deploy_tokens) + + if token_payload['user_id'] + token_user = User.find(token_payload['user_id']) + return unless token_user + + @authentication_result = Gitlab::Auth::Result.new(token_user, nil, :user, []) + return token_user + elsif token_payload['deploy_token'] + deploy_token = DeployToken.active.find_by_token(token_payload['deploy_token']) + return unless deploy_token + + @authentication_result = Gitlab::Auth::Result.new(deploy_token, nil, :deploy_token, []) + return deploy_token + end + + nil + rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature + nil + end + end + end +end diff --git a/app/controllers/groups/dependency_proxy_auth_controller.rb b/app/controllers/groups/dependency_proxy_auth_controller.rb index e3e9bd88e24..60b2371fa9a 100644 --- a/app/controllers/groups/dependency_proxy_auth_controller.rb +++ b/app/controllers/groups/dependency_proxy_auth_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -class Groups::DependencyProxyAuthController < ApplicationController - include DependencyProxy::Auth - +class Groups::DependencyProxyAuthController < ::Groups::DependencyProxy::ApplicationController feature_category :dependency_proxy def authenticate diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb index e341b9d75e0..f7dc552bd3e 100644 --- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb +++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true -class Groups::DependencyProxyForContainersController < Groups::ApplicationController - include DependencyProxy::Auth +class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy::ApplicationController + include Gitlab::Utils::StrongMemoize include DependencyProxy::GroupAccess include SendFileUpload include ::PackagesHelper # for event tracking + before_action :ensure_group before_action :ensure_token_granted! before_action :ensure_feature_enabled! @@ -24,7 +25,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro content_type = result[:manifest].content_type event_name = tracking_event_name(object_type: :manifest, from_cache: result[:from_cache]) - track_package_event(event_name, :dependency_proxy, namespace: group, user: current_user) + track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user) send_upload( result[:manifest].file, proxy: true, @@ -42,7 +43,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro if result[:status] == :success event_name = tracking_event_name(object_type: :blob, from_cache: result[:from_cache]) - track_package_event(event_name, :dependency_proxy, namespace: group, user: current_user) + track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user) send_upload(result[:blob].file) else head result[:http_status] @@ -51,6 +52,12 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro private + def group + strong_memoize(:group) do + Group.find_by_full_path(params[:group_id], follow_redirects: request.get?) + end + end + def image params[:image] end @@ -71,6 +78,10 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro group.dependency_proxy_setting || group.create_dependency_proxy_setting end + def ensure_group + render_404 unless group + end + def ensure_feature_enabled! render_404 unless dependency_proxy.enabled end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 0d5d63371fc..537d18646c5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -128,7 +128,6 @@ module Ci end scope :eager_load_job_artifacts, -> { includes(:job_artifacts) } - scope :eager_load_job_artifacts_archive, -> { includes(:job_artifacts_archive) } scope :eager_load_tags, -> { includes(:tags) } scope :eager_load_everything, -> do diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 5fa9f2ef9f9..326d3fb8470 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -10,6 +10,7 @@ class DeployToken < ApplicationRecord AVAILABLE_SCOPES = %i(read_repository read_registry write_registry read_package_registry write_package_registry).freeze GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token' + REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze default_value_for(:expires_at) { Forever.date } @@ -46,6 +47,12 @@ class DeployToken < ApplicationRecord active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME) end + def valid_for_dependency_proxy? + group_type? && + active? && + REQUIRED_DEPENDENCY_PROXY_SCOPES.all? { |scope| scope.in?(scopes) } + end + def revoke! update!(revoked: true) end @@ -73,6 +80,14 @@ class DeployToken < ApplicationRecord holder.has_access_to?(requested_project) end + def has_access_to_group?(requested_group) + return false unless active? + return false unless group_type? + return false unless holder + + holder.has_access_to_group?(requested_group) + end + # This is temporal. Currently we limit DeployToken # to a single project or group, later we're going to # extend that to be for multiple projects and namespaces. diff --git a/app/models/group_deploy_token.rb b/app/models/group_deploy_token.rb index 084a8672460..d9667e7c74d 100644 --- a/app/models/group_deploy_token.rb +++ b/app/models/group_deploy_token.rb @@ -11,9 +11,14 @@ class GroupDeployToken < ApplicationRecord def has_access_to?(requested_project) requested_project_group = requested_project&.group return false unless requested_project_group - return true if requested_project_group.id == group_id - requested_project_group + has_access_to_group?(requested_project_group) + end + + def has_access_to_group?(requested_group) + return true if requested_group.id == group_id + + requested_group .ancestors .where(id: group_id) .exists? diff --git a/app/models/label.rb b/app/models/label.rb index 1a07620f944..a46d6bc5c0f 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -9,10 +9,6 @@ class Label < ApplicationRecord include Sortable include FromUnion include Presentable - include IgnorableColumns - - # TODO: Project#create_labels can remove column exception when this column is dropped from all envs - ignore_column :remove_on_close, remove_with: '14.1', remove_after: '2021-06-22' cache_markdown_field :description, pipeline: :single_line diff --git a/app/models/project.rb b/app/models/project.rb index 5cad761ce2c..0d32138b08c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1427,8 +1427,7 @@ class Project < ApplicationRecord # rubocop: disable CodeReuse/ServiceClass def create_labels Label.templates.each do |label| - # TODO: remove_on_close exception can be removed after the column is dropped from all envs - params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type', 'remove_on_close') + params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type') Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true) end end diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index 8aeeae1330c..8c3b85ac4c3 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -20,7 +20,6 @@ module Terraform foreign_key: :terraform_state_id, inverse_of: :terraform_state - scope :versioning_not_enabled, -> { where(versioning_enabled: false) } scope :ordered_by_name, -> { order(:name) } scope :with_name, -> (name) { where(name: name) } diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 7e07cd12ede..fa7834c10df 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -50,6 +50,14 @@ class GroupPolicy < BasePolicy @subject.dependency_proxy_feature_available? end + condition(:dependency_proxy_access_allowed) do + if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) + access_level >= GroupMember::REPORTER || valid_dependency_proxy_deploy_token + else + can?(:read_group) + end + end + desc "Deploy token with read_package_registry scope" condition(:read_package_registry_deploy_token) do @user.is_a?(DeployToken) && @user.groups.include?(@subject) && @user.read_package_registry @@ -212,7 +220,7 @@ class GroupPolicy < BasePolicy enable :read_group end - rule { can?(:read_group) & dependency_proxy_available } + rule { dependency_proxy_access_allowed & dependency_proxy_available } .enable :read_dependency_proxy rule { developer & dependency_proxy_available } @@ -260,6 +268,10 @@ class GroupPolicy < BasePolicy def resource_access_token_creation_allowed? resource_access_token_feature_available? && group.root_ancestor.namespace_settings.resource_access_token_creation_allowed? end + + def valid_dependency_proxy_deploy_token + @user.is_a?(DeployToken) && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject) + end end GroupPolicy.prepend_mod_with('GroupPolicy') diff --git a/app/services/auth/dependency_proxy_authentication_service.rb b/app/services/auth/dependency_proxy_authentication_service.rb index 4335fb0bd06..164594d6f6c 100644 --- a/app/services/auth/dependency_proxy_authentication_service.rb +++ b/app/services/auth/dependency_proxy_authentication_service.rb @@ -8,10 +8,7 @@ module Auth def execute(authentication_abilities:) return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled - - # Because app/controllers/concerns/dependency_proxy/auth.rb consumes this - # JWT only as `User.find`, we currently only allow User (not DeployToken, etc) - return error('access forbidden', 403) unless current_user + return error('access forbidden', 403) unless valid_user_actor? { token: authorized_token.encoded } end @@ -36,11 +33,24 @@ module Auth private + def valid_user_actor? + current_user || valid_deploy_token? + end + + def valid_deploy_token? + deploy_token && deploy_token.valid_for_dependency_proxy? + end + def authorized_token JSONWebToken::HMACToken.new(self.class.secret).tap do |token| - token['user_id'] = current_user.id + token['user_id'] = current_user.id if current_user + token['deploy_token'] = deploy_token.token if deploy_token token.expire_time = self.class.token_expire_at end end + + def deploy_token + params[:deploy_token] + end end end diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 4bf47c3d60d..b6266c3ea34 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -9,6 +9,7 @@ = f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold' = f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control' = render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f + = render_if_exists 'admin/application_settings/default_delayed_project_deletion_setting', form: f = render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f .form-group.visibility-level-setting = f.label :default_project_visibility, class: 'label-bold' diff --git a/app/views/layouts/nav/sidebar/_group_menus.html.haml b/app/views/layouts/nav/sidebar/_group_menus.html.haml index 42114287cdf..4f171f2777a 100644 --- a/app/views/layouts/nav/sidebar/_group_menus.html.haml +++ b/app/views/layouts/nav/sidebar/_group_menus.html.haml @@ -1,16 +1,3 @@ -- if group_sidebar_link?(:kubernetes) - = nav_link(controller: [:clusters]) do - = link_to group_clusters_path(@group) do - .nav-icon-container - = sprite_icon('cloud-gear') - %span.nav-item-name - = _('Kubernetes') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do - = link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do - %strong.fly-out-top-item-name - = _('Kubernetes') - = render 'groups/sidebar/packages' = render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user) diff --git a/config/feature_flags/development/dast_runner_site_validation.yml b/config/feature_flags/development/dast_runner_site_validation.yml index f8ad90062f6..e39a8a6d1e3 100644 --- a/config/feature_flags/development/dast_runner_site_validation.yml +++ b/config/feature_flags/development/dast_runner_site_validation.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331082 milestone: '14.0' type: development group: group::dynamic analysis -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/dependency_proxy_deploy_tokens.yml b/config/feature_flags/development/dependency_proxy_deploy_tokens.yml new file mode 100644 index 00000000000..f3cb1fc2c18 --- /dev/null +++ b/config/feature_flags/development/dependency_proxy_deploy_tokens.yml @@ -0,0 +1,8 @@ +--- +name: dependency_proxy_deploy_tokens +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64363 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334565 +milestone: '14.2' +type: development +group: group::package +default_enabled: false diff --git a/doc/api/settings.md b/doc/api/settings.md index e3366cf176c..d481d336c4c 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -96,7 +96,7 @@ Example response: ``` Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see -the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters: +the `file_template_project_id`, `delayed_project_deletion`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters: ```json { @@ -104,6 +104,7 @@ the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_al "signup_enabled" : true, "file_template_project_id": 1, "geo_node_allowed_ips": "0.0.0.0/0, ::/0", + "delayed_project_deletion": false, "deletion_adjourned_period": 7, ... } @@ -200,6 +201,7 @@ these parameters: - `file_template_project_id` - `geo_node_allowed_ips` - `geo_status_timeout` +- `delayed_project_delection` - `deletion_adjourned_period` Example responses: **(PREMIUM SELF)** @@ -250,6 +252,7 @@ listed in the descriptions of the relevant settings. | `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. | | `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | +| `delayed_project_deletion` | boolean | no | **(PREMIUM SELF)** Enable delayed project deletion by default in new groups. Default is `false`. | | `deletion_adjourned_period` | integer | no | **(PREMIUM SELF)** The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90. | `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../user/admin_area/diff_limits.md), in bytes. | | `diff_max_files` | integer | no | Maximum [files in a diff](../user/admin_area/diff_limits.md). | diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md index 4af33c133a4..7474d9f8821 100644 --- a/doc/user/admin_area/settings/visibility_and_access_controls.md +++ b/doc/user/admin_area/settings/visibility_and_access_controls.md @@ -71,6 +71,18 @@ To ensure only Administrator users can delete projects: 1. Check the **Default project deletion protection** checkbox. 1. Click **Save changes**. +## Default delayed project deletion **(PREMIUM SELF)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255449) in GitLab 14.2. + +Projects in a group (but not a personal namespace) can be deleted after a delayed period, by +[configuring in Group Settings](../../group/index.md#enable-delayed-project-removal). + +To enable delayed project deletion by default in new groups: + +1. Check the **Default delayed project deletion** checkbox. +1. Click **Save changes**. + ## Default deletion delay **(PREMIUM SELF)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6. diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 93ece70655e..500ec8208bf 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -1063,6 +1063,9 @@ follows: Both methods are equivalent in functionality. Use whichever is feasible. +In [GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/324990), site profile +validation happens in a CI job using the [GitLab Runner](../../../ci/runners/index.md). + #### Create a site profile To create a site profile: diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index e2957aff756..dd7ad7d4f8d 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -89,6 +89,7 @@ You can authenticate using: - Your GitLab username and password. - A [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `read_registry` and `write_registry`. +- A [group deploy token](../../../user/project/deploy_tokens/index.md#group-deploy-token) with the scope set to `read_registry` and `write_registry`. #### Authenticate within CI/CD @@ -123,7 +124,7 @@ Proxy manually without including the port: docker pull gitlab.example.com:443/my-group/dependency_proxy/containers/alpine:latest ``` -You can also use [custom CI/CD variables](../../../ci/variables/index.md#custom-cicd-variables) to store and access your personal access token or other valid credentials. +You can also use [custom CI/CD variables](../../../ci/variables/index.md#custom-cicd-variables) to store and access your personal access token or deploy token. ### Store a Docker image in Dependency Proxy cache diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index 5e045ee2455..34f7a1aee92 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -94,3 +94,48 @@ the following table. You may enable or disable project access token creation for all projects in a group in **Group > Settings > General > Permissions, LFS, 2FA > Allow project access token creation**. Even when creation is disabled, you can still use and revoke existing project access tokens. This setting is available only on top-level groups. + +## Group access token workaround **(FREE SELF)** + +NOTE: +This section describes a workaround and is subject to change. + +Group access tokens let you use a single token to: + +- Perform actions at the group level. +- Manage the projects within the group. + +We don't support group access tokens in the GitLab UI, though GitLab self-managed +administrators can create them using the [Rails console](../../../administration/operations/rails_console.md). + +