diff options
42 files changed, 670 insertions, 130 deletions
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb index 248323a0bb5..9b7cbcb2dfe 100644 --- a/app/controllers/repositories/lfs_api_controller.rb +++ b/app/controllers/repositories/lfs_api_controller.rb @@ -91,12 +91,17 @@ module Repositories def upload_actions(object) { upload: { - href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", + href: "#{upload_http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", header: upload_headers } } end + # Overridden in EE + def upload_http_url_to_repo + project.http_url_to_repo + end + def upload_headers headers = { Authorization: authorization_header, diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index cea28fd4611..a643fea6d5a 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -22,4 +22,34 @@ module InviteMembersHelper def invite_group_members?(group) experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group) end + + def dropdown_invite_members_link(form_model) + link_to invite_members_url(form_model), + data: { + 'track-event': 'click_link', + 'track-label': tracking_label(current_user), + 'track-property': experiment_tracking_category_and_group(:invite_members_new_dropdown, subject: current_user) + } do + invite_member_link_content + end + end + + private + + def invite_members_url(form_model) + case form_model + when Project + project_project_members_path(form_model) + when Group + group_group_members_path(form_model) + end + end + + def invite_member_link_content + text = s_('InviteMember|Invite members') + + return text unless experiment_enabled?(:invite_members_new_dropdown) + + "#{text} #{emoji_icon('shaking_hands', 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe + end end diff --git a/app/models/snippet_repository_storage_move.rb b/app/models/snippet_repository_storage_move.rb index a365569bfa8..bb157c08995 100644 --- a/app/models/snippet_repository_storage_move.rb +++ b/app/models/snippet_repository_storage_move.rb @@ -12,7 +12,11 @@ class SnippetRepositoryStorageMove < ApplicationRecord override :schedule_repository_storage_update_worker def schedule_repository_storage_update_worker - # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/218991 + SnippetUpdateRepositoryStorageWorker.perform_async( + snippet_id, + destination_storage_name, + id + ) end private diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index b5bfbc7bd1c..d89c4bf0161 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -38,6 +38,8 @@ = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :nonce, @pre_auth.nonce + = hidden_field_tag :code_challenge, @pre_auth.code_challenge + = hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method = submit_tag _("Deny"), class: "gl-button btn btn-danger" = form_tag oauth_authorization_path, method: :post, class: 'inline' do = hidden_field_tag :client_id, @pre_auth.client.uid @@ -46,4 +48,6 @@ = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :nonce, @pre_auth.nonce + = hidden_field_tag :code_challenge, @pre_auth.code_challenge + = hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method = submit_tag _("Authorize"), class: "gl-button btn btn-success gl-ml-3", data: { qa_selector: 'authorization_button' } diff --git a/app/views/layouts/header/_group_invite_members_new_dropdown_item.html.haml b/app/views/layouts/header/_group_invite_members_new_dropdown_item.html.haml new file mode 100644 index 00000000000..cb74c77dff8 --- /dev/null +++ b/app/views/layouts/header/_group_invite_members_new_dropdown_item.html.haml @@ -0,0 +1,3 @@ +- return unless Gitlab::Experimentation.active?(:invite_members_new_dropdown) && can?(current_user, :admin_group_member, @group) + +%li= dropdown_invite_members_link(@group) diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 2c5cd7e96c7..1fc9831d271 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -2,7 +2,7 @@ = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do = sprite_icon('plus-square') = sprite_icon('chevron-down', css_class: 'caret-down') - .dropdown-menu.dropdown-menu-right + .dropdown-menu.dropdown-menu-right.dropdown-extended-height %ul - if @group&.persisted? - create_group_project = can?(current_user, :create_projects, @group) @@ -16,6 +16,7 @@ - if create_group_subgroup %li= link_to _('New subgroup'), new_group_path(parent_id: @group.id) = render_if_exists 'layouts/header/create_epic_new_dropdown_item' + = render 'layouts/header/group_invite_members_new_dropdown_item' %li.divider %li.dropdown-bold-header GitLab @@ -33,6 +34,7 @@ %li= link_to _('New merge request'), project_new_merge_request_path(merge_project) - if create_project_snippet %li= link_to _('New snippet'), new_project_snippet_path(@project) + = render 'layouts/header/project_invite_members_new_dropdown_item' %li.divider %li.dropdown-bold-header GitLab - if current_user.can_create_project? diff --git a/app/views/layouts/header/_project_invite_members_new_dropdown_item.html.haml b/app/views/layouts/header/_project_invite_members_new_dropdown_item.html.haml new file mode 100644 index 00000000000..2cb67e857e3 --- /dev/null +++ b/app/views/layouts/header/_project_invite_members_new_dropdown_item.html.haml @@ -0,0 +1,3 @@ +- return unless Gitlab::Experimentation.active?(:invite_members_new_dropdown) && can_import_members? + +%li= dropdown_invite_members_link(@project) diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index c0b49a8624f..e56e13360d7 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -2103,6 +2103,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: snippet_update_repository_storage + :feature_category: :gitaly + :has_external_dependencies: + :urgency: :throttled + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: system_hook_push :feature_category: :source_code_management :has_external_dependencies: diff --git a/app/workers/concerns/update_repository_storage_worker.rb b/app/workers/concerns/update_repository_storage_worker.rb new file mode 100644 index 00000000000..f46b64895a2 --- /dev/null +++ b/app/workers/concerns/update_repository_storage_worker.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module UpdateRepositoryStorageWorker + extend ActiveSupport::Concern + include ApplicationWorker + + included do + idempotent! + feature_category :gitaly + urgency :throttled + end + + def perform(container_id, new_repository_storage_key, repository_storage_move_id = nil) + repository_storage_move = + if repository_storage_move_id + find_repository_storage_move(repository_storage_move_id) + else + # maintain compatibility with workers queued before release + container = find_container(container_id) + container.repository_storage_moves.create!( + source_storage_name: container.repository_storage, + destination_storage_name: new_repository_storage_key + ) + end + + update_repository_storage(repository_storage_move) + end + + private + + def find_repository_storage_move(repository_storage_move_id) + raise NotImplementedError + end + + def find_container(container_id) + raise NotImplementedError + end + + def update_repository_storage(repository_storage_move) + raise NotImplementedError + end +end diff --git a/app/workers/project_update_repository_storage_worker.rb b/app/workers/project_update_repository_storage_worker.rb index 7c0b1ae07fa..5636eec8233 100644 --- a/app/workers/project_update_repository_storage_worker.rb +++ b/app/workers/project_update_repository_storage_worker.rb @@ -1,25 +1,23 @@ # frozen_string_literal: true -class ProjectUpdateRepositoryStorageWorker - include ApplicationWorker +class ProjectUpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker + extend ::Gitlab::Utils::Override + include UpdateRepositoryStorageWorker - idempotent! - feature_category :gitaly - urgency :throttled + private - def perform(project_id, new_repository_storage_key, repository_storage_move_id = nil) - repository_storage_move = - if repository_storage_move_id - ProjectRepositoryStorageMove.find(repository_storage_move_id) - else - # maintain compatibility with workers queued before release - project = Project.find(project_id) - project.repository_storage_moves.create!( - source_storage_name: project.repository_storage, - destination_storage_name: new_repository_storage_key - ) - end + override :find_repository_storage_move + def find_repository_storage_move(repository_storage_move_id) + ProjectRepositoryStorageMove.find(repository_storage_move_id) + end + + override :find_container + def find_container(container_id) + Project.find(container_id) + end + override :update_repository_storage + def update_repository_storage(repository_storage_move) ::Projects::UpdateRepositoryStorageService.new(repository_storage_move).execute end end diff --git a/app/workers/snippet_update_repository_storage_worker.rb b/app/workers/snippet_update_repository_storage_worker.rb new file mode 100644 index 00000000000..a28a02a0298 --- /dev/null +++ b/app/workers/snippet_update_repository_storage_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class SnippetUpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker + extend ::Gitlab::Utils::Override + include UpdateRepositoryStorageWorker + + private + + override :find_repository_storage_move + def find_repository_storage_move(repository_storage_move_id) + SnippetRepositoryStorageMove.find(repository_storage_move_id) + end + + override :find_container + def find_container(container_id) + Snippet.find(container_id) + end + + override :update_repository_storage + def update_repository_storage(repository_storage_move) + ::Snippets::UpdateRepositoryStorageService.new(repository_storage_move).execute + end +end diff --git a/changelogs/unreleased/add-terraform-state-api-usage-tracking.yml b/changelogs/unreleased/add-terraform-state-api-usage-tracking.yml new file mode 100644 index 00000000000..f73bb82811c --- /dev/null +++ b/changelogs/unreleased/add-terraform-state-api-usage-tracking.yml @@ -0,0 +1,5 @@ +--- +title: Track usage for Terraform State API +merge_request: 50224 +author: +type: added diff --git a/changelogs/unreleased/oauth-pkce.yml b/changelogs/unreleased/oauth-pkce.yml new file mode 100644 index 00000000000..731df657f5b --- /dev/null +++ b/changelogs/unreleased/oauth-pkce.yml @@ -0,0 +1,5 @@ +--- +title: Enable OAuth PKCE flow +merge_request: 49756 +author: +type: added diff --git a/config/feature_flags/development/usage_data_p_terraform_state_api_unique_users.yml b/config/feature_flags/development/usage_data_p_terraform_state_api_unique_users.yml new file mode 100644 index 00000000000..e11fb0a8b25 --- /dev/null +++ b/config/feature_flags/development/usage_data_p_terraform_state_api_unique_users.yml @@ -0,0 +1,8 @@ +--- +name: usage_data_p_terraform_state_api_unique_users +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50224 +rollout_issue_url: +milestone: '13.8' +type: development +group: group::configure +default_enabled: true diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index e0c64e4c2b7..f38353e8eb6 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -320,6 +320,8 @@ - 1 - - set_user_status_based_on_user_cap_setting - 1 +- - snippet_update_repository_storage + - 1 - - status_page_publish - 1 - - sync_seat_link_request diff --git a/db/migrate/20201209163958_add_code_challenge_to_oauth_access_grants.rb b/db/migrate/20201209163958_add_code_challenge_to_oauth_access_grants.rb new file mode 100644 index 00000000000..48292cce55b --- /dev/null +++ b/db/migrate/20201209163958_add_code_challenge_to_oauth_access_grants.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class AddCodeChallengeToOauthAccessGrants < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column(:oauth_access_grants, :code_challenge, :text, null: true) unless column_exists?(:oauth_access_grants, :code_challenge) + # If `code_challenge_method` is 'plain' the length is at most 128 characters as per the spec + # https://tools.ietf.org/html/rfc7636#section-4.1 + # Otherwise the max length of base64(SHA256(code_verifier)) is 44 characters + add_text_limit(:oauth_access_grants, :code_challenge, 128, constraint_name: 'oauth_access_grants_code_challenge') + + add_column(:oauth_access_grants, :code_challenge_method, :text, null: true) unless column_exists?(:oauth_access_grants, :code_challenge_method) + # Values are either 'plain' or 'S256' + add_text_limit(:oauth_access_grants, :code_challenge_method, 5, constraint_name: 'oauth_access_grants_code_challenge_method') + end + + def down + remove_column(:oauth_access_grants, :code_challenge) + remove_column(:oauth_access_grants, :code_challenge_method) + end +end diff --git a/db/schema_migrations/20201209163958 b/db/schema_migrations/20201209163958 new file mode 100644 index 00000000000..081f12e64ea --- /dev/null +++ b/db/schema_migrations/20201209163958 @@ -0,0 +1 @@ +4bdd5eba48a76d8feab948857ec32ef7fe25e04e8633ee7d94fd059e73703472
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index cf126999bf2..b66cda410c0 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14373,7 +14373,11 @@ CREATE TABLE oauth_access_grants ( redirect_uri text NOT NULL, created_at timestamp without time zone NOT NULL, revoked_at timestamp without time zone, - scopes character varying + scopes character varying, + code_challenge text, + code_challenge_method text, + CONSTRAINT oauth_access_grants_code_challenge CHECK ((char_length(code_challenge) <= 128)), + CONSTRAINT oauth_access_grants_code_challenge_method CHECK ((char_length(code_challenge_method) <= 5)) ); CREATE SEQUENCE oauth_access_grants_id_seq diff --git a/doc/.vale/gitlab/AlertBoxStyle.yml b/doc/.vale/gitlab/AlertBoxStyle.yml index bdf66236ef2..78db7fa99ae 100644 --- a/doc/.vale/gitlab/AlertBoxStyle.yml +++ b/doc/.vale/gitlab/AlertBoxStyle.yml @@ -17,5 +17,5 @@ nonword: true scope: raw raw: - '(\n *\> *(?:NOTE|WARNING)|' - - '\n(NOTE):[^\n]|' # Adding "WARNING" here causes a false positive - - '\n *(?:> )?\**(Note|note|TIP|Tip|tip|CAUTION|Caution|caution|DANGER|Danger|danger|warning):.*)' ## Adding "Warning" here causes a false positive + - '\n\n(NOTE|WARNING):[^\n]|' + - '\n\n *(?:> )?\**(Note|note|TIP|Tip|tip|CAUTION|Caution|caution|DANGER|Danger|danger|Warning|warning):.*)' diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 50d063bdf71..1607c6910c9 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -2,7 +2,7 @@ type: reference, howto stage: Manage group: Access -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technica l-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers --- # GitLab as an OAuth2 provider @@ -19,17 +19,26 @@ documentation. This functionality is based on the GitLab currently supports the following authorization flows: -- **Web application flow:** Most secure and common type of flow, designed for - applications with secure server-side. -- **Implicit grant flow:** This flow is designed for user-agent only apps (e.g., single - page web application running on GitLab Pages). -- **Resource owner password credentials flow:** To be used **only** for securely - hosted, first-party services. +- **Authorization code with [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636):** + Most secure. Without PKCE, you'd have to include client secrets on mobile clients, + and is recommended for both client and server aoos. +- **Authorization code:** Secure and common flow. Recommended option for secure + server-side apps. +- **Implicit grant:** Originally designed for user-agent only apps, such as + single page web apps running on GitLab Pages). + The [IETF](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-09#section-2.1.2) + recommends against Implicit grant flow. +- **Resource owner password credentials:** To be used **only** for securely + hosted, first-party services. GitLab recommends against use of this flow. + +The draft specification for [OAuth 2.1](https://oauth.net/2.1/) specifically omits both the +Implicit grant and Resource Owner Password Credentials flows. + it will be deprecated in the next OAuth specification version. Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out how all those flows work and pick the right one for your use case. -Both **web application** and **implicit grant** flows require `application` to be +Both **authorization code** (with or without PKCE) and **implicit grant** flows require `application` to be registered first via the `/profile/applications` page in your user's account. During registration, by enabling proper scopes, you can limit the range of resources which the `application` can access. Upon creation, you'll obtain the @@ -57,19 +66,84 @@ These factors are particularly important when using the In the following sections you will find detailed instructions on how to obtain authorization with each flow. -### Web application flow +### Authorization code with Proof Key for Code Exchange (PKCE) + +The [PKCE RFC](https://tools.ietf.org/html/rfc7636#section-1.1) includes a +detailed flow description, from authorization request through access token. +The following steps describe our implementation of the flow. + +The Authorization code with PKCE flow, PKCE for short, makes it possible to securely perform +the OAuth exchange of client credentials for access tokens on public clients. + +Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `CODE_CHALLENGE`. + +- The `STATE` a value that can't be predicted used by the client to maintain + state between the request and callback. It should also be used as a CSRF token. +- The `CODE_VERIFIER` is a random string, between 43 and 128 characters in length, + which use the characters `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, and `~`. +- The `CODE_CHALLENGE` is an URL-safe base64-encoded string of the SHA256 hash of the + `CODE_VERIFIER` + - In Ruby, you can set that up with `Base64.urlsafe_encode64(Digest::SHA256.digest(CODE_VERIFIER))`. + +1. Request authorization code. To do that, you should redirect the user to the + `/oauth/authorize` page with the following query parameters: + + ```plaintext + https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH&scope=REQUESTED_SCOPES&code_challenge=CODE_CHALLENGE&code_challenge_method=S256 + ``` + + This page asks the user to approve the request from the app to access their + account based on the scopes specified in `REQUESTED_SCOPES`. The user is then + redirected back to the specified `REDIRECT_URI`. The [scope parameter](https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes#requesting-particular-scopes) + is a space separated list of scopes associated with the user. + For example,`scope=read_user+profile` requests the `read_user` and `profile` scopes. + The redirect includes the authorization `code`, for example: + + ```plaintext + https://example.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH + ``` + +1. With the authorization `code` returned from the previous request (denoted as + `RETURNED_CODE` in the following example), you can request an `access_token`, with + any HTTP client. The following example uses Ruby's `rest-client`: + + ```ruby + parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI&code_verifier=CODE_VERIFIER' + RestClient.post 'https://gitlab.example.com/oauth/token', parameters + ``` + + Example response: + + ```json + { + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1", + "created_at": 1607635748 + } + ``` + +NOTE: +The `redirect_uri` must match the `redirect_uri` used in the original +authorization request. + +You can now make requests to the API with the access token. + +### Authorization code flow NOTE: Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.1) for a detailed flow description. -The web application flow is: +The authorization code flow is essentially the same as +[authorization code flow with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce), 1. Request authorization code. To do that, you should redirect the user to the `/oauth/authorize` endpoint with the following GET parameters: ```plaintext - https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH&scope=REQUESTED_SCOPES + https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES ``` This will ask the user to approve the applications access to their account @@ -80,7 +154,7 @@ The web application flow is: include the GET `code` parameter, for example: ```plaintext - https://example.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH + https://example.com/oauth/redirect?code=1234567890&state=STATE ``` You should then use `code` to request an access token. @@ -101,7 +175,8 @@ The web application flow is: "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", "token_type": "bearer", "expires_in": 7200, - "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1", + "created_at": 1607635748 } ``` @@ -114,19 +189,20 @@ You can now make requests to the API with the access token returned. ### Implicit grant flow NOTE: -Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.2) for a -detailed flow description. +For a detailed flow diagram, see the [RFC specification](https://tools.ietf.org/html/rfc6749#section-4.2). WARNING: -Avoid using this flow for applications that store data outside of the GitLab -instance. If you do, make sure to verify `application id` associated with the -access token before granting access to the data -(see [`/oauth/token/info`](#retrieving-the-token-information)). - -Unlike the web flow, the client receives an `access token` immediately as a -result of the authorization request. The flow does not use the client secret -or the authorization code because all of the application code and storage is -easily accessible, therefore secrets can leak easily. +The Implicit grant flow is inherently insecure. The IETF plans to remove it in +[OAuth 2.1](https://oauth.net/2.1/). + +We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce) instead. If you choose to use Implicit flow, be sure to verify the +`application id` (or `client_id`) associated with the access token before granting +access to the data, as described in [Retrieving the token information](#retrieving-the-token-information)). + +Unlike the authorization code flow, the client receives an `access token` +immediately as a result of the authorization request. The flow does not use +the client secret or the authorization code because all of the application code +and storage is easily accessible on client browsers and mobile devices. To request the access token, you should redirect the user to the `/oauth/authorize` endpoint using `token` response type: diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index b1896863af9..975a5a6d3d2 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -1031,7 +1031,6 @@ the following Apollo Client warning when passing only handlers: ```shell Unexpected call of console.warn() with: - Warning: mock-apollo-client - The query is entirely client-side (using @client directives) and resolvers have been configured. The request handler will not be called. ``` diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 8c1541f8531..4875a7f9350 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -1268,6 +1268,11 @@ record. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21966) in GitLab 12.7. +WARNING: +The Web Application Firewall is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/271276) +in GitLab 13.6, and planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/271349) +in GitLab 14.0. + A Web Application Firewall (WAF) examines traffic being sent or received, and can block malicious traffic before it reaches your application. The benefits of a WAF are: diff --git a/doc/user/project/clusters/protect/web_application_firewall/index.md b/doc/user/project/clusters/protect/web_application_firewall/index.md index 9851a46fa7a..6e2e71c6ced 100644 --- a/doc/user/project/clusters/protect/web_application_firewall/index.md +++ b/doc/user/project/clusters/protect/web_application_firewall/index.md @@ -6,6 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Web Application Firewall +WARNING: +The Web Application Firewall is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/271276) +in GitLab 13.6, and planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/271349) +in GitLab 14.0. + A web application firewall (or WAF) filters, monitors, and blocks HTTP traffic to and from a web application. By inspecting HTTP traffic, it can prevent attacks stemming from web application security flaws. It can be used to detect SQL injection, diff --git a/doc/user/project/clusters/protect/web_application_firewall/quick_start_guide.md b/doc/user/project/clusters/protect/web_application_firewall/quick_start_guide.md index fe4576bb9c7..e9a05b58fec 100644 --- a/doc/user/project/clusters/protect/web_application_firewall/quick_start_guide.md +++ b/doc/user/project/clusters/protect/web_application_firewall/quick_start_guide.md @@ -6,6 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Getting started with the Web Application Firewall +WARNING: +The Web Application Firewall is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/271276) +in GitLab 13.6, and planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/271349) +in GitLab 14.0. + This is a step-by-step guide to help you use the GitLab [Web Application Firewall](index.md) after deploying a project hosted on GitLab.com to Google Kubernetes Engine using [Auto DevOps](../../../../../topics/autodevops/index.md). diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index c664c0a4590..f6dfbcafbb6 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -14,6 +14,8 @@ module API before do authenticate! authorize! :read_terraform_state, user_project + + increment_unique_values('p_terraform_state_api_unique_users', current_user.id) end params do diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 4abe697263d..8de6eb71613 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -96,6 +96,9 @@ module Gitlab }, pipelines_empty_state: { tracking_category: 'Growth::Activation::Experiment::PipelinesEmptyState' + }, + invite_members_new_dropdown: { + tracking_category: 'Growth::Expansion::Experiment::InviteMembersNewDropdown' } }.freeze diff --git a/lib/gitlab/experimentation/controller_concern.rb b/lib/gitlab/experimentation/controller_concern.rb index ecabfa21f7d..e43f3c8c007 100644 --- a/lib/gitlab/experimentation/controller_concern.rb +++ b/lib/gitlab/experimentation/controller_concern.rb @@ -15,7 +15,7 @@ module Gitlab included do before_action :set_experimentation_subject_id_cookie, unless: :dnt_enabled? - helper_method :experiment_enabled?, :experiment_tracking_category_and_group + helper_method :experiment_enabled?, :experiment_tracking_category_and_group, :tracking_label end def set_experimentation_subject_id_cookie diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb index b61720c7638..51435bbca4e 100644 --- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb +++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb @@ -123,7 +123,7 @@ module Gitlab Gitlab::Redis::HLL.add(key: redis_key(event, time, context), value: value, expiry: expiry(event)) end - # The aray of valid context on which we allow tracking + # The array of valid context on which we allow tracking def valid_context_list Plan.all_plans end diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml index 0d0c787d6d2..ced8e6b53a6 100644 --- a/lib/gitlab/usage_data_counters/known_events/common.yml +++ b/lib/gitlab/usage_data_counters/known_events/common.yml @@ -440,3 +440,9 @@ category: code_review aggregation: weekly feature_flag: usage_data_i_code_review_mr_single_file_diffs +# Terraform +- name: p_terraform_state_api_unique_users + category: terraform + redis_slot: terraform + aggregation: weekly + feature_flag: usage_data_p_terraform_state_api_unique_users diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index bd7ef3db8b6..51ac005883e 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -232,7 +232,7 @@ RSpec.describe ProjectsController do before do sign_in(user) - allow(controller).to receive(:record_experiment_user).with(:invite_members_empty_project_version_a) + allow(controller).to receive(:record_experiment_user) end User.project_views.keys.each do |project_view| diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb index d75b3c9f2e3..914d0931476 100644 --- a/spec/helpers/invite_members_helper_spec.rb +++ b/spec/helpers/invite_members_helper_spec.rb @@ -114,4 +114,69 @@ RSpec.describe InviteMembersHelper do end end end + + describe '#dropdown_invite_members_link' do + shared_examples_for 'dropdown invite members link' do + let(:link_regex) do + /data-track-event="click_link".*data-track-property="_track_property_".*Invite members/ + end + + before do + allow(helper).to receive(:experiment_tracking_category_and_group) { '_track_property_' } + allow(helper).to receive(:tracking_label).with(owner) + allow(helper).to receive(:current_user) { owner } + end + + it 'records the experiment' do + allow(helper).to receive(:experiment_enabled?) + + helper.dropdown_invite_members_link(form_model) + + expect(helper).to have_received(:experiment_tracking_category_and_group) + .with(:invite_members_new_dropdown, subject: owner) + end + + context 'with experiment enabled' do + before do + allow(helper).to receive(:experiment_enabled?).with(:invite_members_new_dropdown) { true } + end + + it 'returns link' do + link = helper.dropdown_invite_members_link(form_model) + + expect(link).to match(link_regex) + expect(link).to include(link_href) + expect(link).to include('gl-emoji') + end + end + + context 'with no experiment enabled' do + before do + allow(helper).to receive(:experiment_enabled?).with(:invite_members_new_dropdown) { false } + end + + it 'returns link' do + link = helper.dropdown_invite_members_link(form_model) + + expect(link).to match(link_regex) + expect(link).to include(link_href) + expect(link).not_to include('gl-emoji') + end + end + end + + context 'with a project' do + let_it_be(:form_model) { project } + let(:link_href) { "href=\"#{project_project_members_path(form_model)}\"" } + + it_behaves_like 'dropdown invite members link' + end + + context 'with a group' do + let_it_be(:form_model) { create(:group) } + let(:link_href) { "href=\"#{group_group_members_path(form_model)}\"" } + + it_behaves_like 'dropdown invite members link' + end + end end diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index f2ec4526464..b390d7c0c9f 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -46,7 +46,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'container_packages', 'tag_packages', 'snippets', - 'code_review' + 'code_review', + 'terraform' ) end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 4d12bb6bd8c..6a613cab999 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1260,7 +1260,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.redis_hll_counters } let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } - let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management incident_management_alerts snippets] } + let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management incident_management_alerts snippets terraform] } it 'has all known_events' do expect(subject).to have_key(:redis_hll_counters) diff --git a/spec/models/snippet_repository_storage_move_spec.rb b/spec/models/snippet_repository_storage_move_spec.rb index c9feff0c22f..357951f8859 100644 --- a/spec/models/snippet_repository_storage_move_spec.rb +++ b/spec/models/snippet_repository_storage_move_spec.rb @@ -8,6 +8,6 @@ RSpec.describe SnippetRepositoryStorageMove, type: :model do let(:repository_storage_factory_key) { :snippet_repository_storage_move } let(:error_key) { :snippet } - let(:repository_storage_worker) { nil } # TODO set to SnippetUpdateRepositoryStorageWorker after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented + let(:repository_storage_worker) { SnippetUpdateRepositoryStorageWorker } end end diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index 0fa088a641e..bfdb5458fd1 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -21,9 +21,36 @@ RSpec.describe API::Terraform::State do stub_terraform_state_object_storage end + shared_examples 'endpoint with unique user tracking' do + context 'without authentication' do + let(:auth_header) { basic_auth_header('bad', 'token') } + + before do + stub_feature_flags(usage_data_p_terraform_state_api_unique_users: false) + end + + it 'does not track unique event' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + request + end + end + + context 'with maintainer permissions' do + let(:current_user) { maintainer } + + it_behaves_like 'tracking unique hll events', :usage_data_p_terraform_state_api_unique_users do + let(:target_id) { 'p_terraform_state_api_unique_users' } + let(:expected_type) { instance_of(Integer) } + end + end + end + describe 'GET /projects/:id/terraform/state/:name' do subject(:request) { get api(state_path), headers: auth_header } + it_behaves_like 'endpoint with unique user tracking' + context 'without authentication' do let(:auth_header) { basic_auth_header('bad', 'token') } @@ -117,6 +144,8 @@ RSpec.describe API::Terraform::State do subject(:request) { post api(state_path), headers: auth_header, as: :json, params: params } + it_behaves_like 'endpoint with unique user tracking' + context 'when terraform state with a given name is already present' do context 'with maintainer permissions' do let(:current_user) { maintainer } @@ -219,6 +248,8 @@ RSpec.describe API::Terraform::State do describe 'DELETE /projects/:id/terraform/state/:name' do subject(:request) { delete api(state_path), headers: auth_header } + it_behaves_like 'endpoint with unique user tracking' + context 'with maintainer permissions' do let(:current_user) { maintainer } @@ -256,6 +287,8 @@ RSpec.describe API::Terraform::State do subject(:request) { post api("#{state_path}/lock"), headers: auth_header, params: params } + it_behaves_like 'endpoint with unique user tracking' + it 'locks the terraform state' do request @@ -305,6 +338,10 @@ RSpec.describe API::Terraform::State do subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params } + it_behaves_like 'endpoint with unique user tracking' do + let(:lock_id) { 'irrelevant to this test, just needs to be present' } + end + context 'with the correct lock id' do let(:lock_id) { '123-456' } diff --git a/spec/support/matchers/be_sorted.rb b/spec/support/matchers/be_sorted.rb index 1455060fe71..b0ab93efbb2 100644 --- a/spec/support/matchers/be_sorted.rb +++ b/spec/support/matchers/be_sorted.rb @@ -4,18 +4,75 @@ # # By default, this checks that the collection is sorted ascending # but you can check order by specific field and order by passing -# them, eg: +# them, either as arguments, or using the fluent interface, eg: # # ``` +# # Usage examples: +# expect(collection).to be_sorted +# expect(collection).to be_sorted(:field) # expect(collection).to be_sorted(:field, :desc) +# expect(collection).to be_sorted.asc +# expect(collection).to be_sorted.desc.by(&:field) +# expect(collection).to be_sorted.by(&:field).desc +# expect(collection).to be_sorted.by { |x| [x.foo, x.bar] } # ``` -RSpec::Matchers.define :be_sorted do |by, order = :asc| +RSpec::Matchers.define :be_sorted do |on = :itself, order = :asc| + def by(&block) + @comparator = block + self + end + + def asc + @direction = :asc + self + end + + def desc + @direction = :desc + self + end + + def format_with(proc) + @format_with = proc + self + end + + define_method :comparator do + @comparator || on + end + + define_method :descending? do + (@direction || order.to_sym) == :desc + end + + def order(items) + descending? ? items.reverse : items + end + + def sort(items) + items.sort_by(&comparator) + end + match do |actual| - next true unless actual.present? # emtpy collection is sorted + next true unless actual.present? # empty collection is sorted + + actual = actual.to_a if actual.respond_to?(:to_a) && !actual.respond_to?(:sort_by) + + @got = actual + @expected = order(sort(actual)) + + @expected == actual + end + + def failure_message + "Expected #{show(@expected)}, got #{show(@got)}" + end - actual - .then { |collection| by ? collection.sort_by(&by) : collection.sort } - .then { |sorted_collection| order.to_sym == :desc ? sorted_collection.reverse : sorted_collection } - .then { |sorted_collection| sorted_collection == actual } + def show(things) + if @format_with + things.map(&@format_with) + else + things + end end end diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb index 5a8388d01df..4c617f3ba46 100644 --- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb @@ -63,7 +63,6 @@ RSpec.shared_examples 'handles repository moves' do context 'and transits to scheduled' do it 'triggers the corresponding repository storage worker' do - skip unless repository_storage_worker # TODO remove after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented expect(repository_storage_worker).to receive(:perform_async).with(container.id, 'test_second_storage', storage_move.id) storage_move.schedule! @@ -72,8 +71,7 @@ RSpec.shared_examples 'handles repository moves' do end context 'when the transition fails' do - it 'does not trigger ProjectUpdateRepositoryStorageWorker and adds an error' do - skip unless repository_storage_worker # TODO remove after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented + it 'does not trigger the corresponding repository storage worker and adds an error' do allow(storage_move.container).to receive(:set_repository_read_only!).and_raise(StandardError, 'foobar') expect(repository_storage_worker).not_to receive(:perform_async) diff --git a/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb b/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb new file mode 100644 index 00000000000..babd7cfbbeb --- /dev/null +++ b/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'an update storage move worker' do + describe '#perform' do + let(:service) { double(:update_repository_storage_service) } + + before do + allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default test_second_storage]) + end + + context 'without repository storage move' do + it 'calls the update repository storage service' do + expect(service_klass).to receive(:new).and_return(service) + expect(service).to receive(:execute) + + expect do + subject.perform(container.id, 'test_second_storage') + end.to change(repository_storage_move_klass, :count).by(1) + + storage_move = container.repository_storage_moves.last + expect(storage_move).to have_attributes( + source_storage_name: 'default', + destination_storage_name: 'test_second_storage' + ) + end + end + + context 'with repository storage move' do + it 'calls the update repository storage service' do + expect(service_klass).to receive(:new).and_return(service) + expect(service).to receive(:execute) + + expect do + subject.perform(nil, nil, repository_storage_move.id) + end.not_to change(repository_storage_move_klass, :count) + end + end + end +end diff --git a/spec/support_specs/matchers/be_sorted_spec.rb b/spec/support_specs/matchers/be_sorted_spec.rb new file mode 100644 index 00000000000..e62bc9b36b3 --- /dev/null +++ b/spec/support_specs/matchers/be_sorted_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +load File.expand_path('../../../spec/support/matchers/be_sorted.rb', __dir__) + +RSpec.describe 'be_sorted' do + it 'matches empty collections, regardless of arguments' do + expect([]) + .to be_sorted + .and be_sorted.asc + .and be_sorted.desc + .and be_sorted(:foo) + .and be_sorted(:bar) + + expect([].to_set).to be_sorted + expect({}).to be_sorted + end + + it 'matches in both directions' do + expect([1, 2, 3]).to be_sorted.asc + expect([3, 2, 1]).to be_sorted.desc + end + + it 'can match on a projection' do + xs = [['a', 10], ['b', 7], ['c', 4]] + + expect(xs).to be_sorted.asc.by(&:first) + expect(xs).to be_sorted(:first, :asc) + expect(xs).to be_sorted.desc.by(&:second) + expect(xs).to be_sorted(:second, :desc) + end +end diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb index cf33ec9884b..01892e72c97 100644 --- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb +++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb @@ -3,10 +3,42 @@ require 'spec_helper' RSpec.describe 'layouts/header/_new_dropdown' do - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } + + shared_examples_for 'invite member quick link' do + context 'when an experiment is active' do + before do + allow(Gitlab::Experimentation).to receive(:active?).and_return(true) + allow(view).to receive(:experiment_tracking_category_and_group) + allow(view).to receive(:tracking_label).with(user) + end + + context 'with ability to invite members' do + it { is_expected.to have_link('Invite members', href: href) } + + it 'records the experiment' do + subject + + expect(view).to have_received(:experiment_tracking_category_and_group) + .with(:invite_members_new_dropdown, subject: user) + expect(view).to have_received(:tracking_label).with(user) + end + end + + context 'without ability to invite members' do + let(:invite_member) { false } + + it { is_expected.not_to have_link('Invite members') } + end + end + + context 'when experiment is not active' do + it { is_expected.not_to have_link('Invite members') } + end + end context 'group-specific links' do - let(:group) { create(:group) } + let_it_be(:group) { create(:group) } before do stub_current_user(user) @@ -22,25 +54,39 @@ RSpec.describe 'layouts/header/_new_dropdown' do it 'has a "New project" link' do render - expect(rendered).to have_link( - 'New project', - href: new_project_path(namespace_id: group.id) - ) + expect(rendered).to have_link('New project', href: new_project_path(namespace_id: group.id)) end it 'has a "New subgroup" link' do render - expect(rendered).to have_link( - 'New subgroup', - href: new_group_path(parent_id: group.id) - ) + expect(rendered).to have_link('New subgroup', href: new_group_path(parent_id: group.id)) end end + + describe 'invite members quick link' do + let(:href) { group_group_members_path(group) } + let(:invite_member) { true } + + before do + allow(view).to receive(:can?).with(user, :create_projects, group).and_return(true) + allow(view).to receive(:can?).with(user, :admin_group_member, group).and_return(invite_member) + allow(view).to receive(:can_import_members?).and_return(invite_member) + allow(view).to receive(:experiment_enabled?) + end + + subject do + render + + rendered + end + + it_behaves_like 'invite member quick link' + end end context 'project-specific links' do - let(:project) { create(:project, creator: user, namespace: user.namespace) } + let_it_be(:project) { create(:project, creator: user, namespace: user.namespace) } before do assign(:project, project) @@ -54,33 +100,24 @@ RSpec.describe 'layouts/header/_new_dropdown' do it 'has a "New issue" link' do render - expect(rendered).to have_link( - 'New issue', - href: new_project_issue_path(project) - ) + expect(rendered).to have_link('New issue', href: new_project_issue_path(project)) end it 'has a "New merge request" link' do render - expect(rendered).to have_link( - 'New merge request', - href: project_new_merge_request_path(project) - ) + expect(rendered).to have_link('New merge request', href: project_new_merge_request_path(project)) end it 'has a "New snippet" link' do render - expect(rendered).to have_link( - 'New snippet', - href: new_project_snippet_path(project) - ) + expect(rendered).to have_link('New snippet', href: new_project_snippet_path(project)) end end context 'as a Project guest' do - let(:guest) { create(:user) } + let_it_be(:guest) { create(:user) } before do stub_current_user(guest) @@ -96,12 +133,28 @@ RSpec.describe 'layouts/header/_new_dropdown' do it 'has no "New snippet" link' do render - expect(rendered).not_to have_link( - 'New snippet', - href: new_project_snippet_path(project) - ) + expect(rendered).not_to have_link('New snippet', href: new_project_snippet_path(project)) end end + + describe 'invite members quick link' do + let(:invite_member) { true } + let(:href) { project_project_members_path(project) } + + before do + allow(view).to receive(:can_import_members?).and_return(invite_member) + stub_current_user(user) + allow(view).to receive(:experiment_enabled?) + end + + subject do + render + + rendered + end + + it_behaves_like 'invite member quick link' + end end context 'global links' do @@ -128,7 +181,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do end context 'when the user is not allowed to create snippets' do - let(:user) { create(:user, :external)} + let(:user) { create(:user, :external) } it 'has no "New snippet" link' do render diff --git a/spec/workers/project_update_repository_storage_worker_spec.rb b/spec/workers/project_update_repository_storage_worker_spec.rb index f75bb3d1642..490f1f5a2ad 100644 --- a/spec/workers/project_update_repository_storage_worker_spec.rb +++ b/spec/workers/project_update_repository_storage_worker_spec.rb @@ -3,45 +3,13 @@ require 'spec_helper' RSpec.describe ProjectUpdateRepositoryStorageWorker do - let(:project) { create(:project, :repository) } - subject { described_class.new } - describe "#perform" do - let(:service) { double(:update_repository_storage_service) } - - before do - allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default test_second_storage]) - end - - context 'without repository storage move' do - it "calls the update repository storage service" do - expect(Projects::UpdateRepositoryStorageService).to receive(:new).and_return(service) - expect(service).to receive(:execute) - - expect do - subject.perform(project.id, 'test_second_storage') - end.to change(ProjectRepositoryStorageMove, :count).by(1) - - storage_move = project.repository_storage_moves.last - expect(storage_move).to have_attributes( - source_storage_name: "default", - destination_storage_name: "test_second_storage" - ) - end - end - - context 'with repository storage move' do - let!(:repository_storage_move) { create(:project_repository_storage_move) } - - it "calls the update repository storage service" do - expect(Projects::UpdateRepositoryStorageService).to receive(:new).and_return(service) - expect(service).to receive(:execute) + it_behaves_like 'an update storage move worker' do + let_it_be_with_refind(:container) { create(:project, :repository) } + let_it_be(:repository_storage_move) { create(:project_repository_storage_move) } - expect do - subject.perform(nil, nil, repository_storage_move.id) - end.not_to change(ProjectRepositoryStorageMove, :count) - end - end + let(:service_klass) { Projects::UpdateRepositoryStorageService } + let(:repository_storage_move_klass) { ProjectRepositoryStorageMove } end end diff --git a/spec/workers/snippet_update_repository_storage_worker_spec.rb b/spec/workers/snippet_update_repository_storage_worker_spec.rb new file mode 100644 index 00000000000..a48abe4abf7 --- /dev/null +++ b/spec/workers/snippet_update_repository_storage_worker_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SnippetUpdateRepositoryStorageWorker do + subject { described_class.new } + + it_behaves_like 'an update storage move worker' do + let_it_be_with_refind(:container) { create(:snippet, :repository) } + let_it_be(:repository_storage_move) { create(:snippet_repository_storage_move) } + + let(:service_klass) { Snippets::UpdateRepositoryStorageService } + let(:repository_storage_move_klass) { SnippetRepositoryStorageMove } + end +end |