Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_KAS_VERSION2
-rw-r--r--app/graphql/gitlab_schema.rb13
-rw-r--r--app/graphql/mutations/packages/bulk_destroy.rb43
-rw-r--r--app/graphql/mutations/packages/destroy_files.rb4
-rw-r--r--app/graphql/types/mutation_type.rb2
-rw-r--r--app/models/packages/package.rb1
-rw-r--r--app/services/packages/mark_packages_for_destruction_service.rb79
-rw-r--r--doc/api/graphql/reference/index.md18
-rw-r--r--doc/user/profile/img/unknown_sign_in_email_v14_0.pngbin21301 -> 0 bytes
-rw-r--r--doc/user/profile/img/wrong_two_factor_authentication_code_v15_5.pngbin56036 -> 0 bytes
-rw-r--r--doc/user/profile/index.md5
-rw-r--r--doc/user/profile/notifications.md30
-rw-r--r--doc/user/profile/unknown_sign_in_notification.md35
-rw-r--r--doc/user/profile/wrong_two_factor_authentication_code_notification.md15
-rw-r--r--lib/gitlab/ci/templates/Elixir.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Flutter.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Go.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Gradle.gitlab-ci.yml7
-rw-r--r--lib/gitlab/ci/templates/Grails.gitlab-ci.yml5
-rw-r--r--spec/graphql/gitlab_schema_spec.rb103
-rw-r--r--spec/policies/project_snippet_policy_spec.rb328
-rw-r--r--spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb128
-rw-r--r--spec/services/packages/mark_packages_for_destruction_service_spec.rb107
-rw-r--r--spec/workers/repository_fork_worker_spec.rb4
24 files changed, 723 insertions, 221 deletions
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index 188dd74f5f5..2e0b428c416 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-15.5.0
+15.5.1
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index c0e063a34d5..37adf4c2d3b 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -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)
diff --git a/app/graphql/mutations/packages/bulk_destroy.rb b/app/graphql/mutations/packages/bulk_destroy.rb
new file mode 100644
index 00000000000..a0756d0c3f9
--- /dev/null
+++ b/app/graphql/mutations/packages/bulk_destroy.rb
@@ -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
diff --git a/app/graphql/mutations/packages/destroy_files.rb b/app/graphql/mutations/packages/destroy_files.rb
index 3900a2c46ae..60a21be20d8 100644
--- a/app/graphql/mutations/packages/destroy_files.rb
+++ b/app/graphql/mutations/packages/destroy_files.rb
@@ -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
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 109152a2f8a..5ffc1aeacad 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -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
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index b7c784d9351..a1d2a47c392 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -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) }
diff --git a/app/services/packages/mark_packages_for_destruction_service.rb b/app/services/packages/mark_packages_for_destruction_service.rb
new file mode 100644
index 00000000000..856e14f9fd3
--- /dev/null
+++ b/app/services/packages/mark_packages_for_destruction_service.rb
@@ -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
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 3f89ff08110..41d578bb95f 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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`
diff --git a/doc/user/profile/img/unknown_sign_in_email_v14_0.png b/doc/user/profile/img/unknown_sign_in_email_v14_0.png
deleted file mode 100644
index 62634739b78..00000000000
--- a/doc/user/profile/img/unknown_sign_in_email_v14_0.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/profile/img/wrong_two_factor_authentication_code_v15_5.png b/doc/user/profile/img/wrong_two_factor_authentication_code_v15_5.png
deleted file mode 100644
index 2774702f003..00000000000
--- a/doc/user/profile/img/wrong_two_factor_authentication_code_v15_5.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index ade5f912c93..66f6b4b52de 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -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
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index f66cf9fd0e1..7942ee38be9 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -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.
diff --git a/doc/user/profile/unknown_sign_in_notification.md b/doc/user/profile/unknown_sign_in_notification.md
index 731526bd61f..3bdcd36a34e 100644
--- a/doc/user/profile/unknown_sign_in_notification.md
+++ b/doc/user/profile/unknown_sign_in_notification.md
@@ -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 -->
diff --git a/doc/user/profile/wrong_two_factor_authentication_code_notification.md b/doc/user/profile/wrong_two_factor_authentication_code_notification.md
deleted file mode 100644
index 787830e7614..00000000000
--- a/doc/user/profile/wrong_two_factor_authentication_code_notification.md
+++ /dev/null
@@ -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)
diff --git a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
index 83ddce936e6..8b20c4cbccc 100644
--- a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
@@ -24,3 +24,8 @@ before_script:
mix:
script:
- mix test
+
+deploy:
+ stage: deploy
+ script: echo "Define your deployment script!"
+ environment: production
diff --git a/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml b/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml
index 021662ab416..7f81755348c 100644
--- a/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Flutter.gitlab-ci.yml
@@ -35,3 +35,8 @@ test:
- $CI_PROJECT_DIR/coverage
reports:
junit: report.xml
+
+deploy:
+ stage: deploy
+ script: echo "Define your deployment script!"
+ environment: production
diff --git a/lib/gitlab/ci/templates/Go.gitlab-ci.yml b/lib/gitlab/ci/templates/Go.gitlab-ci.yml
index 603aede4d46..8cfea3e236f 100644
--- a/lib/gitlab/ci/templates/Go.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Go.gitlab-ci.yml
@@ -28,3 +28,8 @@ compile:
artifacts:
paths:
- mybinaries
+
+deploy:
+ stage: deploy
+ script: echo "Define your deployment script!"
+ environment: production
diff --git a/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml b/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml
index 08dc10d34b7..671925c5df6 100644
--- a/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml
@@ -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
diff --git a/lib/gitlab/ci/templates/Grails.gitlab-ci.yml b/lib/gitlab/ci/templates/Grails.gitlab-ci.yml
index 03c8941169f..01697f67b89 100644
--- a/lib/gitlab/ci/templates/Grails.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Grails.gitlab-ci.yml
@@ -46,3 +46,8 @@ before_script:
build:
script:
- ./gradlew build
+
+deploy:
+ stage: deploy
+ script: echo "Define your deployment script!"
+ environment: production
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index 60b3edfc279..517897311e1 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -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,66 +245,85 @@ 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 'parses the gid' do
+ gid = parse_gid
- it 'raises an error' do
- expect { parse_gid }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID.")
+ expect(gid.model_id).to eq '2147483647'
+ expect(gid.model_class).to eq TestOne
end
- end
- context 'when using expected_type' do
- it 'accepts a single type' do
- gid = described_class.parse_gid(global_id, expected_type: TestOne)
+ 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 'accepts an ancestor type' do
- gid = described_class.parse_gid(global_id, expected_type: TestBase)
+ 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
+ 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
+ it 'accepts an ancestor type' do
+ gid = described_class.parse_gid(global_id, expected_type: TestBase)
- context 'when expected_type is an array' do
- subject(:parse_gid) { described_class.parse_gid(global_id, expected_type: [TestOne, TestTwo]) }
+ expect(gid.model_class).to eq TestOne
+ end
- 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
+ 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 global_id is of type TestTwo' do
- let_it_be(:global_id) { 'gid://gitlab/TestTwo/2147483647' }
+ context 'when expected_type is an array' do
+ subject(:parse_gid) { described_class.parse_gid(global_id, expected_type: [TestOne, TestTwo]) }
- it 'returns an object of an expected type' do
- expect(parse_gid.model_class).to eq 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
- end
- context 'when global_id is of type TestThree' do
- let_it_be(:global_id) { 'gid://gitlab/TestThree/2147483647' }
+ 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.")
+ 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
+
+ describe '.parse_gids' do
+ let_it_be(:global_ids) { %w[gid://gitlab/TestOne/123 gid://gitlab/TestOne/456] }
+
+ subject(:parse_gids) { described_class.parse_gids(global_ids, expected_type: TestOne) }
+
+ 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
end
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index 8b96aa99f69..c6d8ef05cfd 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -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)
+ expect_allowed(:read_snippet, :create_note, :update_snippet)
+ expect_disallowed(:admin_snippet)
end
end
- context 'project 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_allowed(:read_snippet, :create_note, *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_allowed(:read_snippet, :create_note, *author_permissions)
end
end
- context 'not a project member' do
+ context 'member (maintainer)' do
+ before do
+ membership_target.add_maintainer(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
end
end
- context 'public snippet' do
- let(:snippet_visibility) { :public }
-
- context 'no user' do
- let(:current_user) { nil }
+ shared_examples 'regular user non-member author permissions' do
+ let(:author) { current_user }
- it do
- expect_allowed(:read_snippet)
- expect_disallowed(*author_permissions)
- end
+ it do
+ expect_allowed(:read_snippet, :create_note, :update_snippet)
+ expect_disallowed(:admin_snippet)
end
+ end
- context 'regular user' do
- let(:current_user) { regular_user }
-
- it do
- expect_allowed(:read_snippet, :create_note)
- expect_disallowed(*author_permissions)
- end
+ context 'when project is public' do
+ let_it_be(:project) { create(:project, :public, group: group) }
- it_behaves_like 'regular user access rights'
- end
+ context 'with public snippet' do
+ let(:snippet_visibility) { :public }
- context 'external user' do
- let(:current_user) { external_user }
+ context 'no user' do
+ let(:current_user) { nil }
- it do
- expect_allowed(:read_snippet, :create_note)
- expect_disallowed(*author_permissions)
+ it do
+ expect_allowed(:read_snippet)
+ expect_disallowed(*author_permissions)
+ end
end
- context 'project team member' do
- before do
- project.add_developer(external_user)
+ 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 'internal snippet' do
- let(:snippet_visibility) { :internal }
- context 'no user' do
- let(:current_user) { nil }
+ context 'when user is a member' do
+ before do
+ project.add_developer(external_user)
+ end
- it do
- expect_disallowed(:read_snippet)
- expect_disallowed(*author_permissions)
+ it do
+ expect_allowed(:read_snippet, :create_note)
+ expect_disallowed(*author_permissions)
+ end
+ end
end
end
- context 'regular user' do
- let(:current_user) { regular_user }
+ context 'with internal snippet' do
+ let(:snippet_visibility) { :internal }
- it do
- expect_allowed(:read_snippet, :create_note)
- expect_disallowed(*author_permissions)
- end
+ context 'no user' do
+ let(:current_user) { nil }
- it_behaves_like 'regular user access rights'
- end
+ it do
+ expect_disallowed(:read_snippet)
+ expect_disallowed(*author_permissions)
+ end
+ end
- context 'external user' do
- let(:current_user) { external_user }
+ 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
- it do
- expect_disallowed(:read_snippet, :create_note)
- expect_disallowed(*author_permissions)
+ context 'when user is a member' do
+ it_behaves_like 'regular user member permissions'
+ end
end
- context 'project team member' do
- before do
- project.add_developer(external_user)
- end
+ context 'external user' do
+ let(:current_user) { external_user }
it do
- expect_allowed(:read_snippet, :create_note)
+ 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
- end
- context 'private snippet' do
- let(:snippet_visibility) { :private }
+ context 'with private snippet' do
+ let(:snippet_visibility) { :private }
- context 'no user' do
- let(:current_user) { nil }
+ context 'no user' do
+ let(:current_user) { nil }
- it do
- expect_disallowed(:read_snippet)
- expect_disallowed(*author_permissions)
+ it do
+ expect_disallowed(:read_snippet)
+ expect_disallowed(*author_permissions)
+ end
end
- end
- context 'regular user' do
- let(:current_user) { regular_user }
+ 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
- it do
- expect_disallowed(:read_snippet, :create_note)
- expect_disallowed(*author_permissions)
+ context 'when user is a member' do
+ it_behaves_like 'regular user member permissions'
+ end
end
- it_behaves_like 'regular user access rights'
- end
-
- context 'external user' do
- let(:current_user) { external_user }
+ context 'inherited user' do
+ let(:current_user) { regular_user }
+ let(:membership_target) { group }
- it do
- expect_disallowed(:read_snippet, :create_note)
- expect_disallowed(*author_permissions)
+ it_behaves_like 'regular user member permissions'
end
- context 'project team member' do
- before do
- project.add_developer(current_user)
- end
+ context 'external user' do
+ let(:current_user) { external_user }
it do
- expect_allowed(:read_snippet, :create_note)
+ expect_disallowed(: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 user is a member' do
+ before do
+ project.add_developer(current_user)
+ end
- context 'when admin mode is enabled', :enable_admin_mode do
- it do
- expect_allowed(:read_snippet, :create_note)
- expect_allowed(*author_permissions)
+ it do
+ expect_allowed(:read_snippet, :create_note)
+ expect_disallowed(*author_permissions)
+ end
end
end
- context 'when admin mode is disabled' do
- it do
- expect_disallowed(:read_snippet, :create_note)
- expect_disallowed(*author_permissions)
+ 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
diff --git a/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb
new file mode 100644
index 00000000000..1fe01af4f1c
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb
@@ -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
diff --git a/spec/services/packages/mark_packages_for_destruction_service_spec.rb b/spec/services/packages/mark_packages_for_destruction_service_spec.rb
new file mode 100644
index 00000000000..5c043b89de8
--- /dev/null
+++ b/spec/services/packages/mark_packages_for_destruction_service_spec.rb
@@ -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
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index e78b696268f..85dee935001 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -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'