Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9a14667521
commit
c66b5f750f
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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)
|
|
@ -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?
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
- return unless Gitlab::Experimentation.active?(:invite_members_new_dropdown) && can_import_members?
|
||||
|
||||
%li= dropdown_invite_members_link(@project)
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Track usage for Terraform State API
|
||||
merge_request: 50224
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable OAuth PKCE flow
|
||||
merge_request: 49756
|
||||
author:
|
||||
type: added
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
4bdd5eba48a76d8feab948857ec32ef7fe25e04e8633ee7d94fd059e73703472
|
|
@ -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
|
||||
|
|
|
@ -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):.*)'
|
||||
|
|
|
@ -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)).
|
||||
The Implicit grant flow is inherently insecure. The IETF plans to remove it in
|
||||
[OAuth 2.1](https://oauth.net/2.1/).
|
||||
|
||||
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.
|
||||
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:
|
||||
|
|
|
@ -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.
|
||||
```
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
||||
|
|
|
@ -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|
|
||||
match do |actual|
|
||||
next true unless actual.present? # emtpy collection is sorted
|
||||
RSpec::Matchers.define :be_sorted do |on = :itself, order = :asc|
|
||||
def by(&block)
|
||||
@comparator = block
|
||||
self
|
||||
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 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? # 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
|
||||
|
||||
def show(things)
|
||||
if @format_with
|
||||
things.map(&@format_with)
|
||||
else
|
||||
things
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
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) }
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue