Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-22 21:10:06 +00:00
parent 9a14667521
commit c66b5f750f
42 changed files with 670 additions and 130 deletions

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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)

View File

@ -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?

View File

@ -0,0 +1,3 @@
- return unless Gitlab::Experimentation.active?(:invite_members_new_dropdown) && can_import_members?
%li= dropdown_invite_members_link(@project)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Track usage for Terraform State API
merge_request: 50224
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Enable OAuth PKCE flow
merge_request: 49756
author:
type: added

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
4bdd5eba48a76d8feab948857ec32ef7fe25e04e8633ee7d94fd059e73703472

View File

@ -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

View File

@ -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):.*)'

View File

@ -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:

View File

@ -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.
```

View File

@ -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:

View File

@ -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,

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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' }

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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