Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-10-17 03:08:57 +00:00
parent eaeb21af27
commit b24742b7ed
24 changed files with 758 additions and 256 deletions

View File

@ -1 +1 @@
15.5.0
15.5.1

View File

@ -137,6 +137,19 @@ class GitlabSchema < GraphQL::Schema
gid
end
# Parse an array of strings to an array of GlobalIDs, raising ArgumentError if there are problems
# with it.
# See #parse_gid
#
# ```
# gids = GitlabSchema.parse_gids(my_array_of_strings, expected_type: ::Project)
# project_ids = gids.map(&:model_id)
# gids.all? { |gid| gid.model_class == ::Project }
# ```
def parse_gids(global_ids, ctx = {})
global_ids.map { |gid| parse_gid(gid, ctx) }
end
private
def max_query_complexity(ctx)

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
module Mutations
module Packages
class BulkDestroy < ::Mutations::BaseMutation
graphql_name 'DestroyPackages'
MAX_PACKAGES = 20
TOO_MANY_IDS_ERROR = "Cannot delete more than #{MAX_PACKAGES} packages"
argument :ids,
[::Types::GlobalIDType[::Packages::Package]],
required: true,
description: "Global IDs of the Packages. Max #{MAX_PACKAGES}"
def resolve(ids:)
raise_resource_not_available_error!(TOO_MANY_IDS_ERROR) if ids.size > MAX_PACKAGES
ids = GitlabSchema.parse_gids(ids, expected_type: ::Packages::Package)
.map(&:model_id)
service = ::Packages::MarkPackagesForDestructionService.new(
packages: packages_from(ids),
current_user: current_user
)
result = service.execute
raise_resource_not_available_error! if result.reason == :unauthorized
errors = result.error? ? Array.wrap(result[:message]) : []
{ errors: errors }
end
private
def packages_from(ids)
::Packages::Package.displayable
.id_in(ids)
end
end
end
end

View File

@ -25,7 +25,7 @@ module Mutations
project = authorized_find!(project_path)
raise_resource_not_available_error! "Cannot delete more than #{MAXIMUM_FILES} files" if ids.size > MAXIMUM_FILES
package_files = ::Packages::PackageFile.where(id: parse_gids(ids)) # rubocop:disable CodeReuse/ActiveRecord
package_files = ::Packages::PackageFile.id_in(parse_gids(ids))
ensure_file_access!(project, package_files)
@ -47,7 +47,7 @@ module Mutations
end
def parse_gids(gids)
gids.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Packages::PackageFile).model_id }
GitlabSchema.parse_gids(gids, expected_type: ::Packages::PackageFile).map(&:model_id)
end
end
end

View File

@ -138,6 +138,8 @@ module Types
mount_mutation Mutations::UserCallouts::Create
mount_mutation Mutations::UserPreferences::Update
mount_mutation Mutations::Packages::Destroy
mount_mutation Mutations::Packages::BulkDestroy,
extensions: [::Gitlab::Graphql::Limit::FieldCallCount => { limit: 1 }]
mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Packages::DestroyFiles
mount_mutation Mutations::Packages::Cleanup::Policy::Update

View File

@ -124,6 +124,7 @@ class Packages::Package < ApplicationRecord
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
scope :without_package_type, ->(package_type) { where.not(package_type: package_type) }
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
scope :including_project_full_path, -> { includes(project: :route) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }

View File

@ -0,0 +1,79 @@
# frozen_string_literal: true
module Packages
class MarkPackagesForDestructionService
include BaseServiceUtility
BATCH_SIZE = 20
UNAUTHORIZED_RESPONSE = ServiceResponse.error(
message: "You don't have the permission to perform this action",
reason: :unauthorized
).freeze
ERROR_RESPONSE = ServiceResponse.error(
message: 'Failed to mark the packages as pending destruction'
).freeze
SUCCESS_RESPONSE = ServiceResponse.success(
message: 'Packages were successfully marked as pending destruction'
).freeze
# Initialize this service with the given packages and user.
#
# * `packages`: must be an ActiveRecord relationship.
# * `current_user`: an User object. Could be nil.
def initialize(packages:, current_user: nil)
@packages = packages
@current_user = current_user
end
def execute(batch_size: BATCH_SIZE)
no_access = false
min_batch_size = [batch_size, BATCH_SIZE].min
@packages.each_batch(of: min_batch_size) do |batched_packages|
loaded_packages = batched_packages.including_project_full_path.to_a
break no_access = true unless can_destroy_packages?(loaded_packages)
::Packages::Package.id_in(loaded_packages.map(&:id))
.update_all(status: :pending_destruction)
sync_maven_metadata(loaded_packages)
mark_package_files_for_destruction(loaded_packages)
end
return UNAUTHORIZED_RESPONSE if no_access
SUCCESS_RESPONSE
rescue StandardError
ERROR_RESPONSE
end
private
def mark_package_files_for_destruction(packages)
::Packages::MarkPackageFilesForDestructionWorker.bulk_perform_async_with_contexts(
packages,
arguments_proc: -> (package) { package.id },
context_proc: -> (package) { { project: package.project, user: @current_user } }
)
end
def sync_maven_metadata(packages)
maven_packages_with_version = packages.select { |pkg| pkg.maven? && pkg.version? }
::Packages::Maven::Metadata::SyncWorker.bulk_perform_async_with_contexts(
maven_packages_with_version,
arguments_proc: -> (package) { [@current_user.id, package.project_id, package.name] },
context_proc: -> (package) { { project: package.project, user: @current_user } }
)
end
def can_destroy_packages?(packages)
packages.all? do |package|
can?(@current_user, :destroy_package, package)
end
end
end
end

View File

@ -2426,6 +2426,24 @@ Input type: `DestroyPackageFilesInput`
| <a id="mutationdestroypackagefilesclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdestroypackagefileserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.destroyPackages`
Input type: `DestroyPackagesInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationdestroypackagesclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdestroypackagesids"></a>`ids` | [`[PackagesPackageID!]!`](#packagespackageid) | Global IDs of the Packages. Max 20. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationdestroypackagesclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdestroypackageserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.destroySnippet`
Input type: `DestroySnippetInput`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -463,8 +463,9 @@ Without the `config.extend_remember_period` flag, you would be forced to sign in
- [Create users](account/create_accounts.md)
- [Sign in to your GitLab account](../../topics/authentication/index.md)
- [Change your password](user_passwords.md)
- [Receive emails for sign-ins from unknown IP addresses or devices](unknown_sign_in_notification.md)
- [Receive emails for attempted sign-ins using a wrong two-factor authentication code](wrong_two_factor_authentication_code_notification.md)
- Receive emails for:
- [Sign-ins from unknown IP addresses or devices](notifications.md#notifications-for-unknown-sign-ins)
- [Attempted sign-ins using wrong two-factor authentication codes](notifications.md#notifications-for-attempted-sign-in-using-wrong-two-factor-authentication-codes)
- Manage applications that can [use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth)
- Manage [personal access tokens](personal_access_tokens.md) to access your account via API and authorized applications
- Manage [SSH keys](../ssh.md) to access your account via SSH

View File

@ -286,6 +286,36 @@ By default, you don't receive notifications for issues, merge requests, or epics
To always receive notifications on your own issues, merge requests, and so on, turn on
[notifications about your own activity](#global-notification-settings).
## Notifications for unknown sign-ins
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0.
NOTE:
This feature is enabled by default for self-managed instances. Administrators may disable this feature
through the [Sign-in restrictions](../admin_area/settings/sign_in_restrictions.md#email-notification-for-unknown-sign-ins) section of the UI.
The feature is always enabled on GitLab.com.
When a user successfully signs in from a previously unknown IP address or device,
GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially
malicious or unauthorized sign-ins.
GitLab uses several methods to identify a known sign-in. All methods must fail for a notification email to be sent.
- Last sign-in IP: The current sign-in IP address is checked against the last sign-in
IP address.
- Current active sessions: If the user has an existing active session from the
same IP address. See [Active Sessions](active_sessions.md).
- Cookie: After successful sign in, an encrypted cookie is stored in the browser.
This cookie is set to expire 14 days after the last successful sign in.
## Notifications for attempted sign-in using wrong two-factor authentication codes
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/374740) in GitLab 15.5.
GitLab sends you an email notification if it detects an attempt to sign in to your account using a wrong two-factor
authentication (2FA) code. This can help you detect that a bad actor gained access to your username and password, and is trying
to brute force 2FA.
## Notifications on designs
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217095) in GitLab 13.6.

View File

@ -1,32 +1,11 @@
---
stage: Manage
group: Authentication and Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
redirect_to: 'notifications.md'
remove_date: '2023-01-15'
---
# Email notification for unknown sign-ins **(FREE)**
This document was moved to [another location](notifications.md).
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0.
NOTE:
This feature is enabled by default for self-managed instances. Administrators may disable this feature
through the [Sign-in restrictions](../admin_area/settings/sign_in_restrictions.md#email-notification-for-unknown-sign-ins) section of the UI.
The feature is always enabled on GitLab.com.
When a user successfully signs in from a previously unknown IP address or device,
GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially
malicious or unauthorized sign-ins.
There are several methods used to identify a known sign-in. All methods must fail
for a notification email to be sent.
- Last sign-in IP: The current sign-in IP address is checked against the last sign-in
IP address.
- Current active sessions: If the user has an existing active session from the
same IP address. See [Active Sessions](active_sessions.md).
- Cookie: After successful sign in, an encrypted cookie is stored in the browser.
This cookie is set to expire 14 days after the last successful sign in.
## Example notification email
![Unknown sign in email](img/unknown_sign_in_email_v14_0.png)
<!-- This redirect file can be deleted after <2023-01-15>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -1,15 +0,0 @@
---
stage: Manage
group: Authentication and Authorization
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/#assignments
---
# Email notification for attempted sign-in using wrong two-factor authentication code **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/374740) in GitLab 15.5.
GitLab sends you an email notification if it detects an attempt to sign in to your account using a wrong two-factor authentication code. This way, GitLab proactively alerts you of potentially malicious or unauthorized sign-ins, in case a bad actor gained access to your username and password, and is trying to bruteforce two-factor authentication.
## Example notification email
![Incorrect two-factor code email](img/wrong_two_factor_authentication_code_v15_5.png)

View File

@ -24,3 +24,8 @@ before_script:
mix:
script:
- mix test
deploy:
stage: deploy
script: echo "Define your deployment script!"
environment: production

View File

@ -35,3 +35,8 @@ test:
- $CI_PROJECT_DIR/coverage
reports:
junit: report.xml
deploy:
stage: deploy
script: echo "Define your deployment script!"
environment: production

View File

@ -28,3 +28,8 @@ compile:
artifacts:
paths:
- mybinaries
deploy:
stage: deploy
script: echo "Define your deployment script!"
environment: production

View File

@ -1,3 +1,5 @@
# You can copy and paste this template into a new `.gitlab-ci.yml` file.
# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword.
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
@ -39,3 +41,8 @@ test:
paths:
- build
- .gradle
deploy:
stage: deploy
script: echo "Define your deployment script!"
environment: production

View File

@ -46,3 +46,8 @@ before_script:
build:
script:
- ./gradlew build
deploy:
stage: deploy
script: echo "Define your deployment script!"
environment: production

View File

@ -232,11 +232,7 @@ RSpec.describe GitlabSchema do
end
end
describe '.parse_gid' do
let_it_be(:global_id) { 'gid://gitlab/TestOne/2147483647' }
subject(:parse_gid) { described_class.parse_gid(global_id) }
context 'for gid parsing' do
before do
test_base = Class.new
test_one = Class.new(test_base)
@ -249,65 +245,84 @@ RSpec.describe GitlabSchema do
stub_const('TestThree', test_three)
end
it 'parses the gid' do
gid = parse_gid
describe '.parse_gid' do
let_it_be(:global_id) { 'gid://gitlab/TestOne/2147483647' }
expect(gid.model_id).to eq '2147483647'
expect(gid.model_class).to eq TestOne
end
subject(:parse_gid) { described_class.parse_gid(global_id) }
context 'when gid is malformed' do
let_it_be(:global_id) { 'malformed://gitlab/TestOne/2147483647' }
it 'raises an error' do
expect { parse_gid }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID.")
end
end
context 'when using expected_type' do
it 'accepts a single type' do
gid = described_class.parse_gid(global_id, expected_type: TestOne)
it 'parses the gid' do
gid = parse_gid
expect(gid.model_id).to eq '2147483647'
expect(gid.model_class).to eq TestOne
end
it 'accepts an ancestor type' do
gid = described_class.parse_gid(global_id, expected_type: TestBase)
context 'when gid is malformed' do
let_it_be(:global_id) { 'malformed://gitlab/TestOne/2147483647' }
expect(gid.model_class).to eq TestOne
it 'raises an error' do
expect { parse_gid }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID.")
end
end
it 'rejects an unknown type' do
expect { described_class.parse_gid(global_id, expected_type: TestTwo) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid ID for TestTwo.")
context 'when using expected_type' do
it 'accepts a single type' do
gid = described_class.parse_gid(global_id, expected_type: TestOne)
expect(gid.model_class).to eq TestOne
end
it 'accepts an ancestor type' do
gid = described_class.parse_gid(global_id, expected_type: TestBase)
expect(gid.model_class).to eq TestOne
end
it 'rejects an unknown type' do
expect { described_class.parse_gid(global_id, expected_type: TestTwo) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid ID for TestTwo.")
end
context 'when expected_type is an array' do
subject(:parse_gid) { described_class.parse_gid(global_id, expected_type: [TestOne, TestTwo]) }
context 'when global_id is of type TestOne' do
it 'returns an object of an expected type' do
expect(parse_gid.model_class).to eq TestOne
end
end
context 'when global_id is of type TestTwo' do
let_it_be(:global_id) { 'gid://gitlab/TestTwo/2147483647' }
it 'returns an object of an expected type' do
expect(parse_gid.model_class).to eq TestTwo
end
end
context 'when global_id is of type TestThree' do
let_it_be(:global_id) { 'gid://gitlab/TestThree/2147483647' }
it 'rejects an unknown type' do
expect { parse_gid }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid ID for TestOne, TestTwo.")
end
end
end
end
end
context 'when expected_type is an array' do
subject(:parse_gid) { described_class.parse_gid(global_id, expected_type: [TestOne, TestTwo]) }
describe '.parse_gids' do
let_it_be(:global_ids) { %w[gid://gitlab/TestOne/123 gid://gitlab/TestOne/456] }
context 'when global_id is of type TestOne' do
it 'returns an object of an expected type' do
expect(parse_gid.model_class).to eq TestOne
end
end
subject(:parse_gids) { described_class.parse_gids(global_ids, expected_type: TestOne) }
context 'when global_id is of type TestTwo' do
let_it_be(:global_id) { 'gid://gitlab/TestTwo/2147483647' }
it 'returns an object of an expected type' do
expect(parse_gid.model_class).to eq TestTwo
end
end
context 'when global_id is of type TestThree' do
let_it_be(:global_id) { 'gid://gitlab/TestThree/2147483647' }
it 'rejects an unknown type' do
expect { parse_gid }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid ID for TestOne, TestTwo.")
end
end
it 'parses the gids' do
expect(described_class).to receive(:parse_gid).with('gid://gitlab/TestOne/123', expected_type: TestOne).and_call_original
expect(described_class).to receive(:parse_gid).with('gid://gitlab/TestOne/456', expected_type: TestOne).and_call_original
expect(parse_gids.map(&:model_id)).to eq %w[123 456]
expect(parse_gids.map(&:model_class)).to match_array [TestOne, TestOne]
end
end
end

View File

@ -2,29 +2,28 @@
require 'spec_helper'
# Snippet visibility scenarios are included in more details in spec/support/snippet_visibility.rb
# Snippet visibility scenarios are included in more details in spec/finders/snippets_finder_spec.rb
RSpec.describe ProjectSnippetPolicy do
let_it_be(:group) { create(:group, :public) }
let_it_be(:regular_user) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:project) { create(:project, :public) }
let(:snippet) { create(:project_snippet, snippet_visibility, project: project, author: author) }
let(:author) { other_user }
let(:author_permissions) do
let_it_be(:author) { create(:user) }
let_it_be(:author_permissions) do
[
:update_snippet,
:admin_snippet
]
end
let(:snippet) { build(:project_snippet, snippet_visibility, project: project, author: author) }
subject { described_class.new(current_user, snippet) }
shared_examples 'regular user access rights' do
shared_examples 'regular user member permissions' do
context 'not snippet author' do
context 'project team member (non guest)' do
context 'member (guest)' do
before do
project.add_developer(current_user)
membership_target.add_guest(current_user)
end
it do
@ -33,25 +32,35 @@ RSpec.describe ProjectSnippetPolicy do
end
end
context 'project team member (guest)' do
context 'member (reporter)' do
before do
project.add_guest(current_user)
membership_target.add_reporter(current_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(:admin_snippet)
expect_disallowed(*author_permissions)
end
end
context 'project team member (maintainer)' do
context 'member (developer)' do
before do
project.add_maintainer(current_user)
membership_target.add_developer(current_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_allowed(*author_permissions)
expect_disallowed(*author_permissions)
end
end
context 'member (maintainer)' do
before do
membership_target.add_maintainer(current_user)
end
it do
expect_allowed(:read_snippet, :create_note, *author_permissions)
end
end
end
@ -59,196 +68,263 @@ RSpec.describe ProjectSnippetPolicy do
context 'snippet author' do
let(:author) { current_user }
context 'project member (non guest)' do
context 'member (guest)' do
before do
project.add_developer(current_user)
membership_target.add_guest(current_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_allowed(*author_permissions)
end
end
context 'project member (guest)' do
before do
project.add_guest(current_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_allowed(:read_snippet, :create_note, :update_snippet)
expect_disallowed(:admin_snippet)
end
end
context 'project team member (maintainer)' do
context 'member (reporter)' do
before do
project.add_maintainer(current_user)
membership_target.add_reporter(current_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_allowed(*author_permissions)
expect_allowed(:read_snippet, :create_note, *author_permissions)
end
end
context 'not a project member' do
context 'member (developer)' do
before do
membership_target.add_developer(current_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(:admin_snippet)
expect_allowed(:read_snippet, :create_note, *author_permissions)
end
end
context 'member (maintainer)' do
before do
membership_target.add_maintainer(current_user)
end
it do
expect_allowed(:read_snippet, :create_note, *author_permissions)
end
end
end
end
context 'public snippet' do
let(:snippet_visibility) { :public }
shared_examples 'regular user non-member author permissions' do
let(:author) { current_user }
context 'no user' do
let(:current_user) { nil }
it do
expect_allowed(:read_snippet)
expect_disallowed(*author_permissions)
end
end
context 'regular user' do
let(:current_user) { regular_user }
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
it_behaves_like 'regular user access rights'
end
context 'external user' do
let(:current_user) { external_user }
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
context 'project team member' do
before do
project.add_developer(external_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
it do
expect_allowed(:read_snippet, :create_note, :update_snippet)
expect_disallowed(:admin_snippet)
end
end
context 'internal snippet' do
let(:snippet_visibility) { :internal }
context 'when project is public' do
let_it_be(:project) { create(:project, :public, group: group) }
context 'no user' do
let(:current_user) { nil }
context 'with public snippet' do
let(:snippet_visibility) { :public }
it do
expect_disallowed(:read_snippet)
expect_disallowed(*author_permissions)
end
end
context 'no user' do
let(:current_user) { nil }
context 'regular user' do
let(:current_user) { regular_user }
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
it_behaves_like 'regular user access rights'
end
context 'external user' do
let(:current_user) { external_user }
it do
expect_disallowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
context 'project team member' do
before do
project.add_developer(external_user)
it do
expect_allowed(:read_snippet)
expect_disallowed(*author_permissions)
end
end
context 'regular user' do
let(:current_user) { regular_user }
let(:membership_target) { project }
context 'when user is not a member' do
context 'and is not the snippet author' do
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
context 'and is the snippet author' do
it_behaves_like 'regular user non-member author permissions'
end
end
context 'when user is a member' do
it_behaves_like 'regular user member permissions'
end
end
context 'external user' do
let(:current_user) { external_user }
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
end
end
context 'private snippet' do
let(:snippet_visibility) { :private }
context 'when user is a member' do
before do
project.add_developer(external_user)
end
context 'no user' do
let(:current_user) { nil }
it do
expect_disallowed(:read_snippet)
expect_disallowed(*author_permissions)
end
end
context 'regular user' do
let(:current_user) { regular_user }
it do
expect_disallowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
it_behaves_like 'regular user access rights'
end
context 'external user' do
let(:current_user) { external_user }
it do
expect_disallowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
context 'project team member' do
before do
project.add_developer(current_user)
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
end
end
context 'with internal snippet' do
let(:snippet_visibility) { :internal }
context 'no user' do
let(:current_user) { nil }
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(:read_snippet)
expect_disallowed(*author_permissions)
end
end
end
context 'admin user' do
let(:snippet_visibility) { :private }
let(:current_user) { create(:admin) }
context 'regular user' do
let(:current_user) { regular_user }
let(:membership_target) { project }
context 'when admin mode is enabled', :enable_admin_mode do
it do
expect_allowed(:read_snippet, :create_note)
expect_allowed(*author_permissions)
context 'when user is not a member' do
context 'and is not the snippet author' do
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
context 'and is the snippet author' do
it_behaves_like 'regular user non-member author permissions'
end
end
context 'when user is a member' do
it_behaves_like 'regular user member permissions'
end
end
context 'when admin mode is disabled' do
context 'external user' do
let(:current_user) { external_user }
it do
expect_disallowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
context 'when user is a member' do
before do
project.add_developer(external_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
end
end
context 'with private snippet' do
let(:snippet_visibility) { :private }
context 'no user' do
let(:current_user) { nil }
it do
expect_disallowed(:read_snippet)
expect_disallowed(*author_permissions)
end
end
context 'regular user' do
let(:current_user) { regular_user }
let(:membership_target) { project }
context 'when user is not a member' do
context 'and is not the snippet author' do
it do
expect_disallowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
context 'and is the snippet author' do
it_behaves_like 'regular user non-member author permissions'
end
end
context 'when user is a member' do
it_behaves_like 'regular user member permissions'
end
end
context 'inherited user' do
let(:current_user) { regular_user }
let(:membership_target) { group }
it_behaves_like 'regular user member permissions'
end
context 'external user' do
let(:current_user) { external_user }
it do
expect_disallowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
context 'when user is a member' do
before do
project.add_developer(current_user)
end
it do
expect_allowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
end
context 'admin user' do
let(:snippet_visibility) { :private }
let(:current_user) { create(:admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it do
expect_allowed(:read_snippet, :create_note)
expect_allowed(*author_permissions)
end
end
context 'when admin mode is disabled' do
it do
expect_disallowed(:read_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
end
end
end
context 'when project is private' do
let_it_be(:project) { create(:project, :private, group: group) }
let(:snippet_visibility) { :private }
context 'inherited user' do
let(:current_user) { regular_user }
let(:membership_target) { group }
it_behaves_like 'regular user member permissions'
end
end
end

View File

@ -0,0 +1,128 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Destroying multiple packages' do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:packages1) { create_list(:package, 3, project: project1) }
let_it_be_with_reload(:packages2) { create_list(:package, 2, project: project2) }
let(:ids) { packages1.append(packages2).flatten.map(&:to_global_id).map(&:to_s) }
let(:query) do
<<~GQL
errors
GQL
end
let(:params) do
{
ids: ids
}
end
let(:mutation) { graphql_mutation(:destroy_packages, params, query) }
describe 'post graphql mutation' do
subject(:mutation_request) { post_graphql_mutation(mutation, current_user: user) }
shared_examples 'destroying the packages' do
it 'marks the packages as pending destruction' do
expect { mutation_request }.to change { ::Packages::Package.pending_destruction.count }.by(5)
end
it_behaves_like 'returning response status', :success
end
shared_examples 'denying the mutation request' do
|response = ::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR|
it 'does not mark the packages as pending destruction' do
expect { mutation_request }.not_to change { ::Packages::Package.pending_destruction.count }
expect_graphql_errors_to_include(response)
end
it_behaves_like 'returning response status', :success
end
context 'with valid params' do
where(:user_role, :shared_examples_name) do
:maintainer | 'destroying the packages'
:developer | 'denying the mutation request'
:reporter | 'denying the mutation request'
:guest | 'denying the mutation request'
:not_in_project | 'denying the mutation request'
end
with_them do
before do
unless user_role == :not_in_project
project1.send("add_#{user_role}", user)
project2.send("add_#{user_role}", user)
end
end
it_behaves_like params[:shared_examples_name]
end
context 'for over the limit' do
before do
project1.add_maintainer(user)
project2.add_maintainer(user)
stub_const("Mutations::Packages::BulkDestroy::MAX_PACKAGES", 2)
end
it_behaves_like 'denying the mutation request', ::Mutations::Packages::BulkDestroy::TOO_MANY_IDS_ERROR
end
context 'with packages outside of the project' do
before do
project1.add_maintainer(user)
end
it_behaves_like 'denying the mutation request'
end
end
context 'with invalid params' do
let(:ids) { 'foo' }
it_behaves_like 'denying the mutation request', 'invalid value for id'
end
context 'with multi mutations' do
let(:package1) { packages1.first }
let(:package2) { packages2.first }
let(:query) do
<<~QUERY
mutation {
a: destroyPackages(input: { ids: ["#{package1.to_global_id}"]}) {
errors
}
b: destroyPackages(input: { ids: ["#{package2.to_global_id}"]}) {
errors
}
}
QUERY
end
subject(:mutation_request) { post_graphql(query, current_user: user) }
before do
project1.add_maintainer(user)
project2.add_maintainer(user)
end
it 'executes the first mutation but not the second one' do
expect { mutation_request }.to change { package1.reload.status }.from('default').to('pending_destruction')
.and not_change { package2.reload.status }
expect_graphql_errors_to_include('"destroyPackages" field can be requested only for 1 Mutation(s) at a time.')
end
end
end
end

View File

@ -0,0 +1,107 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::MarkPackagesForDestructionService, :sidekiq_inline do
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:packages) { create_list(:npm_package, 3, project: project) }
let(:user) { project.owner }
# The service only accepts ActiveRecord relationships and not arrays.
let(:service) { described_class.new(packages: ::Packages::Package.id_in(package_ids), current_user: user) }
let(:package_ids) { packages.map(&:id) }
describe '#execute' do
subject { service.execute }
context 'when the user is authorized' do
before do
project.add_maintainer(user)
end
context 'when it is successful' do
it 'marks the packages as pending destruction' do
expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new)
expect { subject }.to change { ::Packages::Package.pending_destruction.count }.from(0).to(3)
.and change { Packages::PackageFile.pending_destruction.count }.from(0).to(3)
packages.each { |pkg| expect(pkg.reload).to be_pending_destruction }
expect(subject).to be_a(ServiceResponse)
expect(subject).to be_success
expect(subject.message).to eq('Packages were successfully marked as pending destruction')
end
context 'with maven packages' do
let_it_be_with_reload(:packages) { create_list(:maven_package, 3, project: project) }
it 'marks the packages as pending destruction' do
expect(::Packages::Maven::Metadata::SyncService).to receive(:new).once.and_call_original
expect { subject }.to change { ::Packages::Package.pending_destruction.count }.from(0).to(3)
.and change { Packages::PackageFile.pending_destruction.count }.from(0).to(9)
packages.each { |pkg| expect(pkg.reload).to be_pending_destruction }
expect(subject).to be_a(ServiceResponse)
expect(subject).to be_success
expect(subject.message).to eq('Packages were successfully marked as pending destruction')
end
context 'without version' do
before do
::Packages::Package.id_in(package_ids).update_all(version: nil)
end
it 'marks the packages as pending destruction' do
expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new)
expect { subject }.to change { ::Packages::Package.pending_destruction.count }.from(0).to(3)
.and change { Packages::PackageFile.pending_destruction.count }.from(0).to(9)
packages.each { |pkg| expect(pkg.reload).to be_pending_destruction }
expect(subject).to be_a(ServiceResponse)
expect(subject).to be_success
expect(subject.message).to eq('Packages were successfully marked as pending destruction')
end
end
end
end
context 'when it is not successful' do
before do
allow(service).to receive(:can_destroy_packages?).and_raise(StandardError, 'test')
end
it 'returns an error ServiceResponse' do
expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new)
expect { subject }.to not_change { ::Packages::Package.pending_destruction.count }
.and not_change { ::Packages::PackageFile.pending_destruction.count }
expect(subject).to be_a(ServiceResponse)
expect(subject).to be_error
expect(subject.message).to eq("Failed to mark the packages as pending destruction")
expect(subject.status).to eq(:error)
end
end
end
context 'when the user is not authorized' do
let(:user) { nil }
it 'returns an error ServiceResponse' do
expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new)
expect { subject }.to not_change { ::Packages::Package.pending_destruction.count }
.and not_change { ::Packages::PackageFile.pending_destruction.count }
expect(subject).to be_a(ServiceResponse)
expect(subject).to be_error
expect(subject.message).to eq("You don't have the permission to perform this action")
expect(subject.status).to eq(:error)
expect(subject.reason).to eq(:unauthorized)
end
end
end
end

View File

@ -115,9 +115,7 @@ RSpec.describe RepositoryForkWorker do
context 'project ID, storage and repo paths passed' do
def perform!
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
subject.perform(forked_project.id, TestEnv.repos_path, project.disk_path)
end
subject.perform(forked_project.id, 'repos/path', project.disk_path)
end
it_behaves_like 'RepositoryForkWorker performing'