Commit d465acca authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch '276882-package-maven-dupe-settings' into 'master'

Add setting to allow Maven package duplicates

See merge request gitlab-org/gitlab!50104
parents 8d49a576 24c7f030
# frozen_string_literal: true
module Mutations
module ResolvesNamespace
extend ActiveSupport::Concern
def resolve_namespace(full_path:)
namespace_resolver.resolve(full_path: full_path)
end
def namespace_resolver
Resolvers::NamespaceResolver.new(object: nil, context: context, field: nil)
end
end
end
# frozen_string_literal: true
module Mutations
module Namespace
module PackageSettings
class Update < Mutations::BaseMutation
include Mutations::ResolvesNamespace
graphql_name 'UpdateNamespacePackageSettings'
authorize :create_package_settings
argument :namespace_path,
GraphQL::ID_TYPE,
required: true,
description: 'The namespace path where the namespace package setting is located.'
argument :maven_duplicates_allowed,
GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_duplicates_allowed)
argument :maven_duplicate_exception_regex,
Types::UntrustedRegexp,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_duplicate_exception_regex)
field :package_settings,
Types::Namespace::PackageSettingsType,
null: true,
description: 'The namespace package setting after mutation.'
def resolve(namespace_path:, **args)
namespace = authorized_find!(namespace_path: namespace_path)
result = ::Namespaces::PackageSettings::UpdateService
.new(container: namespace, current_user: current_user, params: args)
.execute
{
package_settings: result.payload[:package_settings],
errors: result.errors
}
end
private
def find_object(namespace_path:)
resolve_namespace(full_path: namespace_path)
end
end
end
end
end
...@@ -91,6 +91,7 @@ module Types ...@@ -91,6 +91,7 @@ module Types
mount_mutation Mutations::Ci::Pipeline::Cancel mount_mutation Mutations::Ci::Pipeline::Cancel
mount_mutation Mutations::Ci::Pipeline::Destroy mount_mutation Mutations::Ci::Pipeline::Destroy
mount_mutation Mutations::Ci::Pipeline::Retry mount_mutation Mutations::Ci::Pipeline::Retry
mount_mutation Mutations::Namespace::PackageSettings::Update
end end
end end
......
# frozen_string_literal: true
module Types
class Namespace::PackageSettingsType < BaseObject
graphql_name 'PackageSettings'
description 'Namespace-level Package Registry settings'
authorize :read_package_settings
field :maven_duplicates_allowed, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
field :maven_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
end
end
...@@ -37,6 +37,11 @@ module Types ...@@ -37,6 +37,11 @@ module Types
description: 'Projects within this namespace', description: 'Projects within this namespace',
resolver: ::Resolvers::NamespaceProjectsResolver resolver: ::Resolvers::NamespaceProjectsResolver
field :package_settings,
Types::Namespace::PackageSettingsType,
null: true,
description: 'The package settings for the namespace'
def root_storage_statistics def root_storage_statistics
Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find
end end
......
...@@ -40,6 +40,7 @@ class Namespace < ApplicationRecord ...@@ -40,6 +40,7 @@ class Namespace < ApplicationRecord
has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics' has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics'
has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule' has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule'
has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting'
validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name, validates :name,
...@@ -160,6 +161,10 @@ class Namespace < ApplicationRecord ...@@ -160,6 +161,10 @@ class Namespace < ApplicationRecord
end end
end end
def package_settings
package_setting_relation || build_package_setting_relation
end
def default_branch_protection def default_branch_protection
super || Gitlab::CurrentSettings.default_branch_protection super || Gitlab::CurrentSettings.default_branch_protection
end end
......
# frozen_string_literal: true
class Namespace::PackageSetting < ApplicationRecord
self.primary_key = :namespace_id
self.table_name = 'namespace_package_settings'
belongs_to :namespace, inverse_of: :package_setting_relation
validates :namespace, presence: true
validates :maven_duplicates_allowed, inclusion: { in: [true, false] }
validates :maven_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
end
...@@ -116,6 +116,7 @@ class GroupPolicy < BasePolicy ...@@ -116,6 +116,7 @@ class GroupPolicy < BasePolicy
enable :delete_metrics_dashboard_annotation enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation
enable :create_custom_emoji enable :create_custom_emoji
enable :create_package_settings
end end
rule { reporter }.policy do rule { reporter }.policy do
...@@ -127,6 +128,7 @@ class GroupPolicy < BasePolicy ...@@ -127,6 +128,7 @@ class GroupPolicy < BasePolicy
enable :read_metrics_dashboard_annotation enable :read_metrics_dashboard_annotation
enable :read_prometheus enable :read_prometheus
enable :read_package enable :read_package
enable :read_package_settings
end end
rule { maintainer }.policy do rule { maintainer }.policy do
......
# frozen_string_literal: true
class Namespace::PackageSettingPolicy < BasePolicy
delegate { @subject.namespace }
end
...@@ -14,6 +14,8 @@ class NamespacePolicy < BasePolicy ...@@ -14,6 +14,8 @@ class NamespacePolicy < BasePolicy
enable :read_namespace enable :read_namespace
enable :read_statistics enable :read_statistics
enable :create_jira_connect_subscription enable :create_jira_connect_subscription
enable :create_package_settings
enable :read_package_settings
end end
rule { personal_project & ~can_create_personal_project }.prevent :create_projects rule { personal_project & ~can_create_personal_project }.prevent :create_projects
......
# frozen_string_literal: true
module Namespaces
module PackageSettings
class UpdateService < BaseContainerService
include Gitlab::Utils::StrongMemoize
ALLOWED_ATTRIBUTES = %i[maven_duplicates_allowed maven_duplicate_exception_regex].freeze
def execute
return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?
if package_settings.update(package_settings_params)
ServiceResponse.success(payload: { package_settings: package_settings })
else
ServiceResponse.error(
message: package_settings.errors.full_messages.to_sentence || 'Bad request',
http_status: 400
)
end
end
private
def package_settings
strong_memoize(:package_settings) do
@container.package_settings
end
end
def allowed?
Ability.allowed?(current_user, :create_package_settings, @container)
end
def package_settings_params
@params.slice(*ALLOWED_ATTRIBUTES)
end
end
end
end
---
title: Add namespace settings to allow or disallow duplicate Maven packages
merge_request: 50104
author:
type: added
# frozen_string_literal: true
class CreateNamespacePackageSettings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
create_table :namespace_package_settings, if_not_exists: true, id: false do |t|
t.references :namespace, primary_key: true, index: false, default: nil, foreign_key: { to_table: :namespaces, on_delete: :cascade }, type: :bigint
t.boolean :maven_duplicates_allowed, null: false, default: true
t.text :maven_duplicate_exception_regex, null: false, default: ''
end
end
add_text_limit :namespace_package_settings, :maven_duplicate_exception_regex, 255
end
def down
drop_table :namespace_package_settings
end
end
b54cf8b75c5882f2dfca74b218a9eeac766ff65233d43de865717d3ab8c7a217
\ No newline at end of file
...@@ -14173,6 +14173,13 @@ CREATE SEQUENCE namespace_onboarding_actions_id_seq ...@@ -14173,6 +14173,13 @@ CREATE SEQUENCE namespace_onboarding_actions_id_seq
ALTER SEQUENCE namespace_onboarding_actions_id_seq OWNED BY namespace_onboarding_actions.id; ALTER SEQUENCE namespace_onboarding_actions_id_seq OWNED BY namespace_onboarding_actions.id;
CREATE TABLE namespace_package_settings (
namespace_id bigint NOT NULL,
maven_duplicates_allowed boolean DEFAULT true NOT NULL,
maven_duplicate_exception_regex text DEFAULT ''::text NOT NULL,
CONSTRAINT check_d63274b2b6 CHECK ((char_length(maven_duplicate_exception_regex) <= 255))
);
CREATE TABLE namespace_root_storage_statistics ( CREATE TABLE namespace_root_storage_statistics (
namespace_id integer NOT NULL, namespace_id integer NOT NULL,
updated_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL,
...@@ -19892,6 +19899,9 @@ ALTER TABLE ONLY namespace_limits ...@@ -19892,6 +19899,9 @@ ALTER TABLE ONLY namespace_limits
ALTER TABLE ONLY namespace_onboarding_actions ALTER TABLE ONLY namespace_onboarding_actions
ADD CONSTRAINT namespace_onboarding_actions_pkey PRIMARY KEY (id); ADD CONSTRAINT namespace_onboarding_actions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY namespace_package_settings
ADD CONSTRAINT namespace_package_settings_pkey PRIMARY KEY (namespace_id);
ALTER TABLE ONLY namespace_root_storage_statistics ALTER TABLE ONLY namespace_root_storage_statistics
ADD CONSTRAINT namespace_root_storage_statistics_pkey PRIMARY KEY (namespace_id); ADD CONSTRAINT namespace_root_storage_statistics_pkey PRIMARY KEY (namespace_id);
...@@ -25291,6 +25301,9 @@ ALTER TABLE ONLY merge_request_metrics ...@@ -25291,6 +25301,9 @@ ALTER TABLE ONLY merge_request_metrics
ALTER TABLE ONLY draft_notes ALTER TABLE ONLY draft_notes
ADD CONSTRAINT fk_rails_e753681674 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_e753681674 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
ALTER TABLE ONLY namespace_package_settings
ADD CONSTRAINT fk_rails_e773444769 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY dast_site_tokens ALTER TABLE ONLY dast_site_tokens
ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......
...@@ -10518,6 +10518,11 @@ type Group { ...@@ -10518,6 +10518,11 @@ type Group {
""" """
name: String! name: String!
"""
The package settings for the namespace
"""
packageSettings: PackageSettings
""" """
Parent group Parent group
""" """
...@@ -15356,6 +15361,7 @@ type Mutation { ...@@ -15356,6 +15361,7 @@ type Mutation {
updateImageDiffNote(input: UpdateImageDiffNoteInput!): UpdateImageDiffNotePayload updateImageDiffNote(input: UpdateImageDiffNoteInput!): UpdateImageDiffNotePayload
updateIssue(input: UpdateIssueInput!): UpdateIssuePayload updateIssue(input: UpdateIssueInput!): UpdateIssuePayload
updateIteration(input: UpdateIterationInput!): UpdateIterationPayload updateIteration(input: UpdateIterationInput!): UpdateIterationPayload
updateNamespacePackageSettings(input: UpdateNamespacePackageSettingsInput!): UpdateNamespacePackageSettingsPayload
""" """
Updates a Note. If the body of the Note contains only quick actions, the Note Updates a Note. If the body of the Note contains only quick actions, the Note
...@@ -15479,6 +15485,11 @@ type Namespace { ...@@ -15479,6 +15485,11 @@ type Namespace {
""" """
name: String! name: String!
"""
The package settings for the namespace
"""
packageSettings: PackageSettings
""" """
Path of the namespace Path of the namespace
""" """
...@@ -16408,6 +16419,22 @@ type PackageFileRegistryEdge { ...@@ -16408,6 +16419,22 @@ type PackageFileRegistryEdge {
node: PackageFileRegistry node: PackageFileRegistry
} }
"""
Namespace-level Package Registry settings
"""
type PackageSettings {
"""
When maven_duplicates_allowed is false, you can publish duplicate packages
with names that match this regex. Otherwise, this setting has no effect.
"""
mavenDuplicateExceptionRegex: UntrustedRegexp
"""
Indicates whether duplicate Maven packages are allowed for this namespace.
"""
mavenDuplicatesAllowed: Boolean!
}
enum PackageTypeEnum { enum PackageTypeEnum {
""" """
Packages from the Composer package manager Packages from the Composer package manager
...@@ -24805,6 +24832,52 @@ type UpdateIterationPayload { ...@@ -24805,6 +24832,52 @@ type UpdateIterationPayload {
iteration: Iteration iteration: Iteration
} }
"""
Autogenerated input type of UpdateNamespacePackageSettings
"""
input UpdateNamespacePackageSettingsInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
When maven_duplicates_allowed is false, you can publish duplicate packages
with names that match this regex. Otherwise, this setting has no effect.
"""
mavenDuplicateExceptionRegex: UntrustedRegexp
"""
Indicates whether duplicate Maven packages are allowed for this namespace.
"""
mavenDuplicatesAllowed: Boolean
"""
The namespace path where the namespace package setting is located.
"""
namespacePath: ID!
}
"""
Autogenerated return type of UpdateNamespacePackageSettings
"""
type UpdateNamespacePackageSettingsPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The namespace package setting after mutation.
"""
packageSettings: PackageSettings
}
""" """
Autogenerated input type of UpdateNote Autogenerated input type of UpdateNote
""" """
......
...@@ -28760,6 +28760,20 @@ ...@@ -28760,6 +28760,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "packageSettings",
"description": "The package settings for the namespace",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PackageSettings",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "parent", "name": "parent",
"description": "Parent group", "description": "Parent group",
...@@ -45317,6 +45331,33 @@ ...@@ -45317,6 +45331,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "updateNamespacePackageSettings",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateNamespacePackageSettingsInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateNamespacePackageSettingsPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "updateNote", "name": "updateNote",
"description": "Updates a Note. If the body of the Note contains only quick actions, the Note will be destroyed during the update, and no Note will be returned", "description": "Updates a Note. If the body of the Note contains only quick actions, the Note will be destroyed during the update, and no Note will be returned",
...@@ -45843,6 +45884,20 @@ ...@@ -45843,6 +45884,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "packageSettings",
"description": "The package settings for the namespace",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PackageSettings",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "path", "name": "path",
"description": "Path of the namespace", "description": "Path of the namespace",
...@@ -48550,6 +48605,51 @@ ...@@ -48550,6 +48605,51 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "PackageSettings",
"description": "Namespace-level Package Registry settings",
"fields": [
{
"name": "mavenDuplicateExceptionRegex",
"description": "When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "UntrustedRegexp",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mavenDuplicatesAllowed",
"description": "Indicates whether duplicate Maven packages are allowed for this namespace.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "ENUM", "kind": "ENUM",
"name": "PackageTypeEnum", "name": "PackageTypeEnum",
...@@ -72132,6 +72232,128 @@ ...@@ -72132,6 +72232,128 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "UpdateNamespacePackageSettingsInput",
"description": "Autogenerated input type of UpdateNamespacePackageSettings",
"fields": null,
"inputFields": [
{
"name": "namespacePath",
"description": "The namespace path where the namespace package setting is located.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "mavenDuplicatesAllowed",
"description": "Indicates whether duplicate Maven packages are allowed for this namespace.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "mavenDuplicateExceptionRegex",
"description": "When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.",
"type": {
"kind": "SCALAR",
"name": "UntrustedRegexp",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UpdateNamespacePackageSettingsPayload",
"description": "Autogenerated return type of UpdateNamespacePackageSettings",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "packageSettings",
"description": "The namespace package setting after mutation.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PackageSettings",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "UpdateNoteInput", "name": "UpdateNoteInput",
...@@ -1608,6 +1608,7 @@ Represents an external issue. ...@@ -1608,6 +1608,7 @@ Represents an external issue.
| `mergeRequests` | MergeRequestConnection | Merge requests for projects in this group | | `mergeRequests` | MergeRequestConnection | Merge requests for projects in this group |
| `milestones` | MilestoneConnection | Milestones of the group | | `milestones` | MilestoneConnection | Milestones of the group |
| `name` | String! | Name of the namespace | | `name` | String! | Name of the namespace |
| `packageSettings` | PackageSettings | The package settings for the namespace |
| `parent` | Group | Parent group | | `parent` | Group | Parent group |
| `path` | String! | Path of the namespace | | `path` | String! | Path of the namespace |
| `projectCreationLevel` | String | The permission level required to create projects in the group | | `projectCreationLevel` | String | The permission level required to create projects in the group |
...@@ -2343,6 +2344,7 @@ Contains statistics about a milestone. ...@@ -2343,6 +2344,7 @@ Contains statistics about a milestone.
| `isTemporaryStorageIncreaseEnabled` | Boolean! | Status of the temporary storage increase | | `isTemporaryStorageIncreaseEnabled` | Boolean! | Status of the temporary storage increase |
| `lfsEnabled` | Boolean | Indicates if Large File Storage (LFS) is enabled for namespace | | `lfsEnabled` | Boolean | Indicates if Large File Storage (LFS) is enabled for namespace |
| `name` | String! | Name of the namespace | | `name` | String! | Name of the namespace |
| `packageSettings` | PackageSettings | The package settings for the namespace |
| `path` | String! | Path of the namespace | | `path` | String! | Path of the namespace |
| `projects` | ProjectConnection! | Projects within this namespace | | `projects` | ProjectConnection! | Projects within this namespace |
| `repositorySizeExcessProjectCount` | Int! | Number of projects in the root namespace where the repository size exceeds the limit | | `repositorySizeExcessProjectCount` | Int! | Number of projects in the root namespace where the repository size exceeds the limit |
...@@ -2476,6 +2478,15 @@ Represents the Geo sync and verification state of a package file. ...@@ -2476,6 +2478,15 @@ Represents the Geo sync and verification state of a package file.
| `retryCount` | Int | Number of consecutive failed sync attempts of the PackageFileRegistry | | `retryCount` | Int | Number of consecutive failed sync attempts of the PackageFileRegistry |
| `state` | RegistryState | Sync state of the PackageFileRegistry | | `state` | RegistryState | Sync state of the PackageFileRegistry |
### PackageSettings
Namespace-level Package Registry settings.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `mavenDuplicateExceptionRegex` | UntrustedRegexp | When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. |
| `mavenDuplicatesAllowed` | Boolean! | Indicates whether duplicate Maven packages are allowed for this namespace. |
### PageInfo ### PageInfo
Information about pagination in a connection.. Information about pagination in a connection..
...@@ -3712,6 +3723,16 @@ Autogenerated return type of UpdateIteration. ...@@ -3712,6 +3723,16 @@ Autogenerated return type of UpdateIteration.
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `iteration` | Iteration | Updated iteration. | | `iteration` | Iteration | Updated iteration. |
### UpdateNamespacePackageSettingsPayload
Autogenerated return type of UpdateNamespacePackageSettings.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `packageSettings` | PackageSettings | The namespace package setting after mutation. |
### UpdateNotePayload ### UpdateNotePayload
Autogenerated return type of UpdateNote. Autogenerated return type of UpdateNote.
......
...@@ -41,7 +41,7 @@ module Mutations ...@@ -41,7 +41,7 @@ module Mutations
end end
def namespace(namespace_path) def namespace(namespace_path)
::Gitlab::Graphql::Loaders::FullPathModelLoader.new(Namespace, namespace_path).find.sync ::Gitlab::Graphql::Loaders::FullPathModelLoader.new(::Namespace, namespace_path).find.sync
end end
end end
end end
......
# frozen_string_literal: true
FactoryBot.define do
factory :namespace_package_setting, class: 'Namespace::PackageSetting' do
namespace
maven_duplicates_allowed { true }
maven_duplicate_exception_regex { 'SNAPSHOT' }
trait :group do
namespace { association(:group) }
end
end
end
{
"type": "object",
"required": ["mavenDuplicatesAllowed", "mavenDuplicateExceptionRegex"],
"properties": {
"mavenDuplicatesAllowed": {
"type": "boolean"
},
"mavenDuplicateExceptionRegex": {
"type": "string"
}
}
}
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Namespace::PackageSettings::Update do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:namespace) { create(:group) }
let_it_be(:user) { create(:user) }
let(:params) { { namespace_path: namespace.full_path } }
specify { expect(described_class).to require_graphql_authorizations(:create_package_settings) }
describe '#resolve' do
subject { described_class.new(object: namespace, context: { current_user: user }, field: nil).resolve(**params) }
RSpec.shared_examples 'returning a success' do
it 'returns the namespace package setting with no errors' do
expect(subject).to eq(
package_settings: package_settings,
errors: []
)
end
end
RSpec.shared_examples 'updating the namespace package setting' do
it_behaves_like 'updating the namespace package setting attributes', from: { maven_duplicates_allowed: true, maven_duplicate_exception_regex: 'SNAPSHOT' }, to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'RELEASE' }
it_behaves_like 'returning a success'
context 'with invalid params' do
let_it_be(:params) { { namespace_path: namespace.full_path, maven_duplicate_exception_regex: '[' } }
it_behaves_like 'not creating the namespace package setting'
it 'doesn\'t update the maven_duplicates_allowed' do
expect { subject }
.not_to change { package_settings.reload.maven_duplicates_allowed }
end
it 'returns an error' do
expect(subject).to eq(
package_settings: nil,
errors: ['Maven duplicate exception regex not valid RE2 syntax: missing ]: [']
)
end
end
end
RSpec.shared_examples 'denying access to namespace package setting' do
it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'with existing namespace package setting' do
let_it_be(:package_settings) { create(:namespace_package_setting, namespace: namespace) }
let_it_be(:params) { { namespace_path: namespace.full_path, maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'RELEASE' } }
where(:user_role, :shared_examples_name) do
:maintainer | 'updating the namespace package setting'
:developer | 'updating the namespace package setting'
:reporter | 'denying access to namespace package setting'
:guest | 'denying access to namespace package setting'
:anonymous | 'denying access to namespace package setting'
end
with_them do
before do
namespace.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
context 'without existing namespace package setting' do
let_it_be(:package_settings) { namespace.package_settings }
where(:user_role, :shared_examples_name) do
:maintainer | 'creating the namespace package setting'
:developer | 'creating the namespace package setting'
:reporter | 'denying access to namespace package setting'
:guest | 'denying access to namespace package setting'
:anonymous | 'denying access to namespace package setting'
end
with_them do
before do
namespace.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageSettings'] do
specify { expect(described_class.graphql_name).to eq('PackageSettings') }
specify { expect(described_class.description).to eq('Namespace-level Package Registry settings') }
specify { expect(described_class).to require_graphql_authorizations(:read_package_settings) }
describe 'maven_duplicate_exception_regex field' do
subject { described_class.fields['mavenDuplicateExceptionRegex'] }
it { is_expected.to have_graphql_type(Types::UntrustedRegexp) }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespace::PackageSetting do
describe 'relationships' do
it { is_expected.to belong_to(:namespace) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:namespace) }
describe '#maven_duplicates_allowed' do
it { is_expected.to allow_value(true).for(:maven_duplicates_allowed) }
it { is_expected.to allow_value(false).for(:maven_duplicates_allowed) }
it { is_expected.not_to allow_value(nil).for(:maven_duplicates_allowed) }
end
describe '#maven_duplicate_exception_regex' do
let_it_be(:package_settings) { create(:namespace_package_setting) }
subject { package_settings }
valid_regexps = %w[SNAPSHOT .* v.+ v10.1.* (?:v.+|SNAPSHOT|TEMP)]
invalid_regexps = ['[', '(?:v.+|SNAPSHOT|TEMP']
valid_regexps.each do |valid_regexp|
it { is_expected.to allow_value(valid_regexp).for(:maven_duplicate_exception_regex) }
end
invalid_regexps.each do |invalid_regexp|
it { is_expected.not_to allow_value(invalid_regexp).for(:maven_duplicate_exception_regex) }
end
end
end
end
...@@ -20,6 +20,7 @@ RSpec.describe Namespace do ...@@ -20,6 +20,7 @@ RSpec.describe Namespace do
it { is_expected.to have_one :namespace_settings } it { is_expected.to have_one :namespace_settings }
it { is_expected.to have_many :custom_emoji } it { is_expected.to have_many :custom_emoji }
it { is_expected.to have_many :namespace_onboarding_actions } it { is_expected.to have_many :namespace_onboarding_actions }
it { is_expected.to have_one :package_setting_relation }
end end
describe 'validations' do describe 'validations' do
......
...@@ -8,7 +8,7 @@ RSpec.describe NamespacePolicy do ...@@ -8,7 +8,7 @@ RSpec.describe NamespacePolicy do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, owner: owner) } let(:namespace) { create(:namespace, owner: owner) }
let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects] } let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :create_package_settings, :read_package_settings] }
subject { described_class.new(current_user, namespace) } subject { described_class.new(current_user, namespace) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Updating the package settings' do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let(:params) do
{
namespace_path: namespace.full_path,
maven_duplicates_allowed: false,
maven_duplicate_exception_regex: 'foo-.*'
}
end
let(:mutation) do
graphql_mutation(:update_namespace_package_settings, params) do
<<~QL
packageSettings {
mavenDuplicatesAllowed
mavenDuplicateExceptionRegex
}
errors
QL
end
end
let(:mutation_response) { graphql_mutation_response(:update_namespace_package_settings) }
let(:package_settings_response) { mutation_response['packageSettings'] }
RSpec.shared_examples 'returning a success' do
it_behaves_like 'returning response status', :success
it 'returns the updated package settings', :aggregate_failures do
subject
expect(mutation_response['errors']).to be_empty
expect(package_settings_response['mavenDuplicatesAllowed']).to eq(params[:maven_duplicates_allowed])
expect(package_settings_response['mavenDuplicateExceptionRegex']).to eq(params[:maven_duplicate_exception_regex])
end
end
RSpec.shared_examples 'rejecting invalid regex' do
context "for field mavenDuplicateExceptionRegex" do
let_it_be(:invalid_regex) { '][' }
let(:params) do
{
:namespace_path => namespace.full_path,
'mavenDuplicateExceptionRegex' => invalid_regex
}
end
it_behaves_like 'returning response status', :success
it_behaves_like 'not creating the namespace package setting'
it 'returns an error', :aggregate_failures do
subject
expect(graphql_errors.size).to eq(1)
expect(graphql_errors.first['message']).to include("#{invalid_regex} is an invalid regexp")
end
end
end
RSpec.shared_examples 'accepting the mutation request updating the package settings' do
it_behaves_like 'updating the namespace package setting attributes',
from: { maven_duplicates_allowed: true, maven_duplicate_exception_regex: 'SNAPSHOT' },
to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'foo-.*' }
it_behaves_like 'returning a success'
it_behaves_like 'rejecting invalid regex'
end
RSpec.shared_examples 'accepting the mutation request creating the package settings' do
it_behaves_like 'creating the namespace package setting'
it_behaves_like 'returning a success'
it_behaves_like 'rejecting invalid regex'
end
RSpec.shared_examples 'denying the mutation request' do
it_behaves_like 'not creating the namespace package setting'
it_behaves_like 'returning response status', :success
it 'returns no response' do
subject
expect(mutation_response).to be_nil
end
end
describe 'post graphql mutation' do
subject { post_graphql_mutation(mutation, current_user: user) }
context 'with existing package settings' do
let_it_be(:package_settings, reload: true) { create(:namespace_package_setting, :group) }
let_it_be(:namespace, reload: true) { package_settings.namespace }
where(:user_role, :shared_examples_name) do
:maintainer | 'accepting the mutation request updating the package settings'
:developer | 'accepting the mutation request updating the package settings'
:reporter | 'denying the mutation request'
:guest | 'denying the mutation request'
:anonymous | 'denying the mutation request'
end
with_them do
before do
namespace.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
context 'without existing package settings' do
let_it_be(:namespace, reload: true) { create(:group) }
let(:package_settings) { namespace.package_settings }
where(:user_role, :shared_examples_name) do
:maintainer | 'accepting the mutation request creating the package settings'
:developer | 'accepting the mutation request creating the package settings'
:reporter | 'denying the mutation request'
:guest | 'denying the mutation request'
:anonymous | 'denying the mutation request'
end
with_them do
before do
namespace.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting namespace package settings in a namespace' do
include GraphqlHelpers
let_it_be(:package_settings) { create(:namespace_package_setting) }
let_it_be(:namespace) { package_settings.namespace }
let_it_be(:current_user) { namespace.owner }
let(:package_settings_response) { graphql_data.dig('namespace', 'packageSettings') }
let(:fields) { all_graphql_fields_for('PackageSettings') }
let(:query) do
graphql_query_for(
'namespace',
{ 'fullPath' => namespace.full_path },
query_graphql_field('package_settings', {}, fields)
)
end
subject { post_graphql(query, current_user: current_user) }
it_behaves_like 'a working graphql query' do
before do
subject
end
it 'matches the JSON schema' do
expect(package_settings_response).to match_schema('graphql/namespace/package_settings')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Namespaces::PackageSettings::UpdateService do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:namespace) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:params) { {} }
describe '#execute' do
subject { described_class.new(container: namespace, current_user: user, params: params).execute }
shared_examples 'returning a success' do
it 'returns a success' do
result = subject
expect(result.payload[:package_settings]).to be_present
expect(result.success?).to be_truthy
end
end
shared_examples 'returning an error' do |message, http_status|
it 'returns an error' do
result = subject
expect(result.message).to eq(message)
expect(result.status).to eq(:error)
expect(result.http_status).to eq(http_status)
end
end
shared_examples 'updating the namespace package setting' do
it_behaves_like 'updating the namespace package setting attributes', from: { maven_duplicates_allowed: true, maven_duplicate_exception_regex: 'SNAPSHOT' }, to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'RELEASE' }
it_behaves_like 'returning a success'
context 'with invalid params' do
let_it_be(:params) { { maven_duplicates_allowed: nil } }
it_behaves_like 'not creating the namespace package setting'
it "doesn't update the maven_duplicates_allowed" do
expect { subject }
.not_to change { package_settings.reload.maven_duplicates_allowed }
end
it_behaves_like 'returning an error', 'Maven duplicates allowed is not included in the list', 400
end
end
shared_examples 'denying access to namespace package setting' do
context 'with existing namespace package setting' do
it_behaves_like 'not creating the namespace package setting'
it_behaves_like 'returning an error', 'Access Denied', 403
end
end
context 'with existing namespace package setting' do
let_it_be(:package_settings) { create(:namespace_package_setting, namespace: namespace) }
let_it_be(:params) { { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'RELEASE' } }
where(:user_role, :shared_examples_name) do
:maintainer | 'updating the namespace package setting'
:developer | 'updating the namespace package setting'
:reporter | 'denying access to namespace package setting'
:guest | 'denying access to namespace package setting'
:anonymous | 'denying access to namespace package setting'
end
with_them do
before do
namespace.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
context 'without existing namespace package setting' do
let_it_be(:package_settings) { namespace.package_settings }
where(:user_role, :shared_examples_name) do
:maintainer | 'creating the namespace package setting'
:developer | 'creating the namespace package setting'
:reporter | 'denying access to namespace package setting'
:guest | 'denying access to namespace package setting'
:anonymous | 'denying access to namespace package setting'
end
with_them do
before do
namespace.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
end
end
...@@ -19,8 +19,28 @@ RSpec.shared_context 'GroupPolicy context' do ...@@ -19,8 +19,28 @@ RSpec.shared_context 'GroupPolicy context' do
end end
let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] } let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] }
let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation read_prometheus] }
let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] } let(:reporter_permissions) do
%i[
admin_label
read_container_image
read_metrics_dashboard_annotation
read_prometheus
read_package_settings
]
end
let(:developer_permissions) do
%i[
admin_milestone
create_metrics_dashboard_annotation
delete_metrics_dashboard_annotation
update_metrics_dashboard_annotation
create_custom_emoji
create_package_settings
]
end
let(:maintainer_permissions) do let(:maintainer_permissions) do
%i[ %i[
create_projects create_projects
......
# frozen_string_literal: true
RSpec.shared_examples 'updating the namespace package setting attributes' do |from: {}, to:|
it_behaves_like 'not creating the namespace package setting'
it 'updates the namespace package setting' do
expect { subject }
.to change { namespace.package_settings.reload.maven_duplicates_allowed }.from(from[:maven_duplicates_allowed]).to(to[:maven_duplicates_allowed])
.and change { namespace.package_settings.reload.maven_duplicate_exception_regex }.from(from[:maven_duplicate_exception_regex]).to(to[:maven_duplicate_exception_regex])
end
end
RSpec.shared_examples 'not creating the namespace package setting' do
it "doesn't create the namespace package setting" do
expect { subject }.not_to change { Namespace::PackageSetting.count }
end
end
RSpec.shared_examples 'creating the namespace package setting' do
it 'creates a new package setting' do
expect { subject }.to change { Namespace::PackageSetting.count }.by(1)
end
it 'saves the settings', :aggregate_failures do
subject
expect(namespace.package_setting_relation.maven_duplicates_allowed).to eq(package_settings[:maven_duplicates_allowed])
expect(namespace.package_setting_relation.maven_duplicate_exception_regex).to eq(package_settings[:maven_duplicate_exception_regex])
end
it_behaves_like 'returning a success'
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment