summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb7
-rw-r--r--app/helpers/invite_members_helper.rb30
-rw-r--r--app/models/snippet_repository_storage_move.rb6
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml4
-rw-r--r--app/views/layouts/header/_group_invite_members_new_dropdown_item.html.haml3
-rw-r--r--app/views/layouts/header/_new_dropdown.haml4
-rw-r--r--app/views/layouts/header/_project_invite_members_new_dropdown_item.html.haml3
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/concerns/update_repository_storage_worker.rb42
-rw-r--r--app/workers/project_update_repository_storage_worker.rb32
-rw-r--r--app/workers/snippet_update_repository_storage_worker.rb23
-rw-r--r--changelogs/unreleased/add-terraform-state-api-usage-tracking.yml5
-rw-r--r--changelogs/unreleased/oauth-pkce.yml5
-rw-r--r--config/feature_flags/development/usage_data_p_terraform_state_api_unique_users.yml8
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20201209163958_add_code_challenge_to_oauth_access_grants.rb26
-rw-r--r--db/schema_migrations/202012091639581
-rw-r--r--db/structure.sql6
-rw-r--r--doc/.vale/gitlab/AlertBoxStyle.yml4
-rw-r--r--doc/api/oauth2.md124
-rw-r--r--doc/development/fe_guide/graphql.md1
-rw-r--r--doc/user/clusters/applications.md5
-rw-r--r--doc/user/project/clusters/protect/web_application_firewall/index.md5
-rw-r--r--doc/user/project/clusters/protect/web_application_firewall/quick_start_guide.md5
-rw-r--r--lib/api/terraform/state.rb2
-rw-r--r--lib/gitlab/experimentation.rb3
-rw-r--r--lib/gitlab/experimentation/controller_concern.rb2
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb2
-rw-r--r--lib/gitlab/usage_data_counters/known_events/common.yml6
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/helpers/invite_members_helper_spec.rb65
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb3
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb2
-rw-r--r--spec/models/snippet_repository_storage_move_spec.rb2
-rw-r--r--spec/requests/api/terraform/state_spec.rb37
-rw-r--r--spec/support/matchers/be_sorted.rb71
-rw-r--r--spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/workers/update_repository_move_shared_examples.rb39
-rw-r--r--spec/support_specs/matchers/be_sorted_spec.rb33
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb111
-rw-r--r--spec/workers/project_update_repository_storage_worker_spec.rb42
-rw-r--r--spec/workers/snippet_update_repository_storage_worker_spec.rb15
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