From b72c0274e93889dda975b752b59a088056e3e404 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 4 Jul 2024 12:24:02 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rules.gitlab-ci.yml | 3 +- .gitlab/ci/test-on-gdk/main.gitlab-ci.yml | 1 + .gitleaksignore | 3 +- .rubocop_todo/style/lambda.yml | 68 ------ GITLAB_KAS_VERSION | 2 +- GITLAB_PAGES_VERSION | 2 +- .../components/blob_content_viewer.vue | 2 +- .../repository/components/table/row.vue | 2 +- .../concerns/project_unauthorized.rb | 2 +- .../profiles/two_factor_auths_controller.rb | 6 +- .../repository/blob_info.query.graphql | 4 +- app/models/concerns/featurable.rb | 2 +- app/models/project_feature.rb | 2 +- .../daily_build_group_report_result_entity.rb | 2 +- app/serializers/group_child_entity.rb | 18 +- .../issuable_sidebar_basic_entity.rb | 2 +- .../merge_request_sidebar_basic_entity.rb | 2 +- .../agnostic_token_revocation_service.rb | 128 +++++++++++ .../groups/deploy_tokens/revoke_service.rb | 2 +- .../merge_strategies/from_source_branch.rb | 31 +-- .../personal_access_tokens/revoke_service.rb | 12 +- .../members_approval_request_metadata.json | 16 +- config/application.rb | 4 +- .../beta/group_agnostic_token_revocation.yml | 9 + config/initializers/0_license.rb | 2 +- config/initializers/action_cable.rb | 2 +- config/initializers/gitlab_experiment.rb | 4 +- config/initializers/lograge.rb | 2 +- config/routes.rb | 2 +- config/routes/dashboard.rb | 2 +- config/routes/group.rb | 2 +- config/routes/issues.rb | 2 +- ...ion_configs_on_partition_id_pipeline_id.rb | 41 ++++ ...builds_execution_configs_on_pipeline_id.rb | 39 ++++ ...lumns_for_merge_requests_head_pipelines.rb | 43 ++++ db/schema_migrations/20240701081515 | 1 + db/schema_migrations/20240701090730 | 1 + db/schema_migrations/20240702081839 | 1 + db/structure.sql | 6 +- .../configure_duo_features.md | 12 +- .../install_infrastructure.md | 2 +- doc/api/groups.md | 66 ++++++ .../blueprints/custom_models/index.md | 10 +- doc/ci/jobs/job_artifacts.md | 2 +- doc/ci/variables/index.md | 8 +- doc/development/ai_features/glossary.md | 2 +- doc/user/group/custom_project_templates.md | 6 + doc/user/project/import/github.md | 8 + lib/api/entities/label.rb | 2 +- lib/api/entities/project.rb | 4 +- lib/api/entities/project_export_status.rb | 2 +- lib/api/groups.rb | 42 ++++ .../types/comma_separated_to_array.rb | 2 +- .../types/comma_separated_to_integer_array.rb | 2 +- .../types/hash_of_integer_values.rb | 2 +- .../validators/check_assignees_count.rb | 2 +- .../filter/ascii_doc_sanitization_filter.rb | 4 +- lib/banzai/filter/base_sanitization_filter.rb | 2 +- lib/banzai/filter/sanitization_filter.rb | 6 +- .../action_cable/request_store_callbacks.rb | 2 +- lib/gitlab/checks/diff_check.rb | 2 +- .../load_balancing/action_cable_callbacks.rb | 2 +- .../rack_multipart_tempfile_factory.rb | 2 +- lib/gitlab/rack_attack.rb | 2 +- lib/gitlab/sidekiq_config/worker_matcher.rb | 12 +- lib/gitlab/sidekiq_middleware.rb | 4 +- lib/gitlab/utils/usage_data.rb | 2 +- locale/gitlab.pot | 72 +++---- qa/Gemfile | 2 +- qa/Gemfile.lock | 6 +- .../cop/inject_enterprise_edition_module.rb | 2 +- scripts/generate-e2e-pipeline | 1 - .../concerns/routable_actions_spec.rb | 2 +- spec/db/schema_spec.rb | 1 + spec/factories/deploy_tokens.rb | 1 - spec/factories/group_deploy_tokens.rb | 2 +- .../dependency_proxy_for_containers_spec.rb | 2 +- ...closes_reopens_merge_request_state_spec.rb | 4 +- spec/graphql/types/base_object_spec.rb | 2 +- .../request_store_callbacks_spec.rb | 2 +- .../class_methods_spec.rb | 2 +- .../action_cable_callbacks_spec.rb | 4 +- .../rack_multipart_tempfile_factory_spec.rb | 2 +- spec/lib/gitlab/path_regex_spec.rb | 4 +- .../members/members/member_approval_spec.rb | 8 +- spec/requests/api/groups_spec.rb | 113 ++++++++++ .../agnostic_token_revocation_service_spec.rb | 202 ++++++++++++++++++ .../groups/autocomplete_service_spec.rb | 2 +- .../referenced_merge_requests_service_spec.rb | 2 +- .../projects/autocomplete_service_spec.rb | 2 +- .../lfs_download_link_list_service_spec.rb | 2 +- spec/support/helpers/email_helpers.rb | 2 +- .../helpers/reference_parser_helpers.rb | 4 +- ...ad_only_gitlab_instance_shared_examples.rb | 4 +- 94 files changed, 890 insertions(+), 251 deletions(-) delete mode 100644 .rubocop_todo/style/lambda.yml create mode 100644 app/services/groups/agnostic_token_revocation_service.rb create mode 100644 config/feature_flags/beta/group_agnostic_token_revocation.yml create mode 100644 db/post_migrate/20240701081515_fk_to_ci_pipelines_from_p_ci_builds_execution_configs_on_partition_id_pipeline_id.rb create mode 100644 db/post_migrate/20240701090730_remove_fk_to_ci_pipelines_p_ci_builds_execution_configs_on_pipeline_id.rb create mode 100644 db/post_migrate/20240702081839_swap_head_pipeline_columns_for_merge_requests_head_pipelines.rb create mode 100644 db/schema_migrations/20240701081515 create mode 100644 db/schema_migrations/20240701090730 create mode 100644 db/schema_migrations/20240702081839 create mode 100644 spec/services/groups/agnostic_token_revocation_service_spec.rb diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 881bbcff6c8..4ffd1e9a448 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1748,8 +1748,7 @@ KNAPSACK_GENERATE_REPORT: "true" QA_SAVE_TEST_METRICS: "true" QA_EXPORT_TEST_METRICS: "false" - COVERBAND_ENABLED: "true" # Record Coverage in scheduled master pipelines - + COVERBAND_ENABLED: "true" .qa:rules:e2e:test-on-cng: rules: diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml index 2d3007f3fe8..d7deed4ef25 100644 --- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml +++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml @@ -70,6 +70,7 @@ include: GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN FF_NETWORK_PER_BUILD: "true" RSPEC_LAST_RUN_RESULTS_FILE: "$CI_PROJECT_DIR/qa/tmp/examples.txt" + COVERBAND_ENABLED: "$COVERBAND_ENABLED" before_script: - echo "SUITE_RAN=true" > "$QA_SUITE_STATUS_ENV_FILE" - echo -e "\e[0Ksection_start:`date +%s`:install_gems[collapsed=true]\r\e[0KInstall gems" diff --git a/.gitleaksignore b/.gitleaksignore index 8d44c5eccc1..306bed4adbd 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -10,4 +10,5 @@ spec/frontend/lib/utils/secret_detection_spec.js:generic-api-key:40 spec/frontend/lib/utils/secret_detection_spec.js:generic-api-key:41 spec/frontend/lib/utils/secret_detection_spec.js:generic-api-key:55 spec/frontend/lib/utils/secret_detection_spec.js:generic-api-key:57 -spec/frontend/lib/utils/secret_detection_spec.js:gitlab-pat:28 \ No newline at end of file +spec/frontend/lib/utils/secret_detection_spec.js:gitlab-pat:28 +d39da82ae304b8813a8fb2c79c3d7cd6f173590e:doc/api/groups.md:gitlab-pat:2342 \ No newline at end of file diff --git a/.rubocop_todo/style/lambda.yml b/.rubocop_todo/style/lambda.yml deleted file mode 100644 index 462cb49c567..00000000000 --- a/.rubocop_todo/style/lambda.yml +++ /dev/null @@ -1,68 +0,0 @@ ---- -# Cop supports --autocorrect. -Style/Lambda: - Exclude: - - 'app/controllers/concerns/project_unauthorized.rb' - - 'app/controllers/profiles/two_factor_auths_controller.rb' - - 'app/models/concerns/featurable.rb' - - 'app/models/project_feature.rb' - - 'app/serializers/ci/daily_build_group_report_result_entity.rb' - - 'app/serializers/group_child_entity.rb' - - 'app/serializers/issuable_sidebar_basic_entity.rb' - - 'app/serializers/merge_request_sidebar_basic_entity.rb' - - 'config/application.rb' - - 'config/initializers/0_license.rb' - - 'config/initializers/action_cable.rb' - - 'config/initializers/gitlab_experiment.rb' - - 'config/initializers/lograge.rb' - - 'config/routes.rb' - - 'config/routes/dashboard.rb' - - 'config/routes/group.rb' - - 'config/routes/issues.rb' - - 'ee/app/controllers/concerns/ee/routable_actions/sso_enforcement_redirect.rb' - - 'ee/app/serializers/ee/group_child_entity.rb' - - 'ee/app/services/ee/issues/export_csv_service.rb' - - 'ee/lib/ee/api/entities/group_detail.rb' - - 'ee/lib/ee/api/entities/group_push_rule.rb' - - 'ee/lib/ee/gitlab/checks/diff_check.rb' - - 'ee/lib/gem_extensions/elasticsearch/model/adapter/active_record/importing.rb' - - 'ee/spec/elastic_integration/global_search_spec.rb' - - 'ee/spec/lib/gitlab/geo/event_gap_tracking_spec.rb' - - 'ee/spec/services/ee/groups/autocomplete_service_spec.rb' - - 'ee/spec/support/helpers/elasticsearch_helpers.rb' - - 'ee/spec/support/shared_examples/lib/gitlab/middleware/maintenance_mode_gitlab_ee_instance_shared_examples.rb' - - 'ee/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_ee_instance_shared_examples.rb' - - 'lib/api/entities/label.rb' - - 'lib/api/entities/project.rb' - - 'lib/api/entities/project_export_status.rb' - - 'lib/api/validations/types/comma_separated_to_array.rb' - - 'lib/api/validations/types/comma_separated_to_integer_array.rb' - - 'lib/api/validations/types/hash_of_integer_values.rb' - - 'lib/api/validations/validators/check_assignees_count.rb' - - 'lib/banzai/filter/ascii_doc_sanitization_filter.rb' - - 'lib/banzai/filter/base_sanitization_filter.rb' - - 'lib/banzai/filter/sanitization_filter.rb' - - 'lib/gitlab/action_cable/request_store_callbacks.rb' - - 'lib/gitlab/checks/diff_check.rb' - - 'lib/gitlab/database/load_balancing/action_cable_callbacks.rb' - - 'lib/gitlab/middleware/rack_multipart_tempfile_factory.rb' - - 'lib/gitlab/rack_attack.rb' - - 'lib/gitlab/sidekiq_config/worker_matcher.rb' - - 'lib/gitlab/sidekiq_middleware.rb' - - 'lib/gitlab/utils/usage_data.rb' - - 'rubocop/cop/inject_enterprise_edition_module.rb' - - 'spec/controllers/concerns/routable_actions_spec.rb' - - 'spec/features/groups/dependency_proxy_for_containers_spec.rb' - - 'spec/graphql/types/base_object_spec.rb' - - 'spec/lib/gitlab/action_cable/request_store_callbacks_spec.rb' - - 'spec/lib/gitlab/cross_project_access/class_methods_spec.rb' - - 'spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb' - - 'spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb' - - 'spec/lib/gitlab/path_regex_spec.rb' - - 'spec/services/groups/autocomplete_service_spec.rb' - - 'spec/services/issues/referenced_merge_requests_service_spec.rb' - - 'spec/services/projects/autocomplete_service_spec.rb' - - 'spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb' - - 'spec/support/helpers/email_helpers.rb' - - 'spec/support/helpers/reference_parser_helpers.rb' - - 'spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb' diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index badb307dc9e..9bee49bfad9 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -v17.1.1 +v17.2.0-rc1 diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index c9e3694ea32..6d1a359f7a3 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -df16e199eba9d1fe851e42b7842bab4bb6215ae2 +2f8c46ee159b96b2c50b43e11654a3864654b05c diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 4160f1893db..4102036d741 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -59,7 +59,7 @@ export default { variables() { const queryVariables = { projectPath: this.projectPath, - filePath: this.path, + filePath: [this.path], ref: this.currentRef, refType: this.refType?.toUpperCase() || null, shouldFetchRawText: true, diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index bc6101c134c..5d953c7180c 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -174,7 +174,7 @@ export default { loadBlob() { this.apolloQuery(blobInfoQuery, { projectPath: this.projectPath, - filePath: this.path, + filePath: [this.path], ref: this.ref, refType: this.refType?.toUpperCase() || null, shouldFetchRawText: true, diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb index 563d6b6273b..41b26ed57e6 100644 --- a/app/controllers/concerns/project_unauthorized.rb +++ b/app/controllers/concerns/project_unauthorized.rb @@ -3,7 +3,7 @@ module ProjectUnauthorized module ControllerActions def self.on_routable_not_found - lambda do |routable, full_path| + ->(routable, full_path) do return unless routable.is_a?(Project) label = routable.external_authorization_classification_label diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 1b506c09e16..20d7ffa7103 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -204,14 +204,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def setup_show_page if two_factor_authentication_required? && !current_user.two_factor_enabled? two_factor_auth_actions = { - global: lambda do |_| + global: ->(_) do flash.now[:alert] = _('The global settings require you to enable Two-Factor Authentication for your account.') end, - admin_2fa: lambda do |_| + admin_2fa: ->(_) do flash.now[:alert] = _('Administrator users are required to enable Two-Factor Authentication for their account.') end, - group: lambda do |groups| + group: ->(groups) do flash.now[:alert] = groups_notification(groups) end } diff --git a/app/graphql/queries/repository/blob_info.query.graphql b/app/graphql/queries/repository/blob_info.query.graphql index 7419961a564..13966d022a9 100644 --- a/app/graphql/queries/repository/blob_info.query.graphql +++ b/app/graphql/queries/repository/blob_info.query.graphql @@ -1,6 +1,6 @@ query getBlobInfo( $projectPath: ID! - $filePath: String! + $filePath: [String!]! $ref: String! $refType: RefType $shouldFetchRawText: Boolean! @@ -11,7 +11,7 @@ query getBlobInfo( repository { __typename empty - blobs(paths: [$filePath], ref: $ref, refType: $refType) { + blobs(paths: $filePath, ref: $ref, refType: $refType) { __typename nodes { __typename diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb index 3b741208221..465ae162d40 100644 --- a/app/models/concerns/featurable.rb +++ b/app/models/concerns/featurable.rb @@ -108,7 +108,7 @@ module Featurable private def allowed_access_levels - validator = lambda do |field| + validator = ->(field) do level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend not_allowed = level > ENABLED self.errors.add(field, "cannot have public visibility level") if not_allowed diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index a8ed19efbe2..c74e157dbe6 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -201,7 +201,7 @@ class ProjectFeature < ApplicationRecord # Validates builds and merge requests access level # which cannot be higher than repository access level def repository_children_level - validator = lambda do |field| + validator = ->(field) do level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend not_allowed = level > repository_access_level self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed diff --git a/app/serializers/ci/daily_build_group_report_result_entity.rb b/app/serializers/ci/daily_build_group_report_result_entity.rb index e4118db9b1f..249fcae5294 100644 --- a/app/serializers/ci/daily_build_group_report_result_entity.rb +++ b/app/serializers/ci/daily_build_group_report_result_entity.rb @@ -5,7 +5,7 @@ module Ci expose :date ::Ci::DailyBuildGroupReportResult::PARAM_TYPES.each do |type| - expose type, if: lambda { |report_result, options| options[:param_type] == type } do |report_result, options| + expose type, if: ->(report_result, options) { options[:param_type] == type } do |report_result, options| report_result.data[options[:param_type]] end end diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb index 669ade079e1..a9fda1e3db6 100644 --- a/app/serializers/group_child_entity.rb +++ b/app/serializers/group_child_entity.rb @@ -33,22 +33,22 @@ class GroupChildEntity < Grape::Entity end # Project only attributes - expose :last_activity_at, if: lambda { |instance| project? } + expose :last_activity_at, if: ->(instance) { project? } - expose :star_count, :archived, if: lambda { |_instance, _options| project? } + expose :star_count, :archived, if: ->(_instance, _options) { project? } # Group only attributes - expose :children_count, :parent_id, unless: lambda { |_instance, _options| project? } + expose :children_count, :parent_id, unless: ->(_instance, _options) { project? } - expose :subgroup_count, if: lambda { |group| access_group_counts?(group) } + expose :subgroup_count, if: ->(group) { access_group_counts?(group) } - expose :project_count, if: lambda { |group| access_group_counts?(group) } + expose :project_count, if: ->(group) { access_group_counts?(group) } - expose :leave_path, unless: lambda { |_instance, _options| project? } do |instance| + expose :leave_path, unless: ->(_instance, _options) { project? } do |instance| leave_group_members_path(instance) end - expose :can_leave, unless: lambda { |_instance, _options| project? } do |instance| + expose :can_leave, unless: ->(_instance, _options) { project? } do |instance| if membership can?(request.current_user, :destroy_group_member, membership) else @@ -56,11 +56,11 @@ class GroupChildEntity < Grape::Entity end end - expose :can_remove, unless: lambda { |_instance, _options| project? } do |group| + expose :can_remove, unless: ->(_instance, _options) { project? } do |group| can?(request.current_user, :admin_group, group) end - expose :number_users_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance| + expose :number_users_with_delimiter, unless: ->(_instance, _options) { project? } do |instance| number_with_delimiter(instance.member_count) end diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb index 9039606a8e5..192a3c41295 100644 --- a/app/serializers/issuable_sidebar_basic_entity.rb +++ b/app/serializers/issuable_sidebar_basic_entity.rb @@ -20,7 +20,7 @@ class IssuableSidebarBasicEntity < Grape::Entity expose :milestone, using: ::API::Entities::Milestone expose :labels, using: LabelEntity - expose :current_user, if: lambda { |_issuable| current_user } do + expose :current_user, if: ->(_issuable) { current_user } do expose :current_user, merge: true, using: ::API::Entities::UserBasic expose :todo, using: IssuableSidebarTodoEntity do |issuable| diff --git a/app/serializers/merge_request_sidebar_basic_entity.rb b/app/serializers/merge_request_sidebar_basic_entity.rb index 2dd6e1997cc..4b6cd50421a 100644 --- a/app/serializers/merge_request_sidebar_basic_entity.rb +++ b/app/serializers/merge_request_sidebar_basic_entity.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity - expose :current_user, if: lambda { |_issuable| current_user } do + expose :current_user, if: ->(_issuable) { current_user } do expose :can_merge do |merge_request| merge_request.can_be_merged_by?(current_user) end diff --git a/app/services/groups/agnostic_token_revocation_service.rb b/app/services/groups/agnostic_token_revocation_service.rb new file mode 100644 index 00000000000..e0992452a11 --- /dev/null +++ b/app/services/groups/agnostic_token_revocation_service.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# This Service takes an authentication token of multiple types, and will +# call a RevokeService for it if the token has access to the group or +# any of the group's descendants. +# +# If the token provided has access to the group and is revoked, it will +# be returned by the service with a :success status. +# If the token type is not supported, if the token doesn't have access +# to the group, or if any error occurs, a generic :failure status is +# returned. +# +# This Service does not create logs or Audit events. Those can be found +# at the API layer or in specific RevokeServices. +# +# This Service returns a ServiceResponse and will: +# - include the token object at payload[:token] +# - the token's class at payload[:type] +module Groups # rubocop:disable Gitlab/BoundedContexts -- This service is strictly related to groups + class AgnosticTokenRevocationService < Groups::BaseService + AUDIT_SOURCE = :group_token_revocation_service + + attr_reader :token + + def initialize(group, current_user, plaintext) + @group = group + @current_user = current_user + @plaintext = plaintext.to_s + end + + def execute + return error("Feature not enabled") unless Feature.enabled?(:group_agnostic_token_revocation, group) + return error("Group cannot be a subgroup") if group.subgroup? + return error("Unauthorized") unless can?(current_user, :admin_group, group) + + # Determine the type of token + if plaintext.start_with?(Gitlab::CurrentSettings.current_application_settings.personal_access_token_prefix, + 'glpat-') + @token = PersonalAccessToken.find_by_token(plaintext) + return error('PAT not found') unless token + + handle_personal_access_token + elsif plaintext.start_with?(DeployToken::DEPLOY_TOKEN_PREFIX) + @token = DeployToken.find_by_token(plaintext) + return error('DeployToken not found') unless token && token.group_type? + + handle_deploy_token + else + error('Unsupported token type') + end + end + + private + + attr_reader :plaintext, :group, :current_user + + def success(token, type) + ServiceResponse.success( + message: "#{type} is revoked", + payload: { + token: token, + type: type + } + ) + end + + def error(message) + ServiceResponse.error(message: message) + end + + def handle_personal_access_token + if user_has_group_membership? + # Only revoke active tokens. (Ignore expired tokens) + if token.active? + ::PersonalAccessTokens::RevokeService.new( + current_user, + token: token, + source: AUDIT_SOURCE + ).execute + end + + # Always validate that, if we're returning token info, it + # has been successfully revoked + return success(token, 'PersonalAccessToken') if token.reset.revoked? + end + + # If we get here the token exists but either: + # - didn't belong to the group or descendants + # - did, but was already expired + # - does and is active, but revocation failed for some reason + error('PAT revocation failed') + end + + # Validate whether the user has access to a group or any of its + # descendants. Includes membership that might not be active, but + # could be later, e.g. bans. Includes membership of non-human + # users. + def user_has_group_membership? + ::GroupMember + .with_user(token.user) + .with_source_id(group.self_and_descendants) + .any? || + ::ProjectMember + .with_user(token.user) + .in_namespaces(group.self_and_descendants) + .any? + end + + def handle_deploy_token + if group.self_and_descendants.include?(token.group) + if token.active? + service = ::Groups::DeployTokens::RevokeService.new( + token.group, + current_user, + { id: token.id } + ) + + service.source = AUDIT_SOURCE + service.execute + end + + return success(token, 'DeployToken') if token.reset.revoked? + end + + error('DeployToken revocation failed') + end + end +end diff --git a/app/services/groups/deploy_tokens/revoke_service.rb b/app/services/groups/deploy_tokens/revoke_service.rb index cf91d3b27fa..0aa88f6190d 100644 --- a/app/services/groups/deploy_tokens/revoke_service.rb +++ b/app/services/groups/deploy_tokens/revoke_service.rb @@ -3,7 +3,7 @@ module Groups module DeployTokens class RevokeService < BaseService - attr_accessor :token + attr_accessor :token, :source def execute @token = group.deploy_tokens.find(params[:id]) diff --git a/app/services/merge_requests/merge_strategies/from_source_branch.rb b/app/services/merge_requests/merge_strategies/from_source_branch.rb index fe0e4d8a90c..731a66ac01d 100644 --- a/app/services/merge_requests/merge_strategies/from_source_branch.rb +++ b/app/services/merge_requests/merge_strategies/from_source_branch.rb @@ -18,21 +18,17 @@ module MergeRequests end def validate! - error_message = - if source_sha.blank? - 'No source for merge' - elsif merge_request.should_be_rebased? - 'Only fast-forward merge is allowed for your project. Please update your source branch' - elsif !merge_request.mergeable?( - skip_discussions_check: @options[:skip_discussions_check], - check_mergeability_retry_lease: @options[:check_mergeability_retry_lease] - ) - 'Merge request is not mergeable' - elsif merge_request.missing_required_squash? - 'This project requires squashing commits when merge requests are accepted.' - end + raise_error('No source for merge') if source_sha.blank? - raise_error(error_message) if error_message + if merge_request.should_be_rebased? + raise_error('Only fast-forward merge is allowed for your project. Please update your source branch') + end + + raise_error('Merge request is not mergeable') unless mergeable? + + return unless merge_request.missing_required_squash? + + raise_error('This project requires squashing commits when merge requests are accepted.') end def execute_git_merge! @@ -104,6 +100,13 @@ module MergeRequests merge_request.default_merge_commit_message(user: current_user) end + def mergeable? + merge_request.mergeable?( + skip_discussions_check: options[:skip_discussions_check], + check_mergeability_retry_lease: options[:check_mergeability_retry_lease] + ) + end + def raise_error(message) raise ::MergeRequests::MergeStrategies::StrategyError, message end diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb index 5df0f5325b0..897a2b12caa 100644 --- a/app/services/personal_access_tokens/revoke_service.rb +++ b/app/services/personal_access_tokens/revoke_service.rb @@ -4,7 +4,7 @@ module PersonalAccessTokens class RevokeService < BaseService attr_reader :token, :current_user, :group, :source - VALID_SOURCES = %i[self secret_detection].freeze + VALID_SOURCES = %i[self secret_detection group_token_revocation_service].freeze def initialize(current_user = nil, token: nil, group: nil, source: nil) @current_user = current_user @@ -43,7 +43,7 @@ module PersonalAccessTokens case source when :self Ability.allowed?(current_user, :revoke_token, token) - when :secret_detection + when :secret_detection, :group_token_revocation_service true else false @@ -54,10 +54,16 @@ module PersonalAccessTokens Gitlab::AppLogger.info( class: self.class.name, message: "PAT Revoked", - revoked_by: current_user&.username || source, + revoked_by: revoked_by, revoked_for: token.user.username, token_id: token.id) end + + def revoked_by + return current_user&.username if source == :self + + source + end end end diff --git a/app/validators/json_schemas/members_approval_request_metadata.json b/app/validators/json_schemas/members_approval_request_metadata.json index 3c9ca4cf7b8..ce33a2de255 100644 --- a/app/validators/json_schemas/members_approval_request_metadata.json +++ b/app/validators/json_schemas/members_approval_request_metadata.json @@ -3,13 +3,23 @@ "type": "object", "properties": { "expires_at": { - "type": "string", "oneOf": [ { - "format": "date" + "type": "null" }, { - "pattern": "^$" + "type": "string", + "oneOf": [ + { + "format": "date" + }, + { + "format": "date-time" + }, + { + "pattern": "^$" + } + ] } ] }, diff --git a/config/application.rb b/config/application.rb index 21e4bb870f0..ecfa9d77e40 100644 --- a/config/application.rb +++ b/config/application.rb @@ -120,7 +120,7 @@ module Gitlab config.generators.templates.push("#{config.root}/generator_templates") foss_eager_load_paths = config.eager_load_paths.dup.freeze - load_paths = lambda do |dir:| + load_paths = ->(dir:) do ext_paths = foss_eager_load_paths.each_with_object([]) do |path, memo| ext_path = config.root.join(dir, Pathname.new(path).relative_path_from(config.root)) memo << ext_path.to_s @@ -581,7 +581,7 @@ module Gitlab asset_roots << config.root.join("ee/app/assets").to_s end - LOOSE_APP_ASSETS = lambda do |logical_path, filename| + LOOSE_APP_ASSETS = ->(logical_path, filename) do filename.start_with?(*asset_roots) && ['.js', '.css', '.md', '.vue', '.graphql', ''].exclude?(File.extname(logical_path)) end diff --git a/config/feature_flags/beta/group_agnostic_token_revocation.yml b/config/feature_flags/beta/group_agnostic_token_revocation.yml new file mode 100644 index 00000000000..4ec5c40785f --- /dev/null +++ b/config/feature_flags/beta/group_agnostic_token_revocation.yml @@ -0,0 +1,9 @@ +--- +name: group_agnostic_token_revocation +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/460777 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/153376 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/463157 +milestone: '17.2' +group: group::authentication +type: beta +default_enabled: false diff --git a/config/initializers/0_license.rb b/config/initializers/0_license.rb index c6a09a2f769..e6b17a522f9 100644 --- a/config/initializers/0_license.rb +++ b/config/initializers/0_license.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -load_license = lambda do |dir:, license_name:| +load_license = ->(dir:, license_name:) do begin public_key_file = File.read(Rails.root.join(dir, ".license_encryption_key.pub")) public_key = OpenSSL::PKey::RSA.new(public_key_file) diff --git a/config/initializers/action_cable.rb b/config/initializers/action_cable.rb index 21c49ef7204..76d9d54eded 100644 --- a/config/initializers/action_cable.rb +++ b/config/initializers/action_cable.rb @@ -23,7 +23,7 @@ end raise "Do not configure cable.yml with a Redis Cluster as ActionCable only works with Redis." if using_redis_cluster # https://github.com/rails/rails/blob/bb5ac1623e8de08c1b7b62b1368758f0d3bb6379/actioncable/lib/action_cable/subscription_adapter/redis.rb#L18 -ActionCable::SubscriptionAdapter::Redis.redis_connector = lambda do |config| +ActionCable::SubscriptionAdapter::Redis.redis_connector = ->(config) do args = config.except(:adapter, :channel_prefix) .merge(custom: { instrumentation_class: "ActionCable" }) diff --git a/config/initializers/gitlab_experiment.rb b/config/initializers/gitlab_experiment.rb index 10ae1fa8ff0..13554fc0bfb 100644 --- a/config/initializers/gitlab_experiment.rb +++ b/config/initializers/gitlab_experiment.rb @@ -48,7 +48,7 @@ Gitlab::Experiment.configure do |config| # so we don't think we should redirect in those cases. # valid_domains = %w[about.gitlab.com docs.gitlab.com gitlab.com gdk.test localhost] - config.redirect_url_validator = lambda do |url| + config.redirect_url_validator = ->(url) do ApplicationExperiment.available? && (url = URI.parse(url)) && valid_domains.include?(url.host) rescue URI::InvalidURIError false @@ -64,7 +64,7 @@ Gitlab::Experiment.configure do |config| # This uses the Gitlab::Tracking interface, so arbitrary event properties are # permitted, and will be sent along using Gitlab::Tracking::StandardContext. # - config.tracking_behavior = lambda do |action, event_args| + config.tracking_behavior = ->(action, event_args) do Gitlab::Tracking.event(name, action, **event_args.merge( context: (event_args[:context] || []) << SnowplowTracker::SelfDescribingJson.new( 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0', signature diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 08439df188e..0a0facefaf5 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -14,7 +14,7 @@ unless Gitlab::Runtime.sidekiq? config.lograge.formatter = Lograge::Formatters::Json.new config.lograge.logger = ActiveSupport::Logger.new(filename, level: Gitlab::Utils.to_rails_log_level(ENV["GITLAB_LOG_LEVEL"], :info)) - config.lograge.before_format = lambda do |data, payload| + config.lograge.before_format = ->(data, payload) do data.delete(:error) data[:db_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:db)) if data[:db] data[:view_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:view)) if data[:view] diff --git a/config/routes.rb b/config/routes.rb index b90429a0051..7a292633b8a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -115,7 +115,7 @@ InitializerConnections.raise_if_new_database_connection do get '/whats_new' => 'whats_new#index' get 'offline' => "pwa#offline" - get 'manifest' => "pwa#manifest", constraints: lambda { |req| req.format == :json } + get 'manifest' => "pwa#manifest", constraints: ->(req) { req.format == :json } scope module: 'clusters' do scope module: 'agents' do diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb index 5c5f063b2af..8ca0aa7c692 100644 --- a/config/routes/dashboard.rb +++ b/config/routes/dashboard.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true resource :dashboard, controller: 'dashboard', only: [] do - get :issues, action: :issues_calendar, constraints: lambda { |req| req.format == :ics } + get :issues, action: :issues_calendar, constraints: ->(req) { req.format == :ics } get :issues get :merge_requests get :activity diff --git a/config/routes/group.rb b/config/routes/group.rb index 0816970d0cf..1e34de30790 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -10,7 +10,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do # These routes are legit and the cop rule will be improved in # https://gitlab.com/gitlab-org/gitlab/-/issues/230703 get :edit, as: :edit_group # rubocop:disable Cop/PutGroupRoutesUnderScope - get :issues, as: :issues_group_calendar, action: :issues_calendar, constraints: lambda { |req| req.format == :ics } # rubocop:disable Cop/PutGroupRoutesUnderScope + get :issues, as: :issues_group_calendar, action: :issues_calendar, constraints: ->(req) { req.format == :ics } # rubocop:disable Cop/PutGroupRoutesUnderScope get :issues, as: :issues_group # rubocop:disable Cop/PutGroupRoutesUnderScope get :merge_requests, as: :merge_requests_group # rubocop:disable Cop/PutGroupRoutesUnderScope get :projects, as: :projects_group # rubocop:disable Cop/PutGroupRoutesUnderScope diff --git a/config/routes/issues.rb b/config/routes/issues.rb index 13fdde5841b..3869300b1d6 100644 --- a/config/routes/issues.rb +++ b/config/routes/issues.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -get :issues, to: 'issues#calendar', constraints: lambda { |req| req.format == :ics } +get :issues, to: 'issues#calendar', constraints: ->(req) { req.format == :ics } resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do member do diff --git a/db/post_migrate/20240701081515_fk_to_ci_pipelines_from_p_ci_builds_execution_configs_on_partition_id_pipeline_id.rb b/db/post_migrate/20240701081515_fk_to_ci_pipelines_from_p_ci_builds_execution_configs_on_partition_id_pipeline_id.rb new file mode 100644 index 00000000000..fcc80f26d3c --- /dev/null +++ b/db/post_migrate/20240701081515_fk_to_ci_pipelines_from_p_ci_builds_execution_configs_on_partition_id_pipeline_id.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class FkToCiPipelinesFromPCiBuildsExecutionConfigsOnPartitionIdPipelineId < Gitlab::Database::Migration[2.2] + include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers + + milestone '17.2' + + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :p_ci_builds_execution_configs + TARGET_TABLE_NAME = :ci_pipelines + COLUMN = :pipeline_id + TARGET_COLUMN = :id + FK_NAME = :fk_rails_c26408d02c_p + PARTITION_COLUMN = :partition_id + + def up + add_concurrent_partitioned_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: [PARTITION_COLUMN, COLUMN], + target_column: [PARTITION_COLUMN, TARGET_COLUMN], + validate: true, + reverse_lock_order: true, + on_update: :cascade, + on_delete: :cascade, + name: FK_NAME + ) + end + + def down + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end +end diff --git a/db/post_migrate/20240701090730_remove_fk_to_ci_pipelines_p_ci_builds_execution_configs_on_pipeline_id.rb b/db/post_migrate/20240701090730_remove_fk_to_ci_pipelines_p_ci_builds_execution_configs_on_pipeline_id.rb new file mode 100644 index 00000000000..3cc0fdaae20 --- /dev/null +++ b/db/post_migrate/20240701090730_remove_fk_to_ci_pipelines_p_ci_builds_execution_configs_on_pipeline_id.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class RemoveFkToCiPipelinesPCiBuildsExecutionConfigsOnPipelineId < Gitlab::Database::Migration[2.2] + include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers + + milestone '17.2' + + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :p_ci_builds_execution_configs + TARGET_TABLE_NAME = :ci_pipelines + COLUMN = :pipeline_id + TARGET_COLUMN = :id + FK_NAME = :fk_rails_c26408d02c + + def up + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end + + def down + add_concurrent_partitioned_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: COLUMN, + target_column: TARGET_COLUMN, + validate: true, + reverse_lock_order: true, + on_delete: :cascade, + name: FK_NAME + ) + end +end diff --git a/db/post_migrate/20240702081839_swap_head_pipeline_columns_for_merge_requests_head_pipelines.rb b/db/post_migrate/20240702081839_swap_head_pipeline_columns_for_merge_requests_head_pipelines.rb new file mode 100644 index 00000000000..7282248155f --- /dev/null +++ b/db/post_migrate/20240702081839_swap_head_pipeline_columns_for_merge_requests_head_pipelines.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class SwapHeadPipelineColumnsForMergeRequestsHeadPipelines < Gitlab::Database::Migration[2.2] + include Gitlab::Database::MigrationHelpers::Swapping + + milestone '17.2' + disable_ddl_transaction! + + TABLE_NAME = :merge_requests + INDEX_NAME = :index_merge_requests_on_head_pipeline_id + BIGINT_INDEX_NAME = :index_merge_requests_on_head_pipeline_id_bigint + COLUMN_NAME = :head_pipeline_id + BIGINT_COLUMN_NAME = :head_pipeline_id_convert_to_bigint + TRIGGER_FUNCTIONS = [:trigger_fb587b1ae7ad, :insert_into_loose_foreign_keys_deleted_records] + + def up + add_concurrent_index TABLE_NAME, BIGINT_COLUMN_NAME, name: BIGINT_INDEX_NAME + + swap + end + + def down + add_concurrent_index TABLE_NAME, BIGINT_COLUMN_NAME, name: BIGINT_INDEX_NAME + + swap + end + + def swap + with_lock_retries(raise_on_exhaustion: true) do + # Not locking ci_pipelines as it's a LFK column + lock_tables(TABLE_NAME) + + swap_columns(TABLE_NAME, COLUMN_NAME, BIGINT_COLUMN_NAME) + + TRIGGER_FUNCTIONS.each do |trigger| + reset_trigger_function(trigger) + end + + execute "DROP INDEX #{INDEX_NAME}" + rename_index TABLE_NAME, BIGINT_INDEX_NAME, INDEX_NAME + end + end +end diff --git a/db/schema_migrations/20240701081515 b/db/schema_migrations/20240701081515 new file mode 100644 index 00000000000..e009761e9f8 --- /dev/null +++ b/db/schema_migrations/20240701081515 @@ -0,0 +1 @@ +057e3f3efbb95245addc1a1d506bba04fe4e023f1383e253527780133c7c2492 \ No newline at end of file diff --git a/db/schema_migrations/20240701090730 b/db/schema_migrations/20240701090730 new file mode 100644 index 00000000000..97a73c9df81 --- /dev/null +++ b/db/schema_migrations/20240701090730 @@ -0,0 +1 @@ +aaee0427152b97f20768d8491c6a03e0bd9485d8a654358e98d8714d644eaea4 \ No newline at end of file diff --git a/db/schema_migrations/20240702081839 b/db/schema_migrations/20240702081839 new file mode 100644 index 00000000000..242353461f8 --- /dev/null +++ b/db/schema_migrations/20240702081839 @@ -0,0 +1 @@ +1da4c72237894935bca55547d219aca80c81fcd908a05208d722299233cfb074 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 4aab6c66f5a..ff7806b27c8 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12726,7 +12726,7 @@ CREATE TABLE merge_requests ( cached_markdown_version integer, last_edited_at timestamp without time zone, last_edited_by_id integer, - head_pipeline_id integer, + head_pipeline_id_convert_to_bigint integer, merge_jid character varying, discussion_locked boolean, latest_merge_request_diff_id integer, @@ -12740,7 +12740,7 @@ CREATE TABLE merge_requests ( prepared_at timestamp with time zone, merged_commit_sha bytea, override_requested_changes boolean DEFAULT false NOT NULL, - head_pipeline_id_convert_to_bigint bigint, + head_pipeline_id bigint, imported_from smallint DEFAULT 0 NOT NULL, retargeted boolean DEFAULT false NOT NULL, CONSTRAINT check_970d272570 CHECK ((lock_version IS NOT NULL)) @@ -34655,7 +34655,7 @@ ALTER TABLE ONLY project_feature_usages ADD CONSTRAINT fk_rails_c22a50024b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ALTER TABLE p_ci_builds_execution_configs - ADD CONSTRAINT fk_rails_c26408d02c FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE; + ADD CONSTRAINT fk_rails_c26408d02c_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY user_canonical_emails ADD CONSTRAINT fk_rails_c2bd828b51 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/doc/administration/self_hosted_models/configure_duo_features.md b/doc/administration/self_hosted_models/configure_duo_features.md index 75b57b0b100..91051c94bba 100644 --- a/doc/administration/self_hosted_models/configure_duo_features.md +++ b/doc/administration/self_hosted_models/configure_duo_features.md @@ -43,21 +43,21 @@ Prerequisites: To configure a self-hosted model: 1. On the left sidebar, at the bottom, select **Admin Area**. -1. Select **AI-Powered Features**. - - If the **AI-Powered Features** menu item is not available, synchronize your +1. Select **AI-powered features**. + - If the **AI-powered features** menu item is not available, synchronize your subscription after purchase: 1. On the left sidebar, select **Subscription**. 1. In **Subscription details**, to the right of **Last sync**, select synchronize subscription (**{retry}**). 1. Select **Models**. -1. Set your model details by selecting **New Self-Hosted Model**. +1. Set your model details by selecting **New self-hosted model**. 1. Complete the fields: - Enter the model name, for example, Mistral. - From the **Model** dropdown list, select the model. Only GitLab-approved models are listed here. - For **Endpoint**, select the self-hosted model endpoint, for example, the server hosting the model. - Optional. For **API token**, add an API key if you need one to access the model. -1. Select **Create Self-Hosted Model**. +1. Select **Create model**. ## Configure the features to your models @@ -68,8 +68,8 @@ Prerequisites: To configure the AI-powered features to use your model: 1. On the left sidebar, at the bottom, select **Admin Area**. -1. Select **AI-Powered Features**. - - If the **AI-Powered Features** menu item is not available, synchronize your +1. Select **AI-powered features**. + - If the **AI-powered features** menu item is not available, synchronize your subscription after purchase: 1. On the left sidebar, select **Subscription**. 1. In **Subscription details**, to the right of **Last sync**, select diff --git a/doc/administration/self_hosted_models/install_infrastructure.md b/doc/administration/self_hosted_models/install_infrastructure.md index aa8e6bbe172..e9fc9e8f9ce 100644 --- a/doc/administration/self_hosted_models/install_infrastructure.md +++ b/doc/administration/self_hosted_models/install_infrastructure.md @@ -1,7 +1,7 @@ --- stage: AI-Powered group: Custom Models -description: Setup your Self-Hosted Model Deployment infrastructure +description: Setup your self-hosted model deployment infrastructure info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/groups.md b/doc/api/groups.md index 75c62b783e7..170e2f41d91 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -2427,3 +2427,69 @@ DELETE /groups/:id/push_rule | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) | + +## Revoke Token + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371117) in GitLab 17.2 [with a flag](../administration/feature_flags.md) named `group_agnostic_token_revocation`. Disabled by default. + +FLAG: +The availability of this feature is controlled by a feature flag. +For more information, see the history. + +Revoke a token, if it has access to the group or any of its subgroups +and projects. If the token is revoked, or was already revoked, its +details are returned in the response. + +The following criteria must be met: + +- The group must be a top-level group. +- You must have the Owner role in the group. +- The token type is one of: + - Personal Access Token + - Group Access Token + - Project Access Token + - Group Deploy Token + +Additional token types may be supported at a later date. + +```plaintext +POST /groups/:id/tokens/revoke +``` + +| Attribute | Type | Required | Description | +|-------------|----------------|------------------------|-------------| +| `id` | integer or string | Yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). | +| `token` | string | Yes | The plaintext token. | + +If successful, returns [`200 OK`](rest/index.md#status-codes) and +a JSON representation of the token. The attributes returned will vary by +token type. + +Example request: + +```shell +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --header "Content-Type: application/json" \ + --data '{"token":"glpat-1234567890abcdefghij"}' \ + --url "https://gitlab.example.com/api/v4/groups/63/tokens/revoke" +``` + +Example response: + +```json +{ + "id": 9, + "name": "my-subgroup-deploytoken", + "username": "gitlab+deploy-token-9", + "expires_at": null, + "scopes": + [ + "read_repository", + "read_package_registry", + "write_package_registry" + ], + "revoked": true, + "expired": false +} +``` diff --git a/doc/architecture/blueprints/custom_models/index.md b/doc/architecture/blueprints/custom_models/index.md index 387d366d70b..9a0f12bbe9a 100644 --- a/doc/architecture/blueprints/custom_models/index.md +++ b/doc/architecture/blueprints/custom_models/index.md @@ -17,7 +17,7 @@ This Blueprint describes support for customer self-deployments of Mistral LLMs a ## Motivation -Self-Hosted LLM models allow customers to manage the end-to-end transmission of requests to enterprise-hosted LLM backends for [GitLab Duo features](../../../user/ai_features.md), and keep all requests within their enterprise network. GitLab provides as a default LLM backends of Google Vertex and Anthropic, hosted externally to GitLab. GitLab Duo feature developers are able to access other LLM choices via the AI Gateway. More details on model and region information can be [found here](https://gitlab.com/groups/gitlab-org/-/epics/13024#current-feature-outline). +Self-hosted LLM models allow customers to manage the end-to-end transmission of requests to enterprise-hosted LLM backends for [GitLab Duo features](../../../user/ai_features.md), and keep all requests within their enterprise network. GitLab provides as a default LLM backends of Google Vertex and Anthropic, hosted externally to GitLab. GitLab Duo feature developers are able to access other LLM choices via the AI Gateway. More details on model and region information can be [found here](https://gitlab.com/groups/gitlab-org/-/epics/13024#current-feature-outline). ### Goals @@ -44,7 +44,7 @@ GitLab will provide support for specific LLMs hosted in a customer's infrastruct This feature is accessible at the instance-level and is intended for use in GitLab Self-Managed instances. -Self-Hosted Model Deployment is a [GitLab Duo Enterprise Add-on](https://about.gitlab.com/pricing/). +Self-hosted model deployment is a [GitLab Duo Enterprise Add-on](https://about.gitlab.com/pricing/). ## Design and implementation details @@ -98,8 +98,8 @@ graph LR Configuration is set at the GitLab instance-level; for each GitLab Duo feature a drop-down list of options will be presented. The following options will be available: -- Self-Hosted Model 1 -- Self-Hosted Model n +- Self-hosted model 1 +- Self-hosted model n - Feature Inactive In the initial implementation a single self-hosted Model will be supported, but this will be expanded to a number of GitLab-defined models. @@ -219,7 +219,7 @@ Self-Managed customers who deploy a self-managed AI Gateway will only be able to Engineering documentation will be produced on how to develop this feature, with work in progress on: - [Include AI Gateway in GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/-/issues/2025) -- [Developer setup for Self-Hosted Models](https://gitlab.com/gitlab-org/gitlab/-/issues/452509) +- [Developer setup for self-hosted models](https://gitlab.com/gitlab-org/gitlab/-/issues/452509) - [Centralized Evaluation Framework](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/prompt-library/-/tree/main) ### Out of scope diff --git a/doc/ci/jobs/job_artifacts.md b/doc/ci/jobs/job_artifacts.md index cf3249adc7f..c23ae78e374 100644 --- a/doc/ci/jobs/job_artifacts.md +++ b/doc/ci/jobs/job_artifacts.md @@ -283,7 +283,7 @@ from: - A job's detail page. On the right of the page, select **Browse**. - The **Artifacts** page. On the right of the job, select **Browse** (**{folder-open}**). -If [GitLab Pages](../../administration/pages/index.md) is enabled in the project, you can preview +If [GitLab Pages](../../administration/pages/index.md) is enabled by the administrator, you can preview some artifacts file extensions directly in your browser. If the project is internal or private, you must enable [GitLab Pages access control](../../administration/pages/index.md#access-control) to enable the preview. The following extensions are supported: diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md index 18afcfae8a8..6004f0ebdba 100644 --- a/doc/ci/variables/index.md +++ b/doc/ci/variables/index.md @@ -569,16 +569,16 @@ test-job4: script: - echo "$BUILD_VERSION" # Output is: 'v1.0.0' needs: - job: build-job1 - artifacts: true + - job: build-job1 + artifacts: true test-job5: stage: deploy script: - echo "$BUILD_VERSION" # Output is '' needs: - job: build-job1 - artifacts: false + - job: build-job1 + artifacts: false test-job6: stage: deploy diff --git a/doc/development/ai_features/glossary.md b/doc/development/ai_features/glossary.md index 7edbb61ca50..701762a3140 100644 --- a/doc/development/ai_features/glossary.md +++ b/doc/development/ai_features/glossary.md @@ -77,7 +77,7 @@ to AI that you think could benefit from being in this list, add it! synthesize the information to generate a coherent, contextualy-relevant answer. This design pattern is helpful in open-domain question answering with LLMs, which is why we use this design pattern for answering questions to GitLab Duo Chat. -- **Self-Hosted Model**: A LLM hosted externally to GitLab by an organisation and interacting with GitLab AI features. +- **Self-hosted model**: A LLM hosted externally to GitLab by an organisation and interacting with GitLab AI features. - **Similarity Score**: A mathematical method to determine the likeness between answers produced by an LLM and the reference ground truth answers. See also the [Model Validation direction page](https://about.gitlab.com/direction/ai-powered/ai_model_validation/ai_evaluation/metrics/#similarity-scores) - **Tool**: logic that performs a specific LLM-related task; each tool has a diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md index 2bf5e7761b4..8b0a3f0fcc8 100644 --- a/doc/user/group/custom_project_templates.md +++ b/doc/user/group/custom_project_templates.md @@ -47,6 +47,12 @@ Projects in nested subgroups are not included in the template list. except for **GitLab Pages** and **Security and Compliance** are set to **Everyone With Access**. - Private projects can be selected only by users who are members of the projects. +There is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/295646): +[Inherited members](../project/members/index.md#membership-types) can't select project templates, +unless the `project_templates_without_min_access` feature flag is enabled. +This feature flag [is disabled](https://gitlab.com/gitlab-org/gitlab/-/issues/425452) +on GitLab.com, and so users must be granted direct membership of the template project. + ## Example structure Here's a sample group and project structure for project templates, for `myorganization`: diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index f563124ac7c..344366c647a 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -532,6 +532,14 @@ repository to be imported manually. Administrators can manually import the repos Gitlab::GithubImport::Stage::ImportRepositoryWorker.perform_async(project.id) ``` +### Import fails due to missing prefix + +In GitLab 16.5 and later, you might get an error that states `Import failed due to a GitHub error: (HTTP 406)`. + +This issue occurs because, in GitLab 16.5, the path prefix `api/v3` was removed from the GitHub importer. This happened because the importer stopped using the `Gitlab::LegacyGithubImport::Client`. This client automatically added the `api/v3` prefix on imports from a GitHub Enterprise URL. + +To work around this error, [add the `api/v3` prefix](https://gitlab.com/gitlab-org/gitlab/-/issues/438358#note_1978902725) when importing from a GitHub Enterprise URL. + ### Errors when importing large projects The GitHub importer might encounter some errors when importing large projects. diff --git a/lib/api/entities/label.rb b/lib/api/entities/label.rb index dc147f33671..6e2949332ab 100644 --- a/lib/api/entities/label.rb +++ b/lib/api/entities/label.rb @@ -3,7 +3,7 @@ module API module Entities class Label < Entities::LabelBasic - with_options if: lambda { |_, options| options[:with_counts] } do + with_options if: ->(_, options) { options[:with_counts] } do expose :open_issues_count do |label, options| label.open_issues_count(options[:current_user]) end diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index 93ef6adbb91..0c36355ed08 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -101,10 +101,10 @@ module API end expose :import_type, documentation: { type: 'string', example: 'git' }, if: ->(project, options) { Ability.allowed?(options[:current_user], :admin_project, project) } expose :import_status, documentation: { type: 'string', example: 'none' } - expose :import_error, documentation: { type: 'string', example: 'Import error' }, if: lambda { |_project, options| options[:user_can_admin_project] } do |project| + expose :import_error, documentation: { type: 'string', example: 'Import error' }, if: ->(_project, options) { options[:user_can_admin_project] } do |project| project.import_state&.last_error end - expose :open_issues_count, documentation: { type: 'integer', example: 1 }, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } + expose :open_issues_count, documentation: { type: 'integer', example: 1 }, if: ->(project, options) { project.feature_available?(:issues, options[:current_user]) } expose :description_html, documentation: { type: 'string' } expose :updated_at, documentation: { type: 'dateTime', example: '2020-05-07T04:27:17.016Z' } diff --git a/lib/api/entities/project_export_status.rb b/lib/api/entities/project_export_status.rb index 9a2aeb7a6bb..fe9216b79cf 100644 --- a/lib/api/entities/project_export_status.rb +++ b/lib/api/entities/project_export_status.rb @@ -8,7 +8,7 @@ module API expose :export_status, documentation: { type: 'string', example: 'finished', values: %w[queued started finished failed] } - expose :_links, if: lambda { |project, _options| project.export_status == :finished } do + expose :_links, if: ->(project, _options) { project.export_status == :finished } do expose :api_url, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/1/export/download' diff --git a/lib/api/groups.rb b/lib/api/groups.rb index f5c54d36d18..88c55a0d5ab 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -580,6 +580,48 @@ module API no_content! end # rubocop: enable CodeReuse/ActiveRecord + + desc 'Revoke a single token' do + detail <<-DETAIL +Revoke a token, if it has access to the group or any of its subgroups +and projects. If the token is revoked, or was already revoked, its +details are returned in the response. + +The following criteria must be met: + +- The group must be a top-level group. +- You must have Owner permission in the group. +- The token type is one of: + - Personal Access Token + - Group Access Token + - Project Access Token + - Group Deploy Token + +This feature is gated by the :group_agnostic_token_revocation feature flag. + DETAIL + end + params do + requires :id, type: String, desc: 'The ID of a top-level group' + requires :token, type: String, desc: 'The token to revoke' + end + post ":id/tokens/revoke", urgency: :low, feature_category: :groups_and_projects do + group = find_group!(params[:id]) + not_found! unless Feature.enabled?(:group_agnostic_token_revocation, group) + bad_request!('Must be a top-level group') if group.subgroup? + authorize! :admin_group, group + + result = ::Groups::AgnosticTokenRevocationService.new(group, current_user, params[:token]).execute + + if result.success? + status :ok + present result.payload[:token], with: "API::Entities::#{result.payload[:type]}".constantize + else + # No matter the error, we always return a 422. + # This prevents disclosing cases like: token is invalid, + # or token is valid but in a different group. + unprocessable_entity! + end + end end end end diff --git a/lib/api/validations/types/comma_separated_to_array.rb b/lib/api/validations/types/comma_separated_to_array.rb index 409eb67a3d3..a4320448ee2 100644 --- a/lib/api/validations/types/comma_separated_to_array.rb +++ b/lib/api/validations/types/comma_separated_to_array.rb @@ -5,7 +5,7 @@ module API module Types class CommaSeparatedToArray def self.coerce - lambda do |value| + ->(value) do case value when String value.split(',').map(&:strip) diff --git a/lib/api/validations/types/comma_separated_to_integer_array.rb b/lib/api/validations/types/comma_separated_to_integer_array.rb index b8ab08b3fd4..dc27924cdb4 100644 --- a/lib/api/validations/types/comma_separated_to_integer_array.rb +++ b/lib/api/validations/types/comma_separated_to_integer_array.rb @@ -5,7 +5,7 @@ module API module Types class CommaSeparatedToIntegerArray < CommaSeparatedToArray def self.coerce - lambda do |value| + ->(value) do super.call(value).map(&:to_i) end end diff --git a/lib/api/validations/types/hash_of_integer_values.rb b/lib/api/validations/types/hash_of_integer_values.rb index 05821acfad5..f9b8b21c6b8 100644 --- a/lib/api/validations/types/hash_of_integer_values.rb +++ b/lib/api/validations/types/hash_of_integer_values.rb @@ -5,7 +5,7 @@ module API module Types class HashOfIntegerValues def self.coerce - lambda do |value| + ->(value) do case value when Hash value.transform_values(&:to_i) diff --git a/lib/api/validations/validators/check_assignees_count.rb b/lib/api/validations/validators/check_assignees_count.rb index 7d90f030f1a..9f6a386c091 100644 --- a/lib/api/validations/validators/check_assignees_count.rb +++ b/lib/api/validations/validators/check_assignees_count.rb @@ -5,7 +5,7 @@ module API module Validators class CheckAssigneesCount < Grape::Validations::Validators::Base def self.coerce - lambda do |value| + ->(value) do case value when String, Array Array.wrap(value) diff --git a/lib/banzai/filter/ascii_doc_sanitization_filter.rb b/lib/banzai/filter/ascii_doc_sanitization_filter.rb index adb9507b4eb..cd49dead0d1 100644 --- a/lib/banzai/filter/ascii_doc_sanitization_filter.rb +++ b/lib/banzai/filter/ascii_doc_sanitization_filter.rb @@ -71,7 +71,7 @@ module Banzai class << self def remove_disallowed_ids - lambda do |env| + ->(env) do node = env[:node] return unless node.name == 'a' || node.name == 'div' || SECTION_HEADINGS.any?(node.name) @@ -88,7 +88,7 @@ module Banzai end def remove_element_classes - lambda do |env| + ->(env) do node = env[:node] return unless (classes_allowlist = ELEMENT_CLASSES_ALLOWLIST[node.name.to_sym]) diff --git a/lib/banzai/filter/base_sanitization_filter.rb b/lib/banzai/filter/base_sanitization_filter.rb index 7b484b22092..a3ee57bddf4 100644 --- a/lib/banzai/filter/base_sanitization_filter.rb +++ b/lib/banzai/filter/base_sanitization_filter.rb @@ -80,7 +80,7 @@ module Banzai class << self def remove_rel - lambda do |env| + ->(env) do if env[:node_name] == 'a' # we allow rel="license" to support the Rel-license microformat # http://microformats.org/wiki/rel-license diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 577d02cc559..93f1e4dd93a 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -46,7 +46,7 @@ module Banzai class << self def remove_unsafe_table_style - lambda do |env| + ->(env) do node = env[:node] return unless node.name == 'th' || node.name == 'td' @@ -61,7 +61,7 @@ module Banzai end def remove_unsafe_link_class - lambda do |env| + ->(env) do node = env[:node] return unless node.name == 'a' @@ -78,7 +78,7 @@ module Banzai end def remove_id_attributes - lambda do |env| + ->(env) do node = env[:node] return unless node.name == 'a' || node.name == 'li' diff --git a/lib/gitlab/action_cable/request_store_callbacks.rb b/lib/gitlab/action_cable/request_store_callbacks.rb index f6dda18a444..bf61d62b1eb 100644 --- a/lib/gitlab/action_cable/request_store_callbacks.rb +++ b/lib/gitlab/action_cable/request_store_callbacks.rb @@ -8,7 +8,7 @@ module Gitlab end def self.wrapper - lambda do |_, inner| + ->(_, inner) do ::Gitlab::SafeRequestStore.ensure_request_store do inner.call end diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb index 67c0243e96d..9153f3c8b44 100644 --- a/lib/gitlab/checks/diff_check.rb +++ b/lib/gitlab/checks/diff_check.rb @@ -67,7 +67,7 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def lfs_file_locks_validation - lambda do |paths| + ->(paths) do lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user_access.user.id).take if lfs_lock diff --git a/lib/gitlab/database/load_balancing/action_cable_callbacks.rb b/lib/gitlab/database/load_balancing/action_cable_callbacks.rb index fab691117ad..0e97f49eb49 100644 --- a/lib/gitlab/database/load_balancing/action_cable_callbacks.rb +++ b/lib/gitlab/database/load_balancing/action_cable_callbacks.rb @@ -9,7 +9,7 @@ module Gitlab end def self.wrapper - lambda do |_, inner| + ->(_, inner) do inner.call ensure ::Gitlab::Database::LoadBalancing.release_hosts diff --git a/lib/gitlab/middleware/rack_multipart_tempfile_factory.rb b/lib/gitlab/middleware/rack_multipart_tempfile_factory.rb index 1686c3324b4..5e68288ca87 100644 --- a/lib/gitlab/middleware/rack_multipart_tempfile_factory.rb +++ b/lib/gitlab/middleware/rack_multipart_tempfile_factory.rb @@ -5,7 +5,7 @@ module Gitlab class RackMultipartTempfileFactory # Immediately unlink the created temporary file so we don't have to rely # on Rack::TempfileReaper catching this after the fact. - FACTORY = lambda do |filename, content_type| + FACTORY = ->(filename, content_type) do Rack::Multipart::Parser::TEMPFILE_FACTORY.call(filename, content_type).tap(&:unlink) end diff --git a/lib/gitlab/rack_attack.rb b/lib/gitlab/rack_attack.rb index 113c6dec6c1..23a1be3eab4 100644 --- a/lib/gitlab/rack_attack.rb +++ b/lib/gitlab/rack_attack.rb @@ -12,7 +12,7 @@ module Gitlab rack_attack::Request.include(Gitlab::RackAttack::Request) # This is Rack::Attack::DEFAULT_THROTTLED_RESPONSE, modified to allow a custom response - rack_attack.throttled_responder = lambda do |request| + rack_attack.throttled_responder = ->(request) do throttled_headers = Gitlab::RackAttack.throttled_response_headers( request.env['rack.attack.matched'], request.env['rack.attack.match_data'] ) diff --git a/lib/gitlab/sidekiq_config/worker_matcher.rb b/lib/gitlab/sidekiq_config/worker_matcher.rb index e6b628da0e6..b35c0bfbb1f 100644 --- a/lib/gitlab/sidekiq_config/worker_matcher.rb +++ b/lib/gitlab/sidekiq_config/worker_matcher.rb @@ -12,7 +12,7 @@ module Gitlab QUERY_PREDICATES = { worker_name: :to_s, feature_category: :to_sym, - has_external_dependencies: lambda { |value| value == 'true' }, + has_external_dependencies: ->(value) { value == 'true' }, name: :to_s, resource_boundary: :to_sym, tags: :to_sym, @@ -35,17 +35,17 @@ module Gitlab private def query_string_to_lambda(query_string) - return lambda { |_worker| true } if query_string.strip == WILDCARD_MATCH + return ->(_worker) { true } if query_string.strip == WILDCARD_MATCH or_clauses = query_string.split(QUERY_OR_OPERATOR).map do |and_clauses_string| and_clauses_predicates = and_clauses_string.split(QUERY_AND_OPERATOR).map do |term| predicate_for_term(term) end - lambda { |worker| and_clauses_predicates.all? { |predicate| predicate.call(worker) } } + ->(worker) { and_clauses_predicates.all? { |predicate| predicate.call(worker) } } end - lambda { |worker| or_clauses.any? { |predicate| predicate.call(worker) } } + ->(worker) { or_clauses.any? { |predicate| predicate.call(worker) } } end def predicate_for_term(term) @@ -63,7 +63,7 @@ module Gitlab when '=' predicate when '!=' - lambda { |worker| !predicate.call(worker) } + ->(worker) { !predicate.call(worker) } else # This is unreachable because InvalidTerm will be raised instead, but # keeping it allows to guard against that changing in future. @@ -76,7 +76,7 @@ module Gitlab raise UnknownPredicate, "Unknown predicate: #{lhs}" unless values_block - lambda do |queue| + ->(queue) do comparator = Array(queue[lhs.to_sym]).to_set values.map(&values_block).to_set.intersect?(comparator) diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb index 96b0dd679de..66b5203f7fe 100644 --- a/lib/gitlab/sidekiq_middleware.rb +++ b/lib/gitlab/sidekiq_middleware.rb @@ -8,7 +8,7 @@ module Gitlab # Sidekiq's `config.server_middleware` method # eg: `config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator)` def self.server_configurator(metrics: true, arguments_logger: true, skip_jobs: true) - lambda do |chain| + ->(chain) do # Size limiter should be placed at the top chain.add ::Gitlab::SidekiqMiddleware::SizeLimiter::Server chain.add ::Gitlab::SidekiqMiddleware::ShardAwarenessValidator @@ -55,7 +55,7 @@ module Gitlab # Sidekiq's `config.client_middleware` method # eg: `config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)` def self.client_configurator - lambda do |chain| + ->(chain) do chain.add ::Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware chain.add ::Labkit::Middleware::Sidekiq::Client # Sidekiq Client Middleware should be placed before DuplicateJobs::Client middleware, diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb index 1e482901929..d712435b518 100644 --- a/lib/gitlab/utils/usage_data.rb +++ b/lib/gitlab/utils/usage_data.rb @@ -121,7 +121,7 @@ module Gitlab def histogram(relation, column, buckets:, bucket_size: buckets.size) with_metadata do # Using lambda to avoid exposing histogram specific methods - parameters_valid = lambda do + parameters_valid = -> do error_message = if buckets.first == buckets.last 'Lower bucket bound cannot equal to upper bucket bound' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 62b989417ae..a5d671ad035 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2288,9 +2288,6 @@ msgstr "" msgid "API key" msgstr "" -msgid "API token (if needed)" -msgstr "" - msgid "API?" msgstr "" @@ -3479,13 +3476,13 @@ msgstr "" msgid "Admin notes" msgstr "" -msgid "AdminAIPoweredFeatures|AI-Powered Features" +msgid "AdminAIPoweredFeatures|AI-powered features" msgstr "" -msgid "AdminAIPoweredFeatures|AI-Powered Features that can be enabled, disabled or linked to a cloud-based or self-hosted model." +msgid "AdminAIPoweredFeatures|AI-powered features that can be enabled, disabled or linked to a cloud-based or self-hosted model." msgstr "" -msgid "AdminAiPoweredFeatures|AI-Powered Features" +msgid "AdminAiPoweredFeatures|AI-powered features" msgstr "" msgid "AdminAiPoweredFeatures|Choose the AI model for %{feature_name} feature" @@ -3722,10 +3719,22 @@ msgstr "" msgid "AdminProjects|Delete Project %{projectName}?" msgstr "" +msgid "AdminSelfHostedModels|API token (if needed)" +msgstr "" + +msgid "AdminSelfHostedModels|Add self-hosted language models to use as backups for GitLab Duo features." +msgstr "" + +msgid "AdminSelfHostedModels|An API key is set" +msgstr "" + msgid "AdminSelfHostedModels|An error occurred while loading self-hosted models. Please try again." msgstr "" -msgid "AdminSelfHostedModels|By enabling Self-Hosted Models, you accept the %{link_start}GitLab Testing Agreement%{link_end}." +msgid "AdminSelfHostedModels|By enabling self-hosted models, you accept the %{link_start}GitLab Testing Agreement%{link_end}." +msgstr "" + +msgid "AdminSelfHostedModels|Create model" msgstr "" msgid "AdminSelfHostedModels|Define your set of self-hosted models" @@ -3734,7 +3743,10 @@ msgstr "" msgid "AdminSelfHostedModels|Delete model" msgstr "" -msgid "AdminSelfHostedModels|Enable Self-hosted models" +msgid "AdminSelfHostedModels|Edit self-hosted model" +msgstr "" + +msgid "AdminSelfHostedModels|Enable self-hosted models" msgstr "" msgid "AdminSelfHostedModels|Endpoint" @@ -3746,15 +3758,15 @@ msgstr "" msgid "AdminSelfHostedModels|Model has API key" msgstr "" +msgid "AdminSelfHostedModels|Name the deployment (must be unique)" +msgstr "" + msgid "AdminSelfHostedModels|New model" msgstr "" msgid "AdminSelfHostedModels|New self-hosted model" msgstr "" -msgid "AdminSelfHostedModels|Self-Hosted models" -msgstr "" - msgid "AdminSelfHostedModels|Self-hosted AI models can be used to power GitLab AI features." msgstr "" @@ -3764,9 +3776,6 @@ msgstr "" msgid "AdminSelfHostedModels|Self-hosted models are an experimental feature." msgstr "" -msgid "AdminSelfHostedModels|They point to the self-hosted AI models that can be used for backing up GitLab AI features." -msgstr "" - msgid "AdminSettings|%{generate_manually_link_start}Generate%{generate_manually_link_end} Service Ping to preview and download service usage data payload." msgstr "" @@ -4649,7 +4658,7 @@ msgstr "" msgid "Administrators are not permitted to connect applications with these scopes: %{code_open}api%{code_close}, %{code_open}read_api%{code_close}, %{code_open}read_repository%{code_close}, %{code_open}write_repository%{code_close}, %{code_open}write_registry%{code_close}, %{code_open}read_registry%{code_close}, and %{code_open}sudo%{code_close}. To permit this, change the %{code_open}disable_admin_oauth_scopes%{code_close} setting using the API." msgstr "" -msgid "Admin|AI-Powered Features" +msgid "Admin|AI-powered features" msgstr "" msgid "Admin|Abuse reports" @@ -4676,6 +4685,9 @@ msgstr "" msgid "Admin|Credentials" msgstr "" +msgid "Admin|Custom models" +msgstr "" + msgid "Admin|Deploy Keys" msgstr "" @@ -4688,9 +4700,6 @@ msgstr "" msgid "Admin|GitLab Duo" msgstr "" -msgid "Admin|GitLab Duo Pro" -msgstr "" - msgid "Admin|Kubernetes" msgstr "" @@ -4703,9 +4712,6 @@ msgstr "" msgid "Admin|Messages" msgstr "" -msgid "Admin|Models" -msgstr "" - msgid "Admin|Monitoring" msgstr "" @@ -5450,9 +5456,6 @@ msgstr "" msgid "An %{link_start}alert%{link_end} with the same fingerprint is already open. To change the status of this alert, resolve the linked alert." msgstr "" -msgid "An API key is set" -msgstr "" - msgid "An Administrator has set the maximum expiration date to %{maxDate}. %{helpLinkStart}Learn more%{helpLinkEnd}." msgstr "" @@ -15349,9 +15352,6 @@ msgstr "" msgid "Create New Directory" msgstr "" -msgid "Create Self-Hosted Model" -msgstr "" - msgid "Create a GitLab account first, and then connect it to your %{label} account." msgstr "" @@ -19587,9 +19587,6 @@ msgstr "" msgid "Edit Deploy Key" msgstr "" -msgid "Edit Edit Self-Hosted Model" -msgstr "" - msgid "Edit Geo Site" msgstr "" @@ -19608,9 +19605,6 @@ msgstr "" msgid "Edit Release" msgstr "" -msgid "Edit Self-Hosted Model" -msgstr "" - msgid "Edit Slack integration" msgstr "" @@ -33957,9 +33951,6 @@ msgstr "" msgid "Name must start with a letter, digit, emoji, or underscore." msgstr "" -msgid "Name the deployment (must be unique)" -msgstr "" - msgid "Name to be used as the sender for emails from Service Desk." msgstr "" @@ -34451,9 +34442,6 @@ msgstr "" msgid "New Security findings" msgstr "" -msgid "New Self-Hosted Model" -msgstr "" - msgid "New Snippet" msgstr "" @@ -49041,12 +49029,6 @@ msgstr "" msgid "Self-Hosted Model was updated" msgstr "" -msgid "Self-Hosted Models" -msgstr "" - -msgid "Self-Hosted models" -msgstr "" - msgid "Self-hosted models dropdown" msgstr "" diff --git a/qa/Gemfile b/qa/Gemfile index 1f3e3dfdc6c..1d25f1a5451 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -28,7 +28,7 @@ gem 'zeitwerk', '~> 2.6', '>= 2.6.15' gem 'influxdb-client', '~> 3.1' gem 'terminal-table', '~> 3.0.2', require: false gem 'slack-notifier', '~> 2.4', require: false -gem 'googleauth', '~> 1.11.0', require: false # see: https://gitlab.com/gitlab-org/gitlab/-/issues/449019 +gem 'googleauth', '~> 1.9.0', require: false # see: https://gitlab.com/gitlab-org/gitlab/-/issues/449019 gem 'fog-google', '~> 1.19', require: false gem 'fog-core', '2.1.0', require: false # fog-google generates a ton of warnings with latest core gem "warning", "~> 1.4" diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index db8d2659c76..750329abe8f 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -184,7 +184,7 @@ GEM google-apis-core (>= 0.15.0, < 2.a) google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) - googleauth (1.11.0) + googleauth (1.9.2) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) @@ -406,7 +406,7 @@ DEPENDENCIES gitlab-qa (~> 14, >= 14.11.0) gitlab-utils! gitlab_quality-test_tooling (~> 1.30.0) - googleauth (~> 1.11.0) + googleauth (~> 1.9.0) influxdb-client (~> 3.1) junit_merge (~> 0.1.2) knapsack (~> 4.0) @@ -430,4 +430,4 @@ DEPENDENCIES zeitwerk (~> 2.6, >= 2.6.15) BUNDLED WITH - 2.5.11 + 2.5.14 diff --git a/rubocop/cop/inject_enterprise_edition_module.rb b/rubocop/cop/inject_enterprise_edition_module.rb index 24b679e092d..5ecb8baf82e 100644 --- a/rubocop/cop/inject_enterprise_edition_module.rb +++ b/rubocop/cop/inject_enterprise_edition_module.rb @@ -82,7 +82,7 @@ module RuboCop # sometimes code needs to be refactored to make this work. As such, we # only allow developers to easily denylist existing offenses. def corrector(node) - lambda do |corrector| + ->(corrector) do corrector.insert_after( node.source_range, " # rubocop: disable #{cop_name}" diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline index d4a62ca44ec..ff56e065126 100755 --- a/scripts/generate-e2e-pipeline +++ b/scripts/generate-e2e-pipeline @@ -49,7 +49,6 @@ variables: QA_SUITES: "$QA_SUITES" QA_TESTS: "$QA_TESTS" KNAPSACK_TEST_FILE_PATTERN: "$KNAPSACK_TEST_FILE_PATTERN" - COVERBAND_ENABLED: "${COVERBAND_ENABLED:-false}" YML ) diff --git a/spec/controllers/concerns/routable_actions_spec.rb b/spec/controllers/concerns/routable_actions_spec.rb index f3adaaea819..7bd55100f64 100644 --- a/spec/controllers/concerns/routable_actions_spec.rb +++ b/spec/controllers/concerns/routable_actions_spec.rb @@ -168,7 +168,7 @@ RSpec.describe RoutableActions do end it 'performs checks in the context of the controller' do - check = lambda { |routable, path_info| redirect_to routable } + check = ->(routable, path_info) { redirect_to routable } allow(subject).to receive(:not_found_actions).and_return([check]) get_routable(routable) diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 14b5d5d5006..c4994b90e12 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -23,6 +23,7 @@ RSpec.describe 'Database schema', feature_category: :database do p_ci_builds: [%w[partition_id stage_id], %w[partition_id execution_config_id]], # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142804#note_1745483081 ci_stages: [%w[partition_id pipeline_id]], # the index on pipeline_id is sufficient ai_testing_terms_acceptances: %w[user_id], # testing terms only have 1 entry, and if the user is deleted the record should remain + p_ci_builds_execution_configs: [%w[partition_id pipeline_id]], # the index on pipeline_id is enough ci_sources_pipelines: [%w[source_partition_id source_pipeline_id], %w[partition_id pipeline_id]], snippets: %w[organization_id] # this index is added in an async manner, hence it needs to be ignored in the first phase. }.with_indifferent_access.freeze diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb index 379178f1a41..f8366d43b05 100644 --- a/spec/factories/deploy_tokens.rb +++ b/spec/factories/deploy_tokens.rb @@ -2,7 +2,6 @@ FactoryBot.define do factory :deploy_token do - token_encrypted { Gitlab::CryptoHelper.aes256_gcm_encrypt(SecureRandom.hex(50)) } sequence(:name) { |n| "PDT #{n}" } read_repository { true } read_registry { true } diff --git a/spec/factories/group_deploy_tokens.rb b/spec/factories/group_deploy_tokens.rb index 9ec7d0701be..fb070bf6267 100644 --- a/spec/factories/group_deploy_tokens.rb +++ b/spec/factories/group_deploy_tokens.rb @@ -3,6 +3,6 @@ FactoryBot.define do factory :group_deploy_token do group - deploy_token + association :deploy_token, factory: [:deploy_token, :group] end end diff --git a/spec/features/groups/dependency_proxy_for_containers_spec.rb b/spec/features/groups/dependency_proxy_for_containers_spec.rb index 93746ea8d54..dd5f5389370 100644 --- a/spec/features/groups/dependency_proxy_for_containers_spec.rb +++ b/spec/features/groups/dependency_proxy_for_containers_spec.rb @@ -23,7 +23,7 @@ RSpec.describe 'Group Dependency Proxy for containers', :js, feature_category: : end let_it_be(:external_server) do - handler = lambda do |env| + handler = ->(env) do if env['REQUEST_PATH'] == '/token' [200, {}, [{ token: 'token' }.to_json]] else diff --git a/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb b/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb index 918abb62b3c..94d50f8824e 100644 --- a/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb +++ b/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe 'User closes/reopens a merge request', :js, feature_category: :code_review_workflow do - let_it_be(:project) { create(:project, :repository) } - let_it_be(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } before do project.add_developer(user) diff --git a/spec/graphql/types/base_object_spec.rb b/spec/graphql/types/base_object_spec.rb index 2bf9f8e3f4d..7931d761db1 100644 --- a/spec/graphql/types/base_object_spec.rb +++ b/spec/graphql/types/base_object_spec.rb @@ -226,7 +226,7 @@ RSpec.describe Types::BaseObject, feature_category: :api do } } - doc = lambda do |after| + doc = ->(after) do GraphQL.parse(<<~GQL) query { x { diff --git a/spec/lib/gitlab/action_cable/request_store_callbacks_spec.rb b/spec/lib/gitlab/action_cable/request_store_callbacks_spec.rb index 3b73252709c..8cdef63bba0 100644 --- a/spec/lib/gitlab/action_cable/request_store_callbacks_spec.rb +++ b/spec/lib/gitlab/action_cable/request_store_callbacks_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::ActionCable::RequestStoreCallbacks do described_class.wrapper.call( nil, - lambda do + -> do expect(RequestStore.active?).to eq(true) end ) diff --git a/spec/lib/gitlab/cross_project_access/class_methods_spec.rb b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb index 3a6e528c9b0..2c048379c55 100644 --- a/spec/lib/gitlab/cross_project_access/class_methods_spec.rb +++ b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::CrossProjectAccess::ClassMethods do end end - let(:dummy_proc) { lambda { false } } + let(:dummy_proc) { -> { false } } describe '#requires_cross_project_access' do it 'creates a correct check when a hash is passed' do diff --git a/spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb b/spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb index a57f02b22df..2c69f39f131 100644 --- a/spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ActionCableCallbacks, :request_s expect(Gitlab::Database::LoadBalancing).to receive(:release_hosts) expect(Gitlab::Database::LoadBalancing::Session).to receive(:clear_session) - described_class.wrapper.call(nil, lambda {}) + described_class.wrapper.call(nil, -> {}) end context 'with an exception' do @@ -17,7 +17,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ActionCableCallbacks, :request_s expect(Gitlab::Database::LoadBalancing::Session).to receive(:clear_session) expect do - described_class.wrapper.call(nil, lambda { raise 'test_exception' }) + described_class.wrapper.call(nil, -> { raise 'test_exception' }) end.to raise_error('test_exception') end end diff --git a/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb b/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb index 02c4ea4df27..aad839d1659 100644 --- a/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb +++ b/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb @@ -6,7 +6,7 @@ require 'tempfile' RSpec.describe Gitlab::Middleware::RackMultipartTempfileFactory do let(:app) do - lambda do |env| + ->(env) do params = Rack::Request.new(env).params if params['file'] diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index b61350592a1..786f9f98978 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -176,7 +176,7 @@ RSpec.describe Gitlab::PathRegex do describe 'TOP_LEVEL_ROUTES' do it 'includes all the top level namespaces' do - failure_block = lambda do + failure_block = -> do missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES additional_words = described_class::TOP_LEVEL_ROUTES - top_level_words failure_message('TOP_LEVEL_ROUTES', 'rename_root_paths', @@ -200,7 +200,7 @@ RSpec.describe Gitlab::PathRegex do describe 'GROUP_ROUTES' do it "don't contain a second wildcard" do - failure_block = lambda do + failure_block = -> do missing_words = paths_after_group_id - described_class::GROUP_ROUTES additional_words = described_class::GROUP_ROUTES - paths_after_group_id failure_message('GROUP_ROUTES', 'rename_child_paths', diff --git a/spec/models/members/members/member_approval_spec.rb b/spec/models/members/members/member_approval_spec.rb index 1ac1e98c561..d4385cb1de6 100644 --- a/spec/models/members/members/member_approval_spec.rb +++ b/spec/models/members/members/member_approval_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Members::MemberApproval, feature_category: :groups_and_projects d it { is_expected.to be_valid } end - context 'with valid expiry' do + context 'with date expiry' do let(:expiry) { "1970-01-01" } it { is_expected.to be_valid } @@ -45,6 +45,12 @@ RSpec.describe Members::MemberApproval, feature_category: :groups_and_projects d it { is_expected.to be_valid } end + context 'with nil expiry' do + let(:expiry) { nil } + + it { is_expected.to be_valid } + end + context 'with not null member_role_id' do let(:attribute_mapping) do { diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index f957ca0a6bf..86dbd91eac0 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -3150,4 +3150,117 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do end end end + + describe 'POST groups/:id/tokens/revoke' do + let(:token) { 'glprefix-AABBCCDDEE1122334455' } + let(:service_response) { ServiceResponse.error(message: '') } + let(:service) { instance_double(service_class, execute: service_response) } + let(:service_class) { Groups::AgnosticTokenRevocationService } + let_it_be(:group) { create(:group, :with_hierarchy, children: 1) } + + let(:path) { "/groups/#{group.id}/tokens/revoke" } + + before do + allow(service_class).to receive(:new).and_return(service) + end + + shared_examples 'revoking token fails' do |status, message| + it 'cannot revoke token' do + revoke_token + + expect(response).to have_gitlab_http_status(status) + expect(json_response['message'] || json_response['error']).to include(message) + end + end + + context 'when not a group owner' do + subject(:revoke_token) { post api(path, user1), params: { token: token } } + + before do + group.add_maintainer(user1) + end + + it_behaves_like 'revoking token fails', :forbidden, 'Forbidden' + end + + context 'when authenticated as a group owner' do + subject(:revoke_token) { post api(path, user1), params: { token: token } } + + before do + group.add_owner(user1) + end + + context 'when group is a top level group' do + it 'calls revocation service' do + revoke_token + expect(service_class).to have_received(:new).with(group, user1, token) + end + + context 'when the service returns successfully' do + let(:token) { create(:personal_access_token, :revoked) } + let(:service_response) do + ServiceResponse.success( + message: 'PersonalAccessToken is revoked', + payload: { + token: token, + type: 'PersonalAccessToken' + } + ) + end + + it 'renders the token with a presenter', :aggregate_failures do + revoke_token + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.with_indifferent_access).to include(revoked: true, id: token.id) + expect(json_response.keys).not_to include(%w[token token_digest]) + end + end + + context 'when the service returns unsuccessfully' do + let(:service_response) do + ServiceResponse.error( + message: 'Some error' + ) + end + + it_behaves_like 'revoking token fails', :unprocessable_entity, 'Unprocessable Entity' + end + + context 'when ff disabled' do + before do + Feature.disable(:group_agnostic_token_revocation, group) + end + + it_behaves_like 'revoking token fails', :not_found, 'Not Found' + + it 'does not call revocation service' do + revoke_token + expect(service_class).not_to have_received(:new) + end + end + end + + context 'when group does not exist' do + let(:path) { "/groups/0/tokens/revoke" } + + it_behaves_like 'revoking token fails', :not_found, 'Group Not Found' + + it 'does not call revocation service' do + revoke_token + expect(service_class).not_to have_received(:new) + end + end + + context 'when group is a subgroup' do + let(:path) { "/groups/#{group.children.first.id}/tokens/revoke" } + + it_behaves_like 'revoking token fails', :bad_request, 'Must be a top-level' + + it 'does not call revocation service' do + revoke_token + expect(service_class).not_to have_received(:new) + end + end + end + end end diff --git a/spec/services/groups/agnostic_token_revocation_service_spec.rb b/spec/services/groups/agnostic_token_revocation_service_spec.rb new file mode 100644 index 00000000000..7dcbf747f01 --- /dev/null +++ b/spec/services/groups/agnostic_token_revocation_service_spec.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::AgnosticTokenRevocationService, feature_category: :system_access do + let_it_be(:group) { create(:group) } + let_it_be(:owner) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:user) { create(:user) } + + before_all do + group.add_owner(owner) + group.add_maintainer(maintainer) + end + + describe '#initialize' do + subject(:result) { described_class.new(group, owner, 'plaintext') } + + it 'accepts a group, user, and plaintext' do + expect(result).to be_a(described_class) + end + end + + shared_examples_for 'a successfully revoked token' do + it { expect(result.success?).to be(true), result.message } + + it 'revokes the token' do + result + expect(token.reload).to be_revoked + end + + it 'returns the token in the payload' do + result + expect(result.payload[:token]).to eq(token) + end + + it 'returns the token class in the payload' do + result + expect(result.payload[:type]).to be(type) + end + end + + shared_examples_for 'an unsuccessfully revoked token' do + it { expect(result.success?).to be(false) } + + it 'does not revoke the token' do + result + expect(token.reload.revoked?).to be(false) + end + end + + describe '#execute' do + subject(:result) { service.execute } + + let_it_be(:member) { create(:user, guest_of: group) } + + let(:service) { described_class.new(group, owner, token.token) } + + context 'with a personal access token' do + let(:type) { 'PersonalAccessToken' } + + context 'when it can access the group' do + let_it_be(:token) { create(:personal_access_token, user: member) } + + it_behaves_like 'a successfully revoked token' + end + + context 'when it can access a sub group' do + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:member) { create(:user, guest_of: group) } + let_it_be(:token) { create(:personal_access_token, user: member) } + + it_behaves_like 'a successfully revoked token' + end + + context 'when it can access a group\'s project' do + let_it_be(:project) { create(:project, group: group) } + let_it_be(:member) { create(:user, :project_bot, guest_of: project) } + let_it_be(:token) { create(:personal_access_token, user: member) } + + it_behaves_like 'a successfully revoked token' + end + + context 'when it belongs to a member with no relation to the group' do + let_it_be(:token) { create(:personal_access_token, user: user) } + + it_behaves_like 'an unsuccessfully revoked token' + end + + context 'when it belongs to a member of multiple groups' do + let_it_be(:group_b) { create(:group) } + let_it_be(:member) { create(:user, guest_of: [group, group_b]) } + let_it_be(:token) { create(:personal_access_token, user: member) } + + it_behaves_like 'a successfully revoked token' + end + + context 'with an already revoked personal access token that can access the group' do + let(:token) { create(:personal_access_token, user: member, revoked: true) } + + it_behaves_like 'a successfully revoked token' + end + + context 'with an already expired token' do + let(:token) { create(:personal_access_token, :expired, user: member) } + + it_behaves_like 'an unsuccessfully revoked token' + end + + context 'with an already expired and revoked token' do + let(:token) { create(:personal_access_token, :expired, revoked: true, user: member) } + + it_behaves_like 'a successfully revoked token' + end + end + + context 'with a group deploy token' do + let(:type) { 'DeployToken' } + + let_it_be(:subgroup) { create(:group, parent: group) } + + context 'when it can access the group' do + let_it_be(:token) { create(:group_deploy_token, group: group).deploy_token } + + it_behaves_like 'a successfully revoked token' + end + + context 'when it can access a subgroup' do + let_it_be(:token) { create(:group_deploy_token, group: subgroup).deploy_token } + + it_behaves_like 'a successfully revoked token' + end + + context 'when it belongs to another group' do + let_it_be(:token) { create(:group_deploy_token).deploy_token } + + it_behaves_like 'an unsuccessfully revoked token' + end + + context 'when it belongs to a project' do + let_it_be(:token) { create(:project_deploy_token).deploy_token } + + it_behaves_like 'an unsuccessfully revoked token' + end + end + + context 'with a token that would otherwise be revoked' do + let_it_be(:token) { create(:personal_access_token, user: member) } + + context 'when ff disabled for group' do + before do + Feature.disable(:group_agnostic_token_revocation, group) + end + + it_behaves_like 'an unsuccessfully revoked token' + end + + context 'when group is a subgroup' do + let_it_be(:group) { create(:group, :nested) } + let_it_be(:member) { create(:user, guest_of: group) } + + before_all do + group.add_owner(owner) + end + + it_behaves_like 'an unsuccessfully revoked token' + end + + context 'when current_user is a maintainer' do + let(:service) { described_class.new(group, maintainer, token.token) } + + it_behaves_like 'an unsuccessfully revoked token' + end + + context 'when current_user is not a member' do + let(:service) { described_class.new(group, user, token.token) } + + it_behaves_like 'an unsuccessfully revoked token' + end + end + + context 'with an unsupported token type' do + let(:token) { create(:oauth_access_token) } + + it_behaves_like 'an unsuccessfully revoked token' + end + + context 'with a plaintext that does not exist' do + let(:plaintext) { 'glpat-abc123' } + let(:service) { described_class.new(group, owner, plaintext) } + + it { expect(result.success?).to be(false) } + end + + context 'with a nil plaintext' do + let(:plaintext) { nil } + let(:service) { described_class.new(group, owner, plaintext) } + + it { expect(result.success?).to be(false) } + end + end +end diff --git a/spec/services/groups/autocomplete_service_spec.rb b/spec/services/groups/autocomplete_service_spec.rb index e623c550144..5d4fcb43655 100644 --- a/spec/services/groups/autocomplete_service_spec.rb +++ b/spec/services/groups/autocomplete_service_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Groups::AutocompleteService, feature_category: :groups_and_projec end def expect_labels_to_equal(labels, expected_labels) - extract_title = lambda { |label| label['title'] } + extract_title = ->(label) { label['title'] } expect(labels.map(&extract_title)).to match_array(expected_labels.map(&extract_title)) end diff --git a/spec/services/issues/referenced_merge_requests_service_spec.rb b/spec/services/issues/referenced_merge_requests_service_spec.rb index 26d0fda5345..1ec552f6afa 100644 --- a/spec/services/issues/referenced_merge_requests_service_spec.rb +++ b/spec/services/issues/referenced_merge_requests_service_spec.rb @@ -53,7 +53,7 @@ RSpec.describe Issues::ReferencedMergeRequestsService, feature_category: :team_p # to avoid false negatives reloaded_issue = Issue.find(issue.id) - pipeline_routes = lambda do |merge_requests| + pipeline_routes = ->(merge_requests) do merge_requests.map { |mr| mr.head_pipeline&.project&.full_path } end diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb index d8e5c7dd753..1c09a8b09a1 100644 --- a/spec/services/projects/autocomplete_service_spec.rb +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -241,7 +241,7 @@ RSpec.describe Projects::AutocompleteService, feature_category: :groups_and_proj describe '#labels_as_hash' do def expect_labels_to_equal(labels, expected_labels) expect(labels.size).to eq(expected_labels.size) - extract_title = lambda { |label| label['title'] } + extract_title = ->(label) { label['title'] } expect(labels.map(&extract_title)).to match_array(expected_labels.map(&extract_title)) end diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb index b6016aef801..574e7f36e78 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService, feature_catego let(:remote_uri) { URI.parse(lfs_endpoint) } let(:request_object) { HTTParty::Request.new(Net::HTTP::Post, '/') } - let(:parsed_block) { lambda {} } + let(:parsed_block) { -> {} } let(:success_net_response) { Net::HTTPOK.new('', '', '') } let(:response) { Gitlab::HTTP::Response.new(request_object, net_response, parsed_block) } diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb index 869e4572a6d..7604e4bbefa 100644 --- a/spec/support/helpers/email_helpers.rb +++ b/spec/support/helpers/email_helpers.rb @@ -36,7 +36,7 @@ module EmailHelpers def should_email(user, times: 1, recipients: email_recipients) amount = sent_to_user(user, recipients: recipients) - failed_message = lambda { "User #{user.username} (#{user.id}): email test failed (expected #{times}, got #{amount})" } + failed_message = -> { "User #{user.username} (#{user.id}): email test failed (expected #{times}, got #{amount})" } expect(amount).to eq(times), failed_message end diff --git a/spec/support/helpers/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb index 370dedabd9b..e635af4f54c 100644 --- a/spec/support/helpers/reference_parser_helpers.rb +++ b/spec/support/helpers/reference_parser_helpers.rb @@ -15,7 +15,7 @@ module ReferenceParserHelpers it 'avoids N+1 queries in #nodes_visible_to_user', :use_sql_query_cache do context = Banzai::RenderContext.new(project, user) - request = lambda do |links| + request = ->(links) do described_class.new(context).nodes_visible_to_user(user, links) end @@ -35,7 +35,7 @@ module ReferenceParserHelpers it 'avoids N+1 queries in #records_for_nodes', :request_store do context = Banzai::RenderContext.new(project, user) - record_queries = lambda do |links| + record_queries = ->(links) do ActiveRecord::QueryRecorder.new do described_class.new(context).records_for_nodes(links) end diff --git a/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb index 0a07a56d417..0e79f20eea8 100644 --- a/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples 'write access for a read-only GitLab instance' do include_context 'with a mocked GitLab instance' context 'normal requests to a read-only GitLab instance' do - let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } + let(:fake_app) { ->(env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } it 'expects PATCH requests to be disallowed' do response = request.patch('/test_request') @@ -208,7 +208,7 @@ RSpec.shared_examples 'write access for a read-only GitLab instance' do end context 'JSON requests to a read-only GitLab instance' do - let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } } + let(:fake_app) { ->(env) { [200, { 'Content-Type' => 'application/json' }, ['OK']] } } let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } } before do