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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-14 03:10:45 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-14 03:10:45 +0300
commitc7e53e67898682ac7ccb61525f66980989ddb75e (patch)
tree2b79592f82d78bbe393af80f73e5917f2b68540e
parent2d4f258f067f55894862e47ec32bbc54649d4741 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/graphql/mutations/dependency_proxy/image_ttl_group_policy/update.rb54
-rw-r--r--app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb16
-rw-r--r--app/graphql/types/group_type.rb5
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/user.rb27
-rw-r--r--app/policies/dependency_proxy/image_ttl_group_policy_policy.rb6
-rw-r--r--app/policies/group_policy.rb5
-rw-r--r--app/services/dependency_proxy/image_ttl_group_policies/update_service.rb41
-rw-r--r--config/feature_flags/development/infinitely_collapsible_sections.yml2
-rw-r--r--config/gitlab.yml.example5
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--data/deprecations/templates/example.yml8
-rw-r--r--doc/api/graphql/reference/index.md71
-rw-r--r--doc/ci/yaml/index.md68
-rw-r--r--doc/user/admin_area/index.md12
-rw-r--r--doc/user/profile/index.md11
-rw-r--r--doc/user/project/integrations/img/slack_setup.pngbin86314 -> 65156 bytes
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md5
-rw-r--r--lib/gitlab/kas/client.rb12
-rw-r--r--spec/factories/dependency_proxy/image_ttl_group_policies.rb10
-rw-r--r--spec/features/atom/merge_requests_spec.rb4
-rw-r--r--spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb110
-rw-r--r--spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb17
-rw-r--r--spec/graphql/types/group_type_spec.rb4
-rw-r--r--spec/lib/gitlab/kas/client_spec.rb27
-rw-r--r--spec/models/group_spec.rb24
-rw-r--r--spec/models/user_spec.rb16
-rw-r--r--spec/policies/group_policy_spec.rb40
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb77
-rw-r--r--spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb70
-rw-r--r--spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb119
-rw-r--r--spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb34
35 files changed, 847 insertions, 65 deletions
diff --git a/Gemfile b/Gemfile
index f5e479dcf21..03e55fefd73 100644
--- a/Gemfile
+++ b/Gemfile
@@ -32,7 +32,7 @@ gem 'bcrypt', '~> 3.1', '>= 3.1.14'
gem 'doorkeeper', '~> 5.5.0.rc2'
gem 'doorkeeper-openid_connect', '~> 1.7.5'
gem 'rexml', '~> 3.2.5'
-gem 'ruby-saml', '~> 1.12.1'
+gem 'ruby-saml', '~> 1.13.0'
gem 'omniauth', '~> 1.8'
gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-activedirectory-v2', '~> 1.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 8b8cd1691e0..50f396a5743 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1124,7 +1124,7 @@ GEM
mini_portile2 (~> 2.5.0)
ruby-prof (1.3.1)
ruby-progressbar (1.11.0)
- ruby-saml (1.12.1)
+ ruby-saml (1.13.0)
nokogiri (>= 1.10.5)
rexml
ruby-statistics (2.1.2)
@@ -1606,7 +1606,7 @@ DEPENDENCIES
ruby-magic (~> 0.4)
ruby-prof (~> 1.3.0)
ruby-progressbar (~> 1.10)
- ruby-saml (~> 1.12.1)
+ ruby-saml (~> 1.13.0)
ruby_parser (~> 3.15)
rubyzip (~> 2.0.0)
rugged (~> 1.1)
diff --git a/app/graphql/mutations/dependency_proxy/image_ttl_group_policy/update.rb b/app/graphql/mutations/dependency_proxy/image_ttl_group_policy/update.rb
new file mode 100644
index 00000000000..a5eb114b2da
--- /dev/null
+++ b/app/graphql/mutations/dependency_proxy/image_ttl_group_policy/update.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Mutations
+ module DependencyProxy
+ module ImageTtlGroupPolicy
+ class Update < Mutations::BaseMutation
+ include Mutations::ResolvesGroup
+
+ graphql_name 'UpdateDependencyProxyImageTtlGroupPolicy'
+
+ authorize :admin_dependency_proxy
+
+ argument :group_path,
+ GraphQL::Types::ID,
+ required: true,
+ description: 'Group path for the group dependency proxy image TTL policy.'
+
+ argument :enabled,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: copy_field_description(Types::DependencyProxy::ImageTtlGroupPolicyType, :enabled)
+
+ argument :ttl,
+ GraphQL::Types::Int,
+ required: false,
+ description: copy_field_description(Types::DependencyProxy::ImageTtlGroupPolicyType, :ttl)
+
+ field :dependency_proxy_image_ttl_policy,
+ Types::DependencyProxy::ImageTtlGroupPolicyType,
+ null: true,
+ description: 'Group image TTL policy after mutation.'
+
+ def resolve(group_path:, **args)
+ group = authorized_find!(group_path: group_path)
+
+ result = ::DependencyProxy::ImageTtlGroupPolicies::UpdateService
+ .new(container: group, current_user: current_user, params: args)
+ .execute
+
+ {
+ dependency_proxy_image_ttl_policy: result.payload[:dependency_proxy_image_ttl_policy],
+ errors: result.errors
+ }
+ end
+
+ private
+
+ def find_object(group_path:)
+ resolve_group(full_path: group_path)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb b/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb
new file mode 100644
index 00000000000..29bba7122d0
--- /dev/null
+++ b/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ class DependencyProxy::ImageTtlGroupPolicyType < BaseObject
+ graphql_name 'DependencyProxyImageTtlGroupPolicy'
+
+ description 'Group-level Dependency Proxy TTL policy settings'
+
+ authorize :read_dependency_proxy
+
+ field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether the policy is enabled or disabled.'
+ field :ttl, GraphQL::Types::Int, null: true, description: 'Number of days to retain a cached image file.'
+ field :created_at, Types::TimeType, null: true, description: 'Timestamp of creation.'
+ field :updated_at, Types::TimeType, null: true, description: 'Timestamp of the most recent update.'
+ end
+end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 80b87044298..8fe4ba557ea 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -163,6 +163,11 @@ module Types
null: false,
description: 'Prefix for pulling images when using the dependency proxy.'
+ field :dependency_proxy_image_ttl_policy,
+ Types::DependencyProxy::ImageTtlGroupPolicyType,
+ null: true,
+ description: 'Dependency proxy TTL policy for the group.'
+
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: group) do |titles, loader, args|
LabelsFinder
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 31f46f65733..11e9d896ff0 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -34,6 +34,7 @@ module Types
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji
mount_mutation Mutations::Discussions::ToggleResolve
+ mount_mutation Mutations::DependencyProxy::ImageTtlGroupPolicy::Update
mount_mutation Mutations::Environments::CanaryIngress::Update
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees
diff --git a/app/models/group.rb b/app/models/group.rb
index c6ab8ac7a64..3e1b40e21bd 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -764,6 +764,10 @@ class Group < Namespace
::CustomerRelations::Contact.where(group_id: self.id)
end
+ def dependency_proxy_image_ttl_policy
+ super || build_dependency_proxy_image_ttl_policy
+ end
+
private
def max_member_access(user_ids)
diff --git a/app/models/user.rb b/app/models/user.rb
index 8dd9f9f90c7..79df91e3d5f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -256,10 +256,7 @@ class User < ApplicationRecord
validates :website_url, allow_blank: true, url: true, if: :website_url_changed?
before_validation :sanitize_attrs
- before_validation :reset_secondary_emails, if: :email_changed?
before_save :default_private_profile_to_false
- before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
- before_save :set_commit_email, if: :commit_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
@@ -2029,30 +2026,6 @@ class User < ApplicationRecord
errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email)
end
- def set_notification_email
- if verified_emails.exclude?(notification_email)
- self.notification_email = nil
- end
- end
-
- def set_public_email
- if verified_emails.exclude?(public_email)
- self.public_email = ''
- end
- end
-
- def set_commit_email
- if verified_emails.exclude?(commit_email)
- self.commit_email = nil
- end
- end
-
- def reset_secondary_emails
- set_public_email
- set_commit_email
- set_notification_email
- end
-
def callouts_by_feature_name
@callouts_by_feature_name ||= callouts.index_by(&:feature_name)
end
diff --git a/app/policies/dependency_proxy/image_ttl_group_policy_policy.rb b/app/policies/dependency_proxy/image_ttl_group_policy_policy.rb
new file mode 100644
index 00000000000..cf7e1ded137
--- /dev/null
+++ b/app/policies/dependency_proxy/image_ttl_group_policy_policy.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+module DependencyProxy
+ class ImageTtlGroupPolicyPolicy < BasePolicy
+ delegate { @subject.group }
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index ffe908593ae..96cfa5c3e6c 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -228,8 +228,9 @@ class GroupPolicy < BasePolicy
rule { dependency_proxy_access_allowed & dependency_proxy_available }
.enable :read_dependency_proxy
- rule { developer & dependency_proxy_available }
- .enable :admin_dependency_proxy
+ rule { developer & dependency_proxy_available }.policy do
+ enable :admin_dependency_proxy
+ end
rule { can?(:admin_group) & resource_access_token_feature_available }.policy do
enable :read_resource_access_tokens
diff --git a/app/services/dependency_proxy/image_ttl_group_policies/update_service.rb b/app/services/dependency_proxy/image_ttl_group_policies/update_service.rb
new file mode 100644
index 00000000000..3c0b6412dc5
--- /dev/null
+++ b/app/services/dependency_proxy/image_ttl_group_policies/update_service.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ module ImageTtlGroupPolicies
+ class UpdateService < BaseContainerService
+ include Gitlab::Utils::StrongMemoize
+
+ ALLOWED_ATTRIBUTES = %i[enabled ttl].freeze
+
+ def execute
+ return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?
+ return ServiceResponse.error(message: 'Dependency proxy image TTL Policy not found', http_status: 404) unless dependency_proxy_image_ttl_policy
+
+ if dependency_proxy_image_ttl_policy.update(dependency_proxy_image_ttl_policy_params)
+ ServiceResponse.success(payload: { dependency_proxy_image_ttl_policy: dependency_proxy_image_ttl_policy })
+ else
+ ServiceResponse.error(
+ message: dependency_proxy_image_ttl_policy.errors.full_messages.to_sentence || 'Bad request',
+ http_status: 400
+ )
+ end
+ end
+
+ private
+
+ def dependency_proxy_image_ttl_policy
+ strong_memoize(:dependency_proxy_image_ttl_policy) do
+ container.dependency_proxy_image_ttl_policy
+ end
+ end
+
+ def allowed?
+ Ability.allowed?(current_user, :admin_dependency_proxy, container)
+ end
+
+ def dependency_proxy_image_ttl_policy_params
+ params.slice(*ALLOWED_ATTRIBUTES)
+ end
+ end
+ end
+end
diff --git a/config/feature_flags/development/infinitely_collapsible_sections.yml b/config/feature_flags/development/infinitely_collapsible_sections.yml
index 44f37c06d70..d0bf063c6f6 100644
--- a/config/feature_flags/development/infinitely_collapsible_sections.yml
+++ b/config/feature_flags/development/infinitely_collapsible_sections.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335297
milestone: '14.1'
type: development
group: group::pipeline execution
-default_enabled: false
+default_enabled: true
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index a5e3b8b24e7..a8881fd8a2e 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -959,6 +959,11 @@ production: &base
# (default: false)
auto_link_saml_user: false
+ # CAUTION!
+ # Allows larger SAML messages to be received. Numeric value in bytes (default: 250000)
+ # Too high limits exposes instance to decompression DDoS attack type.
+ saml_message_max_byte_size: 250000
+
# Allow users with existing accounts to sign in and auto link their account via OmniAuth
# login, without having to do a manual login first and manually add OmniAuth. Links on email.
# Define the allowed providers using an array, e.g. ["saml", "twitter"], or as true/false to
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 34f8080ac23..1c22216d442 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -95,6 +95,7 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block
Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil?
Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil?
Settings.omniauth['auto_link_user'] = false if Settings.omniauth['auto_link_user'].nil?
+Settings.omniauth['saml_message_max_byte_size'] = 250000 if Settings.omniauth['saml_message_max_byte_size'].nil?
Settings.omniauth['sync_profile_from_provider'] = false if Settings.omniauth['sync_profile_from_provider'].nil?
Settings.omniauth['sync_profile_attributes'] = ['email'] if Settings.omniauth['sync_profile_attributes'].nil?
diff --git a/data/deprecations/templates/example.yml b/data/deprecations/templates/example.yml
index 18ec63c0a46..fa90dba6e84 100644
--- a/data/deprecations/templates/example.yml
+++ b/data/deprecations/templates/example.yml
@@ -16,13 +16,11 @@
body: | # Do not modify this line, instead modify the lines below.
<!-- START OF BODY COMMENT
- This area supports markdown.
+ This area supports markdown. Delete this entire comment and replace it with your markdown content.
- It is recommended to copy and paste the release post entry content here.
+ Make sure to run `bin/rake gitlab:docs:compile_deprecations` locally before committing and pushing your changes.
- You can shorten it if needed, but remember that the entry itself has already been reviewed by Tech Writing so don't feel like you need to reword anything.
-
- Delete this entire comment and replace it with your markdown content.
+ When ready, assign to your tech writer to review and merge.
END OF BODY COMMENT -->
stage: # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index fa19a502c8f..83d3e67e5fc 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -4180,6 +4180,27 @@ Input type: `UpdateContainerExpirationPolicyInput`
| <a id="mutationupdatecontainerexpirationpolicycontainerexpirationpolicy"></a>`containerExpirationPolicy` | [`ContainerExpirationPolicy`](#containerexpirationpolicy) | Container expiration policy after mutation. |
| <a id="mutationupdatecontainerexpirationpolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.updateDependencyProxyImageTtlGroupPolicy`
+
+Input type: `UpdateDependencyProxyImageTtlGroupPolicyInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationupdatedependencyproxyimagettlgrouppolicyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationupdatedependencyproxyimagettlgrouppolicyenabled"></a>`enabled` | [`Boolean`](#boolean) | Indicates whether the policy is enabled or disabled. |
+| <a id="mutationupdatedependencyproxyimagettlgrouppolicygrouppath"></a>`groupPath` | [`ID!`](#id) | Group path for the group dependency proxy image TTL policy. |
+| <a id="mutationupdatedependencyproxyimagettlgrouppolicyttl"></a>`ttl` | [`Int`](#int) | Number of days to retain a cached image file. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationupdatedependencyproxyimagettlgrouppolicyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationupdatedependencyproxyimagettlgrouppolicydependencyproxyimagettlpolicy"></a>`dependencyProxyImageTtlPolicy` | [`DependencyProxyImageTtlGroupPolicy`](#dependencyproxyimagettlgrouppolicy) | Group image TTL policy after mutation. |
+| <a id="mutationupdatedependencyproxyimagettlgrouppolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.updateEpic`
Input type: `UpdateEpicInput`
@@ -5249,6 +5270,29 @@ The edge type for [`ComplianceFramework`](#complianceframework).
| <a id="complianceframeworkedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="complianceframeworkedgenode"></a>`node` | [`ComplianceFramework`](#complianceframework) | The item at the end of the edge. |
+#### `ConnectedAgentConnection`
+
+The connection type for [`ConnectedAgent`](#connectedagent).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="connectedagentconnectionedges"></a>`edges` | [`[ConnectedAgentEdge]`](#connectedagentedge) | A list of edges. |
+| <a id="connectedagentconnectionnodes"></a>`nodes` | [`[ConnectedAgent]`](#connectedagent) | A list of nodes. |
+| <a id="connectedagentconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `ConnectedAgentEdge`
+
+The edge type for [`ConnectedAgent`](#connectedagent).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="connectedagentedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="connectedagentedgenode"></a>`node` | [`ConnectedAgent`](#connectedagent) | The item at the end of the edge. |
+
#### `ContainerRepositoryConnection`
The connection type for [`ContainerRepository`](#containerrepository).
@@ -8290,6 +8334,7 @@ GitLab CI/CD configuration template.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="clusteragentconnections"></a>`connections` | [`ConnectedAgentConnection`](#connectedagentconnection) | Active connections for the cluster agent. (see [Connections](#connections)) |
| <a id="clusteragentcreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp the cluster agent was created. |
| <a id="clusteragentcreatedbyuser"></a>`createdByUser` | [`UserCore`](#usercore) | User object, containing information about the person who created the agent. |
| <a id="clusteragentid"></a>`id` | [`ID!`](#id) | ID of the cluster agent. |
@@ -8452,6 +8497,18 @@ Conan metadata.
| <a id="conanmetadatarecipepath"></a>`recipePath` | [`String!`](#string) | Recipe path of the Conan package. |
| <a id="conanmetadataupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
+### `ConnectedAgent`
+
+Connection details for an Agent.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="connectedagentconnectedat"></a>`connectedAt` | [`Time`](#time) | When the connection was established. |
+| <a id="connectedagentconnectionid"></a>`connectionId` | [`BigInt`](#bigint) | ID of the connection. |
+| <a id="connectedagentmetadata"></a>`metadata` | [`JSON`](#json) | Information about the Agent. |
+
### `ContainerExpirationPolicy`
A tag expiration policy designed to keep only the images that matter most.
@@ -8752,6 +8809,19 @@ Dependency proxy blob.
| <a id="dependencyproxyblobsize"></a>`size` | [`String!`](#string) | Size of the blob file. |
| <a id="dependencyproxyblobupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
+### `DependencyProxyImageTtlGroupPolicy`
+
+Group-level Dependency Proxy TTL policy settings.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="dependencyproxyimagettlgrouppolicycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of creation. |
+| <a id="dependencyproxyimagettlgrouppolicyenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether the policy is enabled or disabled. |
+| <a id="dependencyproxyimagettlgrouppolicyttl"></a>`ttl` | [`Int`](#int) | Number of days to retain a cached image file. |
+| <a id="dependencyproxyimagettlgrouppolicyupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of the most recent update. |
+
### `DependencyProxyManifest`
Dependency proxy manifest.
@@ -9830,6 +9900,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupdependencyproxyblobs"></a>`dependencyProxyBlobs` | [`DependencyProxyBlobConnection`](#dependencyproxyblobconnection) | Dependency Proxy blobs. (see [Connections](#connections)) |
| <a id="groupdependencyproxyimagecount"></a>`dependencyProxyImageCount` | [`Int!`](#int) | Number of dependency proxy images cached in the group. |
| <a id="groupdependencyproxyimageprefix"></a>`dependencyProxyImagePrefix` | [`String!`](#string) | Prefix for pulling images when using the dependency proxy. |
+| <a id="groupdependencyproxyimagettlpolicy"></a>`dependencyProxyImageTtlPolicy` | [`DependencyProxyImageTtlGroupPolicy`](#dependencyproxyimagettlgrouppolicy) | Dependency proxy TTL policy for the group. |
| <a id="groupdependencyproxymanifests"></a>`dependencyProxyManifests` | [`DependencyProxyManifestConnection`](#dependencyproxymanifestconnection) | Dependency Proxy manifests. (see [Connections](#connections)) |
| <a id="groupdependencyproxysetting"></a>`dependencyProxySetting` | [`DependencyProxySetting`](#dependencyproxysetting) | Dependency Proxy settings for the group. |
| <a id="groupdependencyproxytotalsize"></a>`dependencyProxyTotalSize` | [`String!`](#string) | Total size of the dependency proxy cached images. |
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 10713f3ae7c..6f55d1b3604 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -1867,36 +1867,43 @@ In this example, only runners with *both* the `ruby` and `postgres` tags can run
### `allow_failure`
-Use `allow_failure` when you want to let a job fail without impacting the rest of the CI
-suite. The default value is `false`, except for [manual](../jobs/job_control.md#create-a-job-that-must-be-run-manually) jobs that use
-the [`when: manual`](#when) syntax.
+Use `allow_failure` to determine whether a pipeline should continue running when a job fails.
-In jobs that use [`rules:`](#rules), all jobs default to `allow_failure: false`,
-*including* `when: manual` jobs.
+- To let the pipeline continue running subsequent jobs, use `allow_failure: true`.
+- To stop the pipeline from running subsequent jobs, use `allow_failure: false`.
-When `allow_failure` is set to `true` and the job fails, the job shows an orange warning in the UI.
-However, the logical flow of the pipeline considers the job a
-success/passed, and is not blocked.
+When jobs are allowed to fail (`allow_failure: true`) an orange warning (**{status_warning}**)
+indicates that a job failed. However, the pipeline is successful and the associated commit
+is marked as passed with no warnings.
-Assuming all other jobs are successful, the job's stage and its pipeline
-show the same orange warning. However, the associated commit is marked as
-"passed", without warnings.
+This same warning is displayed when:
-In the following example, `job1` and `job2` run in parallel. If `job1`
-fails, it doesn't stop the next stage from running, because it's marked with
-`allow_failure: true`:
+- All other jobs in the stage are successful.
+- All other jobs in the pipeline are successful.
+
+The default value for `allow_failure` is:
+
+- `true` for [manual jobs](../jobs/job_control.md#create-a-job-that-must-be-run-manually).
+- `false` for manual jobs that also use [`rules`](#rules).
+- `false` in all other cases.
+
+**Keyword type**: Job keyword. You can use it only as part of a job.
+
+**Possible inputs**: `true` or `false`.
+
+**Example of `allow_failure`**:
```yaml
job1:
stage: test
script:
- - execute_script_that_will_fail
- allow_failure: true
+ - execute_script_1
job2:
stage: test
script:
- - execute_script_that_will_succeed
+ - execute_script_2
+ allow_failure: true
job3:
stage: deploy
@@ -1904,14 +1911,35 @@ job3:
- deploy_to_staging
```
+In this example, `job1` and `job2` run in parallel:
+
+- If `job1` fails, jobs in the `deploy` stage do not start.
+- If `job2` fails, jobs in the `deploy` stage can still start.
+
+**Additional details**:
+
+- You can use `allow_failure` as a subkey of [`rules:`](#rulesallow_failure).
+- You can use `allow_failure: false` with a manual job to create a [blocking manual job](../jobs/job_control.md#types-of-manual-jobs).
+ A blocked pipeline does not run any jobs in later stages until the manual job
+ is started and completes successfully.
+
#### `allow_failure:exit_codes`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/273157) in GitLab 13.8.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/292024) in GitLab 13.9.
-Use `allow_failure:exit_codes` to dynamically control if a job should be allowed
-to fail. You can list which exit codes are not considered failures. The job fails
-for any other exit code:
+Use `allow_failure:exit_codes` to control when a job should be
+allowed to fail. The job is `allow_failure: true` for any of the listed exit codes,
+and `allow_failure` false for any other exit code.
+
+**Keyword type**: Job keyword. You can use it only as part of a job.
+
+**Possible inputs**:
+
+- A single exit code.
+- An array of exit codes.
+
+**Example of `allow_failure`**:
```yaml
test_job_1:
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 85eca17f6e7..a5c3a2a7aeb 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -199,6 +199,18 @@ The following totals are also included:
GitLab billing is based on the number of [**Billable users**](../../subscriptions/self_managed/index.md#billable-users).
+#### Add email to user
+
+You must be an administrator to manually add emails to users:
+
+1. On the top bar, select **Menu > Admin**.
+1. On the left sidebar, select **Overview > Users** (`/admin/users`).
+1. Locate the user and select them.
+1. Select **Edit**.
+1. In **Email**, enter the new email address. This adds the new email address to the
+ user and sets the previous email address to be a secondary.
+1. Select **Save changes**.
+
### User cohorts
The [Cohorts](user_cohorts.md) tab displays the monthly cohorts of new users and their activities over time.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 82a56d00118..24006e6f875 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -59,6 +59,17 @@ To change your username:
1. In the **Change username** section, enter a new username as the path.
1. Select **Update username**.
+## Add emails to your user profile
+
+To add new email to your account:
+
+1. In the top-right corner, select your avatar.
+1. Select **Edit profile**.
+1. On the left sidebar, select **Emails**.
+1. In the **Email** box, enter the new email.
+1. Select **Add email address**.
+1. Verify your email address with the verification email received.
+
## Make your user profile page private
You can make your user profile visible to only you and GitLab administrators.
diff --git a/doc/user/project/integrations/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png
index 7928fb7d495..8acae659fb4 100644
--- a/doc/user/project/integrations/img/slack_setup.png
+++ b/doc/user/project/integrations/img/slack_setup.png
Binary files differ
diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md
index 3da50524a4d..066a2f83753 100644
--- a/doc/user/project/integrations/slack_slash_commands.md
+++ b/doc/user/project/integrations/slack_slash_commands.md
@@ -31,12 +31,13 @@ are scoped to a project.
Select **Add Slash Command Integration**.
1. Complete the rest of the fields in the Slack configuration page using information from the GitLab browser tab.
In particular, make sure you copy and paste the **URL**.
+
+ ![Slack setup instructions](img/slack_setup.png)
+
1. On the Slack configuration page, select **Save Integration** and copy the **Token**.
1. Go back to the GitLab configuration page and paste in the **Token**.
1. Ensure the **Active** checkbox is selected and select **Save changes**.
-![Slack setup instructions](img/slack_setup.png)
-
## Slash commands
You can now use the available [Slack slash commands](../../../integration/slash_commands.md).
diff --git a/lib/gitlab/kas/client.rb b/lib/gitlab/kas/client.rb
index 842ee98e4da..753a185344e 100644
--- a/lib/gitlab/kas/client.rb
+++ b/lib/gitlab/kas/client.rb
@@ -7,6 +7,7 @@ module Gitlab
JWT_AUDIENCE = 'gitlab-kas'
STUB_CLASSES = {
+ agent_tracker: Gitlab::Agent::AgentTracker::Rpc::AgentTracker::Stub,
configuration_project: Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub
}.freeze
@@ -17,6 +18,15 @@ module Gitlab
raise ConfigurationError, 'KAS internal URL is not configured' unless Gitlab::Kas.internal_url.present?
end
+ def get_connected_agents(project:)
+ request = Gitlab::Agent::AgentTracker::Rpc::GetConnectedAgentsRequest.new(project_id: project.id)
+
+ stub_for(:agent_tracker)
+ .get_connected_agents(request, metadata: metadata)
+ .agents
+ .to_a
+ end
+
def list_agent_config_files(project:)
request = Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest.new(
repository: repository(project),
@@ -49,7 +59,7 @@ module Gitlab
end
def kas_endpoint_url
- Gitlab::Kas.internal_url.sub(%r{^grpc://|^grpcs://}, '')
+ Gitlab::Kas.internal_url.sub(%r{^grpcs?://}, '')
end
def credentials
diff --git a/spec/factories/dependency_proxy/image_ttl_group_policies.rb b/spec/factories/dependency_proxy/image_ttl_group_policies.rb
new file mode 100644
index 00000000000..21e5dd44cf5
--- /dev/null
+++ b/spec/factories/dependency_proxy/image_ttl_group_policies.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :image_ttl_group_policy, class: 'DependencyProxy::ImageTtlGroupPolicy' do
+ group
+
+ enabled { true }
+ ttl { 90 }
+ end
+end
diff --git a/spec/features/atom/merge_requests_spec.rb b/spec/features/atom/merge_requests_spec.rb
index 29f0b4660c9..48db8fcbf1e 100644
--- a/spec/features/atom/merge_requests_spec.rb
+++ b/spec/features/atom/merge_requests_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe 'Merge Requests Feed' do
describe 'GET /merge_requests' do
- let_it_be_with_reload(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
- let_it_be(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
+ let_it_be_with_reload(:user) { create(:user, email: 'private1@example.com') }
+ let_it_be(:assignee) { create(:user, email: 'private2@example.com') }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, assignees: [assignee]) }
diff --git a/spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb b/spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb
new file mode 100644
index 00000000000..792e87f0d25
--- /dev/null
+++ b/spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::DependencyProxy::ImageTtlGroupPolicy::Update do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ let(:params) { { group_path: group.full_path } }
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_dependency_proxy) }
+
+ describe '#resolve' do
+ subject { described_class.new(object: group, context: { current_user: user }, field: nil).resolve(**params) }
+
+ shared_examples 'returning a success' do
+ it 'returns the dependency proxy image ttl group policy with no errors' do
+ expect(subject).to eq(
+ dependency_proxy_image_ttl_policy: ttl_policy,
+ errors: []
+ )
+ end
+ end
+
+ shared_examples 'updating the dependency proxy image ttl policy' do
+ it_behaves_like 'updating the dependency proxy image ttl policy attributes',
+ from: { enabled: true, ttl: 90 },
+ to: { enabled: false, ttl: 2 }
+
+ it_behaves_like 'returning a success'
+
+ context 'with invalid params' do
+ let_it_be(:params) { { group_path: group.full_path, enabled: nil } }
+
+ it "doesn't create the dependency proxy image ttl policy" do
+ expect { subject }.not_to change { DependencyProxy::ImageTtlGroupPolicy.count }
+ end
+
+ it 'does not update' do
+ expect { subject }
+ .not_to change { ttl_policy.reload.enabled }
+ end
+
+ it 'returns an error' do
+ expect(subject).to eq(
+ dependency_proxy_image_ttl_policy: nil,
+ errors: ['Enabled is not included in the list']
+ )
+ end
+ end
+ end
+
+ shared_examples 'denying access to dependency proxy image ttl policy' do
+ it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ context 'with existing dependency proxy image ttl policy' do
+ let_it_be(:ttl_policy) { create(:image_ttl_group_policy, group: group) }
+ let_it_be(:params) do
+ { group_path: group.full_path,
+ enabled: false,
+ ttl: 2 }
+ end
+
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'updating the dependency proxy image ttl policy'
+ :developer | 'updating the dependency proxy image ttl policy'
+ :reporter | 'denying access to dependency proxy image ttl policy'
+ :guest | 'denying access to dependency proxy image ttl policy'
+ :anonymous | 'denying access to dependency proxy image ttl policy'
+ end
+
+ with_them do
+ before do
+ group.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'without existing dependency proxy image ttl policy' do
+ let_it_be(:ttl_policy) { group.dependency_proxy_image_ttl_policy }
+
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'creating the dependency proxy image ttl policy'
+ :developer | 'creating the dependency proxy image ttl policy'
+ :reporter | 'denying access to dependency proxy image ttl policy'
+ :guest | 'denying access to dependency proxy image ttl policy'
+ :anonymous | 'denying access to dependency proxy image ttl policy'
+ end
+
+ with_them do
+ before do
+ group.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb b/spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb
new file mode 100644
index 00000000000..46347e0434f
--- /dev/null
+++ b/spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DependencyProxyImageTtlGroupPolicy'] do
+ it { expect(described_class.graphql_name).to eq('DependencyProxyImageTtlGroupPolicy') }
+
+ it { expect(described_class.description).to eq('Group-level Dependency Proxy TTL policy settings') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_dependency_proxy) }
+
+ it 'includes dependency proxy image ttl policy fields' do
+ expected_fields = %w[enabled ttl created_at updated_at]
+
+ expect(described_class).to have_graphql_fields(*expected_fields).only
+ end
+end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index 35fda69ffe3..dca2c930eea 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -21,8 +21,8 @@ RSpec.describe GitlabSchema.types['Group'] do
packages dependency_proxy_setting dependency_proxy_manifests
dependency_proxy_blobs dependency_proxy_image_count
dependency_proxy_blob_count dependency_proxy_total_size
- dependency_proxy_image_prefix shared_runners_setting
- timelogs organizations contacts
+ dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
+ shared_runners_setting timelogs organizations contacts
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/lib/gitlab/kas/client_spec.rb b/spec/lib/gitlab/kas/client_spec.rb
index 40e18f58ee4..5b89023cc13 100644
--- a/spec/lib/gitlab/kas/client_spec.rb
+++ b/spec/lib/gitlab/kas/client_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Kas::Client do
let_it_be(:project) { create(:project) }
+ let_it_be(:agent) { create(:cluster_agent, project: project) }
describe '#initialize' do
context 'kas is not enabled' do
@@ -44,6 +45,32 @@ RSpec.describe Gitlab::Kas::Client do
expect(token).to receive(:audience=).with(described_class::JWT_AUDIENCE)
end
+ describe '#get_connected_agents' do
+ let(:stub) { instance_double(Gitlab::Agent::AgentTracker::Rpc::AgentTracker::Stub) }
+ let(:request) { instance_double(Gitlab::Agent::AgentTracker::Rpc::GetConnectedAgentsRequest) }
+ let(:response) { double(Gitlab::Agent::AgentTracker::Rpc::GetConnectedAgentsResponse, agents: connected_agents) }
+
+ let(:connected_agents) { [double] }
+
+ subject { described_class.new.get_connected_agents(project: project) }
+
+ before do
+ expect(Gitlab::Agent::AgentTracker::Rpc::AgentTracker::Stub).to receive(:new)
+ .with('example.kas.internal', :this_channel_is_insecure, timeout: described_class::TIMEOUT)
+ .and_return(stub)
+
+ expect(Gitlab::Agent::AgentTracker::Rpc::GetConnectedAgentsRequest).to receive(:new)
+ .with(project_id: project.id)
+ .and_return(request)
+
+ expect(stub).to receive(:get_connected_agents)
+ .with(request, metadata: { 'authorization' => 'bearer test-token' })
+ .and_return(response)
+ end
+
+ it { expect(subject).to eq(connected_agents) }
+ end
+
describe '#list_agent_config_files' do
let(:stub) { instance_double(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 8e6be32ea3c..38f3eadbb22 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -2743,4 +2743,28 @@ RSpec.describe Group do
expect(group.dependency_proxy_image_prefix).not_to include('http')
end
end
+
+ describe '#dependency_proxy_image_ttl_policy' do
+ subject(:ttl_policy) { group.dependency_proxy_image_ttl_policy }
+
+ it 'builds a new policy if one does not exist', :aggregate_failures do
+ expect(ttl_policy.ttl).to eq(90)
+ expect(ttl_policy.enabled).to eq(false)
+ expect(ttl_policy.created_at).to be_nil
+ expect(ttl_policy.updated_at).to be_nil
+ end
+
+ context 'with existing policy' do
+ before do
+ group.dependency_proxy_image_ttl_policy.update!(ttl: 30, enabled: true)
+ end
+
+ it 'returns the policy if it already exists', :aggregate_failures do
+ expect(ttl_policy.ttl).to eq(30)
+ expect(ttl_policy.enabled).to eq(true)
+ expect(ttl_policy.created_at).not_to be_nil
+ expect(ttl_policy.updated_at).not_to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4a83d62c401..ba20343c30a 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -681,6 +681,22 @@ RSpec.describe User do
end
end
end
+
+ context 'when secondary email is same as primary' do
+ let(:user) { create(:user, email: 'user@example.com') }
+
+ it 'lets user change primary email without failing validations' do
+ user.commit_email = user.email
+ user.notification_email = user.email
+ user.public_email = user.email
+ user.save!
+
+ user.email = 'newemail@example.com'
+ user.confirm
+
+ expect(user).to be_valid
+ end
+ end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 95144fae9ce..482e12c029d 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -893,6 +893,34 @@ RSpec.describe GroupPolicy do
end
end
+ describe 'dependency proxy' do
+ context 'feature disabled' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_disallowed(:read_dependency_proxy) }
+ it { is_expected.to be_disallowed(:admin_dependency_proxy) }
+ end
+
+ context 'feature enabled' do
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ group.create_dependency_proxy_setting!(enabled: true)
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:admin_dependency_proxy) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_allowed(:admin_dependency_proxy) }
+ end
+ end
+ end
+
context 'deploy token access' do
let!(:group_deploy_token) do
create(:group_deploy_token, group: group, deploy_token: deploy_token)
@@ -920,6 +948,18 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:read_contact) }
it { is_expected.to be_disallowed(:destroy_package) }
end
+
+ context 'a deploy token with dependency proxy scopes' do
+ let_it_be(:deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) }
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ group.create_dependency_proxy_setting!(enabled: true)
+ end
+
+ it { is_expected.to be_allowed(:read_dependency_proxy) }
+ it { is_expected.to be_disallowed(:admin_dependency_proxy) }
+ end
end
it_behaves_like 'Self-managed Core resource access tokens'
diff --git a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb
new file mode 100644
index 00000000000..c8797d84906
--- /dev/null
+++ b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting dependency proxy image ttl policy for a group' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:group) { create(:group) }
+
+ let(:dependency_proxy_image_ttl_policy_fields) do
+ <<~GQL
+ #{all_graphql_fields_for('dependency_proxy_image_ttl_group_policy'.classify, max_depth: 1)}
+ GQL
+ end
+
+ let(:fields) do
+ <<~GQL
+ #{query_graphql_field('dependency_proxy_image_ttl_policy', {}, dependency_proxy_image_ttl_policy_fields)}
+ GQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { 'fullPath' => group.full_path },
+ fields
+ )
+ end
+
+ let(:variables) { {} }
+ let(:dependency_proxy_image_ttl_policy_response) { graphql_data.dig('group', 'dependencyProxyImageTtlPolicy') }
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ subject { post_graphql(query, current_user: user, variables: variables) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+ end
+
+ context 'with different permissions' do
+ where(:group_visibility, :role, :access_granted) do
+ :private | :maintainer | true
+ :private | :developer | true
+ :private | :reporter | true
+ :private | :guest | true
+ :private | :anonymous | false
+ :public | :maintainer | true
+ :public | :developer | true
+ :public | :reporter | true
+ :public | :guest | true
+ :public | :anonymous | false
+ end
+
+ with_them do
+ before do
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
+ group.add_user(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(dependency_proxy_image_ttl_policy_response).to eq("createdAt" => nil, "enabled" => false, "ttl" => 90, "updatedAt" => nil)
+ else
+ expect(dependency_proxy_image_ttl_policy_response).to be_blank
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb b/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb
new file mode 100644
index 00000000000..c9e9a22ee0b
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Updating the dependency proxy image ttl policy' do
+ include GraphqlHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+
+ let(:params) do
+ {
+ group_path: group.full_path,
+ enabled: false,
+ ttl: 2
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:update_dependency_proxy_image_ttl_group_policy, params) do
+ <<~QL
+ dependencyProxyImageTtlPolicy {
+ enabled
+ ttl
+ }
+ errors
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:update_dependency_proxy_image_ttl_group_policy) }
+ let(:ttl_policy_response) { mutation_response['dependencyProxyImageTtlPolicy'] }
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ describe 'post graphql mutation' do
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ let_it_be(:ttl_policy, reload: true) { create(:image_ttl_group_policy) }
+ let_it_be(:group, reload: true) { ttl_policy.group }
+
+ context 'without permission' do
+ it 'returns no response' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response).to be_nil
+ end
+ end
+
+ context 'with permission' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns the updated dependency proxy image ttl policy', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(ttl_policy_response).to include(
+ 'enabled' => params[:enabled],
+ 'ttl' => params[:ttl]
+ )
+ end
+ end
+ end
+end
diff --git a/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb b/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb
new file mode 100644
index 00000000000..ceac8985c8e
--- /dev/null
+++ b/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::DependencyProxy::ImageTtlGroupPolicies::UpdateService do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:params) { {} }
+
+ describe '#execute' do
+ subject { described_class.new(container: group, current_user: user, params: params).execute }
+
+ shared_examples 'returning a success' do
+ it 'returns a success' do
+ result = subject
+
+ expect(result.payload[:dependency_proxy_image_ttl_policy]).to be_present
+ expect(result).to be_success
+ end
+ end
+
+ shared_examples 'returning an error' do |message, http_status|
+ it 'returns an error' do
+ result = subject
+
+ expect(result).to have_attributes(
+ message: message,
+ status: :error,
+ http_status: http_status
+ )
+ end
+ end
+
+ shared_examples 'updating the dependency proxy image ttl policy' do
+ it_behaves_like 'updating the dependency proxy image ttl policy attributes',
+ from: { enabled: true, ttl: 90 },
+ to: { enabled: false, ttl: 2 }
+
+ it_behaves_like 'returning a success'
+
+ context 'with invalid params' do
+ let_it_be(:params) { { enabled: nil } }
+
+ it_behaves_like 'not creating the dependency proxy image ttl policy'
+
+ it "doesn't update" do
+ expect { subject }
+ .not_to change { ttl_policy.reload.enabled }
+ end
+
+ it_behaves_like 'returning an error', 'Enabled is not included in the list', 400
+ end
+ end
+
+ shared_examples 'denying access to dependency proxy image ttl policy' do
+ context 'with existing dependency proxy image ttl policy' do
+ it_behaves_like 'not creating the dependency proxy image ttl policy'
+
+ it_behaves_like 'returning an error', 'Access Denied', 403
+ end
+ end
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ context 'with existing dependency proxy image ttl policy' do
+ let_it_be(:ttl_policy) { create(:image_ttl_group_policy, group: group) }
+ let_it_be(:params) { { enabled: false, ttl: 2 } }
+
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'updating the dependency proxy image ttl policy'
+ :developer | 'updating the dependency proxy image ttl policy'
+ :reporter | 'denying access to dependency proxy image ttl policy'
+ :guest | 'denying access to dependency proxy image ttl policy'
+ :anonymous | 'denying access to dependency proxy image ttl policy'
+ end
+
+ with_them do
+ before do
+ group.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'without existing dependency proxy image ttl policy' do
+ let_it_be(:ttl_policy) { group.dependency_proxy_image_ttl_policy }
+
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'creating the dependency proxy image ttl policy'
+ :developer | 'creating the dependency proxy image ttl policy'
+ :reporter | 'denying access to dependency proxy image ttl policy'
+ :guest | 'denying access to dependency proxy image ttl policy'
+ :anonymous | 'denying access to dependency proxy image ttl policy'
+ end
+
+ with_them do
+ before do
+ group.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+
+ context 'when the policy is not found' do
+ before do
+ group.add_developer(user)
+ expect(group).to receive(:dependency_proxy_image_ttl_policy).and_return nil
+ end
+
+ it_behaves_like 'returning an error', 'Dependency proxy image TTL Policy not found', 404
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb b/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb
new file mode 100644
index 00000000000..f6692646ca8
--- /dev/null
+++ b/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'updating the dependency proxy image ttl policy attributes' do |from: {}, to:|
+ it_behaves_like 'not creating the dependency proxy image ttl policy'
+
+ it 'updates the dependency proxy image ttl policy' do
+ expect { subject }
+ .to change { group.dependency_proxy_image_ttl_policy.reload.enabled }.from(from[:enabled]).to(to[:enabled])
+ .and change { group.dependency_proxy_image_ttl_policy.reload.ttl }.from(from[:ttl]).to(to[:ttl])
+ end
+end
+
+RSpec.shared_examples 'not creating the dependency proxy image ttl policy' do
+ it "doesn't create the dependency proxy image ttl policy" do
+ expect { subject }.not_to change { DependencyProxy::ImageTtlGroupPolicy.count }
+ end
+end
+
+RSpec.shared_examples 'creating the dependency proxy image ttl policy' do
+ it 'creates a new package setting' do
+ expect { subject }.to change { DependencyProxy::ImageTtlGroupPolicy.count }.by(1)
+ end
+
+ it 'saves the settings' do
+ subject
+
+ expect(group.dependency_proxy_image_ttl_policy).to have_attributes(
+ enabled: ttl_policy[:enabled],
+ ttl: ttl_policy[:ttl]
+ )
+ end
+
+ it_behaves_like 'returning a success'
+end