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>2023-04-26 09:09:25 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-26 09:09:25 +0300
commit8759459c84757589002830279dfe3872ffc852bd (patch)
treec7eb8b77a10db86f7cead8301d21650e628021d8
parent77da08b6e8159daae9b352082bad0c55a003994f (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/layout/space_inside_parens.yml1
-rw-r--r--app/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver.rb19
-rw-r--r--app/graphql/types/clusters/agents/authorizations/ci_access_type.rb21
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/models/clusters/agent.rb57
-rw-r--r--app/models/environment_status.rb5
-rw-r--r--app/models/project.rb1
-rw-r--r--app/policies/clusters/agent_policy.rb10
-rw-r--r--config/feature_flags/development/expose_authorized_cluster_agents.yml8
-rw-r--r--config/feature_flags/development/review_apps_redeploy_mr_widget.yml8
-rw-r--r--doc/api/graphql/reference/index.md33
-rw-r--r--doc/development/rake_tasks.md52
-rw-r--r--lib/gitlab/seeders/ci/variables_group_seeder.rb53
-rw-r--r--lib/gitlab/seeders/ci/variables_instance_seeder.rb43
-rw-r--r--lib/gitlab/seeders/ci/variables_project_seeder.rb52
-rw-r--r--lib/gitlab/seeders/project_environment_seeder.rb44
-rw-r--r--lib/tasks/gitlab/seed/ci_variables_group.rake28
-rw-r--r--lib/tasks/gitlab/seed/ci_variables_instance.rake23
-rw-r--r--lib/tasks/gitlab/seed/ci_variables_project.rake28
-rw-r--r--lib/tasks/gitlab/seed/project_environments.rake24
-rw-r--r--spec/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver_spec.rb28
-rw-r--r--spec/graphql/types/clusters/agents/authorizations/ci_access_type_spec.rb11
-rw-r--r--spec/graphql/types/project_type_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/seeders/ci/variables_group_seeder_spec.rb92
-rw-r--r--spec/lib/gitlab/seeders/ci/variables_instance_seeder_spec.rb54
-rw-r--r--spec/lib/gitlab/seeders/ci/variables_project_seeder_spec.rb92
-rw-r--r--spec/lib/gitlab/seeders/project_environment_seeder_spec.rb52
-rw-r--r--spec/models/clusters/agent_spec.rb73
-rw-r--r--spec/models/environment_status_spec.rb12
-rw-r--r--spec/policies/clusters/agent_policy_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb122
-rw-r--r--spec/support/rspec_order_todo.yml1
35 files changed, 1059 insertions, 7 deletions
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index bb17d0371dc..33f6e917932 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -1474,7 +1474,6 @@ Layout/ArgumentAlignment:
- 'ee/spec/services/ee/vulnerability_feedback_module/update_service_spec.rb'
- 'ee/spec/services/elastic/process_bookkeeping_service_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
- - 'ee/spec/services/epics/tree_reorder_service_spec.rb'
- 'ee/spec/services/geo/blob_upload_service_spec.rb'
- 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- 'ee/spec/services/geo/hashed_storage_attachments_migration_service_spec.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index a2cbf2b5b2d..d3181bbeef2 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -2251,7 +2251,6 @@ Layout/LineLength:
- 'ee/spec/services/epic_issues/create_service_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
- 'ee/spec/services/epics/related_epic_links/create_service_spec.rb'
- - 'ee/spec/services/epics/tree_reorder_service_spec.rb'
- 'ee/spec/services/epics/update_dates_service_spec.rb'
- 'ee/spec/services/epics/update_service_spec.rb'
- 'ee/spec/services/external_status_checks/update_service_spec.rb'
diff --git a/.rubocop_todo/layout/space_inside_parens.yml b/.rubocop_todo/layout/space_inside_parens.yml
index 53e00f78f29..ebe25607970 100644
--- a/.rubocop_todo/layout/space_inside_parens.yml
+++ b/.rubocop_todo/layout/space_inside_parens.yml
@@ -67,7 +67,6 @@ Layout/SpaceInsideParens:
- 'ee/spec/services/ee/users/update_service_spec.rb'
- 'ee/spec/services/epic_issues/update_service_spec.rb'
- 'ee/spec/services/epics/related_epic_links/destroy_service_spec.rb'
- - 'ee/spec/services/epics/tree_reorder_service_spec.rb'
- 'ee/spec/services/geo/container_repository_sync_spec.rb'
- 'ee/spec/services/geo/replication_toggle_request_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
diff --git a/app/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver.rb b/app/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver.rb
new file mode 100644
index 00000000000..c36338439e6
--- /dev/null
+++ b/app/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Clusters
+ module Agents
+ module Authorizations
+ class CiAccessResolver < BaseResolver
+ type Types::Clusters::Agents::Authorizations::CiAccessType, null: true
+
+ alias_method :project, :object
+
+ def resolve(*)
+ ::Clusters::Agents::Authorizations::CiAccess::Finder.new(project).execute
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/clusters/agents/authorizations/ci_access_type.rb b/app/graphql/types/clusters/agents/authorizations/ci_access_type.rb
new file mode 100644
index 00000000000..a60f32b8b0b
--- /dev/null
+++ b/app/graphql/types/clusters/agents/authorizations/ci_access_type.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ module Clusters
+ module Agents
+ module Authorizations
+ class CiAccessType < BaseObject # rubocop:disable Graphql/AuthorizeTypes
+ graphql_name 'ClusterAgentAuthorizationCiAccess'
+
+ field :agent, Types::Clusters::AgentType,
+ description: 'Authorized cluster agent.',
+ null: true
+
+ field :config, GraphQL::Types::JSON, # rubocop:disable Graphql/JSONType
+ description: 'Configuration for the authorized project.',
+ null: true
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 5ebc1cf7ddd..a0f9864351c 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -522,6 +522,12 @@ module Types
description: 'Cluster agents associated with the project.',
resolver: ::Resolvers::Clusters::AgentsResolver
+ field :ci_access_authorized_agents, ::Types::Clusters::Agents::Authorizations::CiAccessType.connection_type,
+ null: true,
+ description: 'Authorized cluster agents for the project through ci_access keyword.',
+ resolver: ::Resolvers::Clusters::Agents::Authorizations::CiAccessResolver,
+ authorize: :read_cluster_agent
+
field :merge_commit_template, GraphQL::Types::String,
null: true,
description: 'Template used to create merge commit message in merge requests.'
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index 374deabfe33..55fd41d6c0a 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -57,6 +57,63 @@ module Clusters
def to_ability_name
:cluster
end
+
+ def ci_access_authorized_for?(user)
+ return false unless user
+ return false unless ::Feature.enabled?(:expose_authorized_cluster_agents, project)
+
+ ::Project.from_union(
+ all_ci_access_authorized_projects_for(user).limit(1),
+ all_ci_access_authorized_namespaces_for(user).limit(1)
+ ).exists?
+ end
+
+ private
+
+ def all_ci_access_authorized_projects_for(user)
+ ::Project.joins(:ci_access_project_authorizations)
+ .joins(:project_authorizations)
+ .where(agent_project_authorizations: { agent_id: id })
+ .where(project_authorizations: { user_id: user.id, access_level: Gitlab::Access::DEVELOPER.. })
+ end
+
+ def all_ci_access_authorized_namespaces_for(user)
+ ::Project.with(root_namespace_cte.to_arel)
+ .with(all_ci_access_authorized_namespaces_cte.to_arel)
+ .joins('INNER JOIN all_authorized_namespaces ON all_authorized_namespaces.id = projects.namespace_id')
+ .joins(:project_authorizations)
+ .where(project_authorizations: { user_id: user.id, access_level: Gitlab::Access::DEVELOPER.. })
+ end
+
+ def root_namespace_cte
+ Gitlab::SQL::CTE.new(:root_namespace, root_namespace.to_sql)
+ end
+
+ def all_ci_access_authorized_namespaces_cte
+ Gitlab::SQL::CTE.new(:all_authorized_namespaces, all_ci_access_authorized_namespaces.to_sql)
+ end
+
+ def all_ci_access_authorized_namespaces
+ Namespace.select("traversal_ids[array_length(traversal_ids, 1)] AS id")
+ .joins("INNER JOIN root_namespace ON " \
+ "namespaces.traversal_ids @> ARRAY[root_namespace.root_id]")
+ .joins("INNER JOIN agent_group_authorizations ON " \
+ "namespaces.traversal_ids @> ARRAY[agent_group_authorizations.group_id::integer]")
+ .where(agent_group_authorizations: { agent_id: id })
+ end
+
+ def root_namespace
+ Namespace.select("traversal_ids[1] AS root_id")
+ .where("traversal_ids @> ARRAY(?)", project_namespace)
+ .limit(1)
+ end
+
+ def project_namespace
+ ::Project.select('namespace_id')
+ .joins(:cluster_agents)
+ .where(cluster_agents: { id: id })
+ .limit(1)
+ end
end
end
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index d06d0a99948..58c57a60f9d 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -100,11 +100,14 @@ class EnvironmentStatus
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline
- pipeline.environments_in_self_and_project_descendants.includes(:project).available.map do |environment|
+ environments = pipeline.environments_in_self_and_project_descendants.includes(:project)
+ environments = environments.available if Feature.disabled?(:review_apps_redeploy_mr_widget, mr.project)
+ environments.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
end.compact
end
+
private_class_method :build_environments_status
end
diff --git a/app/models/project.rb b/app/models/project.rb
index c1f5a2315ef..a33f0fc11b8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -364,6 +364,7 @@ class Project < ApplicationRecord
has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
has_many :management_clusters, class_name: 'Clusters::Cluster', foreign_key: :management_project_id, inverse_of: :management_project
has_many :cluster_agents, class_name: 'Clusters::Agent'
+ has_many :ci_access_project_authorizations, class_name: 'Clusters::Agents::Authorizations::CiAccess::ProjectAuthorization'
has_many :prometheus_metrics
has_many :prometheus_alerts, inverse_of: :project
diff --git a/app/policies/clusters/agent_policy.rb b/app/policies/clusters/agent_policy.rb
index 25e78c84802..afacf782a76 100644
--- a/app/policies/clusters/agent_policy.rb
+++ b/app/policies/clusters/agent_policy.rb
@@ -5,5 +5,15 @@ module Clusters
alias_method :cluster_agent, :subject
delegate { cluster_agent.project }
+
+ # This condition is more expensive than the same permission check in ProjectPolicy,
+ # so having a higher score.
+ condition(:ci_access_authorized_agent, score: 10) do
+ @subject.ci_access_authorized_for?(@user)
+ end
+
+ rule { ci_access_authorized_agent }.policy do
+ enable :read_cluster_agent
+ end
end
end
diff --git a/config/feature_flags/development/expose_authorized_cluster_agents.yml b/config/feature_flags/development/expose_authorized_cluster_agents.yml
new file mode 100644
index 00000000000..9110c2be6c6
--- /dev/null
+++ b/config/feature_flags/development/expose_authorized_cluster_agents.yml
@@ -0,0 +1,8 @@
+---
+name: expose_authorized_cluster_agents
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117128
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407841
+milestone: '16.0'
+type: development
+group: group::environments
+default_enabled: false
diff --git a/config/feature_flags/development/review_apps_redeploy_mr_widget.yml b/config/feature_flags/development/review_apps_redeploy_mr_widget.yml
new file mode 100644
index 00000000000..c2bbdef3c83
--- /dev/null
+++ b/config/feature_flags/development/review_apps_redeploy_mr_widget.yml
@@ -0,0 +1,8 @@
+---
+name: review_apps_redeploy_mr_widget
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118260
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407456
+milestone: '15.11'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index aa27bdbb64b..c8272c1a90d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7678,6 +7678,29 @@ The edge type for [`ClusterAgentActivityEvent`](#clusteragentactivityevent).
| <a id="clusteragentactivityeventedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="clusteragentactivityeventedgenode"></a>`node` | [`ClusterAgentActivityEvent`](#clusteragentactivityevent) | The item at the end of the edge. |
+#### `ClusterAgentAuthorizationCiAccessConnection`
+
+The connection type for [`ClusterAgentAuthorizationCiAccess`](#clusteragentauthorizationciaccess).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="clusteragentauthorizationciaccessconnectionedges"></a>`edges` | [`[ClusterAgentAuthorizationCiAccessEdge]`](#clusteragentauthorizationciaccessedge) | A list of edges. |
+| <a id="clusteragentauthorizationciaccessconnectionnodes"></a>`nodes` | [`[ClusterAgentAuthorizationCiAccess]`](#clusteragentauthorizationciaccess) | A list of nodes. |
+| <a id="clusteragentauthorizationciaccessconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `ClusterAgentAuthorizationCiAccessEdge`
+
+The edge type for [`ClusterAgentAuthorizationCiAccess`](#clusteragentauthorizationciaccess).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="clusteragentauthorizationciaccessedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="clusteragentauthorizationciaccessedgenode"></a>`node` | [`ClusterAgentAuthorizationCiAccess`](#clusteragentauthorizationciaccess) | The item at the end of the edge. |
+
#### `ClusterAgentConnection`
The connection type for [`ClusterAgent`](#clusteragent).
@@ -12375,6 +12398,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="clusteragentactivityeventrecordedat"></a>`recordedAt` | [`Time`](#time) | Timestamp the event was recorded. |
| <a id="clusteragentactivityeventuser"></a>`user` | [`UserCore`](#usercore) | User associated with the event. |
+### `ClusterAgentAuthorizationCiAccess`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="clusteragentauthorizationciaccessagent"></a>`agent` | [`ClusterAgent`](#clusteragent) | Authorized cluster agent. |
+| <a id="clusteragentauthorizationciaccessconfig"></a>`config` | [`JSON`](#json) | Configuration for the authorized project. |
+
### `ClusterAgentToken`
#### Fields
@@ -18586,6 +18618,7 @@ Represents a product analytics dashboard visualization.
| <a id="projectautoclosereferencedissues"></a>`autocloseReferencedIssues` | [`Boolean`](#boolean) | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically. |
| <a id="projectavatarurl"></a>`avatarUrl` | [`String`](#string) | URL to avatar image file of the project. |
| <a id="projectbranchrules"></a>`branchRules` | [`BranchRuleConnection`](#branchruleconnection) | Branch rules configured for the project. (see [Connections](#connections)) |
+| <a id="projectciaccessauthorizedagents"></a>`ciAccessAuthorizedAgents` | [`ClusterAgentAuthorizationCiAccessConnection`](#clusteragentauthorizationciaccessconnection) | Authorized cluster agents for the project through ci_access keyword. (see [Connections](#connections)) |
| <a id="projectcicdsettings"></a>`ciCdSettings` | [`ProjectCiCdSetting`](#projectcicdsetting) | CI/CD settings for the project. |
| <a id="projectciconfigpathordefault"></a>`ciConfigPathOrDefault` | [`String!`](#string) | Path of the CI configuration file. |
| <a id="projectcijobtokenscope"></a>`ciJobTokenScope` | [`CiJobTokenScopeType`](#cijobtokenscopetype) | The CI Job Tokens scope of access. |
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 82e96befd11..a5345527203 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -145,6 +145,58 @@ bin/rake 'gitlab:seed:vulnerabilities'
bin/rake 'gitlab:seed:vulnerabilities[group-path/project-path]'
```
+#### Seed a project with environments
+
+You can seed a project with [environments](../ci/environments/index.md).
+
+By default, this creates 10 environments, each with the prefix `ENV_`.
+Only `project_path` is required to run this command.
+
+```shell
+bundle exec rake "gitlab:seed:project_environments[project_path, seed_count, prefix]"
+
+# Examples
+bundle exec rake "gitlab:seed:project_environments[flightjs/Flight]"
+bundle exec rake "gitlab:seed:project_environments[flightjs/Flight, 25, FLIGHT_ENV_]"
+```
+
+#### Seed CI variables
+
+You can seed a project, group, or instance with [CI variables](../ci/variables/index.md).
+
+By default, each command creates 10 CI variables. Variable names are prepended with its own
+default prefix (`VAR_` for project-level variables, `GROUP_VAR_` for group-level variables,
+and `INSTANCE_VAR_` for instance-level variables).
+
+Instance-level variables do not have environment scopes. Project-level and group-level variables
+use the default `"*"` environment scope if no `environment_scope` is supplied. If `environment_scope`
+is set to `"unique"`, each variable is created with its own unique environment.
+
+```shell
+# Seed a project with project-level CI variables
+# Only `project_path` is required to run this command.
+bundle exec rake "gitlab:seed:ci_variables_project[project_path, seed_count, environment_scope, prefix]"
+
+# Seed a group with group-level CI variables
+# Only `group_name` is required to run this command.
+bundle exec rake "gitlab:seed:ci_variables_group[group_name, seed_count, environment_scope, prefix]"
+
+# Seed an instance with instance-level CI variables
+bundle exec rake "gitlab:seed:ci_variables_instance[seed_count, prefix]"
+
+# Examples
+bundle exec rake "gitlab:seed:ci_variables_project[flightjs/Flight]"
+bundle exec rake "gitlab:seed:ci_variables_project[flightjs/Flight, 25, staging]"
+bundle exec rake "gitlab:seed:ci_variables_project[flightjs/Flight, 25, unique, CI_VAR_]"
+
+bundle exec rake "gitlab:seed:ci_variables_group[group_name]"
+bundle exec rake "gitlab:seed:ci_variables_group[group_name, 25, staging]"
+bundle exec rake "gitlab:seed:ci_variables_group[group_name, 25, unique, CI_VAR_]"
+
+bundle exec rake "gitlab:seed:ci_variables_instance"
+bundle exec rake "gitlab:seed:ci_variables_instance[25, CI_VAR_]"
+```
+
### Automation
If you're very sure that you want to **wipe the current database** and refill
diff --git a/lib/gitlab/seeders/ci/variables_group_seeder.rb b/lib/gitlab/seeders/ci/variables_group_seeder.rb
new file mode 100644
index 00000000000..e5c5ee4b75f
--- /dev/null
+++ b/lib/gitlab/seeders/ci/variables_group_seeder.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ module Ci
+ class VariablesGroupSeeder
+ DEFAULT_SEED_COUNT = 10
+ DEFAULT_PREFIX = 'GROUP_VAR_'
+ DEFAULT_ENV = '*'
+
+ def initialize(params)
+ @group = Group.find_by_name(params[:name])
+ @seed_count = params[:seed_count] || DEFAULT_SEED_COUNT
+ @environment_scope = params[:environment_scope] || DEFAULT_ENV
+ @prefix = params[:prefix] || DEFAULT_PREFIX
+ end
+
+ def seed
+ if @group.nil?
+ warn 'ERROR: Group name is invalid.'
+ return
+ end
+
+ max_id = group.variables.maximum(:id).to_i
+ seed_count.times do
+ max_id += 1
+ create_ci_variable(max_id)
+ end
+ end
+
+ private
+
+ attr_reader :environment_scope, :group, :prefix, :seed_count
+
+ def create_ci_variable(id)
+ env = environment_scope == 'unique' ? "env_#{id}" : environment_scope
+ key = "#{prefix}#{id}"
+
+ if group.variables.by_environment_scope(env).find_by_key(key).present?
+ warn "WARNING: Group CI Variable with key '#{key}' already exists. Skipping to next CI variable..."
+ return
+ end
+
+ group.variables.create(
+ environment_scope: env,
+ key: key,
+ value: SecureRandom.hex(32)
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/seeders/ci/variables_instance_seeder.rb b/lib/gitlab/seeders/ci/variables_instance_seeder.rb
new file mode 100644
index 00000000000..d43defd192f
--- /dev/null
+++ b/lib/gitlab/seeders/ci/variables_instance_seeder.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ module Ci
+ class VariablesInstanceSeeder
+ DEFAULT_SEED_COUNT = 10
+ DEFAULT_PREFIX = 'INSTANCE_VAR_'
+
+ def initialize(params = {})
+ @seed_count = params[:seed_count] || DEFAULT_SEED_COUNT
+ @prefix = params[:prefix] || DEFAULT_PREFIX
+ end
+
+ def seed
+ max_id = ::Ci::InstanceVariable.maximum(:id).to_i
+ seed_count.times do
+ max_id += 1
+ create_ci_variable(max_id)
+ end
+ end
+
+ private
+
+ attr_reader :prefix, :seed_count
+
+ def create_ci_variable(id)
+ key = "#{prefix}#{id}"
+
+ if ::Ci::InstanceVariable.find_by_key(key)
+ warn "WARNING: Instance CI Variable with key '#{key}' already exists. Skipping to next CI variable..."
+ return
+ end
+
+ ::Ci::InstanceVariable.new(
+ key: key,
+ value: SecureRandom.hex(32)
+ ).save
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/seeders/ci/variables_project_seeder.rb b/lib/gitlab/seeders/ci/variables_project_seeder.rb
new file mode 100644
index 00000000000..c6b3dac7a4d
--- /dev/null
+++ b/lib/gitlab/seeders/ci/variables_project_seeder.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ module Ci
+ class VariablesProjectSeeder
+ DEFAULT_SEED_COUNT = 10
+ DEFAULT_PREFIX = 'VAR_'
+ DEFAULT_ENV = '*'
+
+ def initialize(params)
+ @project = Project.find_by_full_path(params[:project_path])
+ @seed_count = params[:seed_count] || DEFAULT_SEED_COUNT
+ @environment_scope = params[:environment_scope] || DEFAULT_ENV
+ @prefix = params[:prefix] || DEFAULT_PREFIX
+ end
+
+ def seed
+ if @project.nil?
+ warn 'ERROR: Project path is invalid.'
+ return
+ end
+
+ max_id = project.variables.maximum(:id).to_i
+ seed_count.times do
+ max_id += 1
+ create_ci_variable(max_id)
+ end
+ end
+
+ private
+
+ attr_reader :environment_scope, :prefix, :project, :seed_count
+
+ def create_ci_variable(id)
+ env = environment_scope == 'unique' ? "env_#{id}" : environment_scope
+ key = "#{prefix}#{id}"
+
+ if project.variables.by_environment_scope(env).find_by_key(key).present?
+ warn "WARNING: Project CI Variable with key '#{key}' already exists. Skipping to next CI variable..."
+ end
+
+ project.variables.create(
+ environment_scope: env,
+ key: key,
+ value: SecureRandom.hex(32)
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/seeders/project_environment_seeder.rb b/lib/gitlab/seeders/project_environment_seeder.rb
new file mode 100644
index 00000000000..3fc7d3d9b12
--- /dev/null
+++ b/lib/gitlab/seeders/project_environment_seeder.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ class ProjectEnvironmentSeeder
+ DEFAULT_SEED_COUNT = 10
+ DEFAULT_PREFIX = 'ENV_'
+
+ def initialize(params)
+ @project = Project.find_by_full_path(params[:project_path])
+ @seed_count = params[:seed_count] || DEFAULT_SEED_COUNT
+ @prefix = params[:prefix] || DEFAULT_PREFIX
+ end
+
+ def seed
+ if @project.nil?
+ warn 'ERROR: Project path is invalid.'
+ return
+ end
+
+ max_id = project.environments.maximum(:id).to_i
+ seed_count.times do
+ max_id += 1
+ create_project_environment_scope(max_id)
+ end
+ end
+
+ private
+
+ attr_reader :project, :seed_count, :prefix
+
+ def create_project_environment_scope(id)
+ name = "#{prefix}#{id}"
+
+ if project.environments.find_by_name(name).present?
+ warn "WARNING: Project Environment '#{name}' already exists. Skipping to next CI variable..."
+ return
+ end
+
+ project.environments.create(name: name)
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/seed/ci_variables_group.rake b/lib/tasks/gitlab/seed/ci_variables_group.rake
new file mode 100644
index 00000000000..d58923eab0e
--- /dev/null
+++ b/lib/tasks/gitlab/seed/ci_variables_group.rake
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# Seed group with CI variables
+#
+# @param name - name of the group to add CI variables to
+# @param seed_count - total number of CI variables to create (default: 10)
+# @param environment_scope - environment scope of the variable (default: '*')
+# If "unique", it will create a unique environment_scope per variable.
+# @param prefix - prefix of the variable key (default: 'GROUP_VAR_')
+#
+# @example
+# bundle exec rake "gitlab:seed:ci_variables_group[kitchen-sink, 5, unique]"
+#
+namespace :gitlab do
+ namespace :seed do
+ desc 'Seed group with CI Variables'
+ task :ci_variables_group,
+ [:name, :seed_count, :environment_scope, :prefix] => :gitlab_environment do |_t, args|
+ Gitlab::Seeders::Ci::VariablesGroupSeeder.new(
+ name: args.name,
+ seed_count: args.seed_count&.to_i,
+ prefix: args&.prefix,
+ environment_scope: args&.environment_scope
+ ).seed
+ puts "Task finished!"
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/seed/ci_variables_instance.rake b/lib/tasks/gitlab/seed/ci_variables_instance.rake
new file mode 100644
index 00000000000..15a101a1cf1
--- /dev/null
+++ b/lib/tasks/gitlab/seed/ci_variables_instance.rake
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Seed instance with CI variables
+#
+# @param seed_count - total number of CI variables to create (default: 10)
+# @param prefix - prefix of the variable key (default: 'INSTANCE_VAR_')
+#
+# @example
+# bundle exec rake "gitlab:seed:ci_variables_instance[5, INSTANCE_TEST_]"
+#
+namespace :gitlab do
+ namespace :seed do
+ desc 'Seed instance with CI Variables'
+ task :ci_variables_instance,
+ [:seed_count, :prefix] => :gitlab_environment do |_t, args|
+ Gitlab::Seeders::Ci::VariablesInstanceSeeder.new(
+ seed_count: args.seed_count&.to_i,
+ prefix: args&.prefix
+ ).seed
+ puts "Task finished!"
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/seed/ci_variables_project.rake b/lib/tasks/gitlab/seed/ci_variables_project.rake
new file mode 100644
index 00000000000..64f8b8d2a86
--- /dev/null
+++ b/lib/tasks/gitlab/seed/ci_variables_project.rake
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# Seed project with CI variables
+#
+# @param project_path - path of the project to add CI variables to
+# @param seed_count - total number of CI variables to create (default: 10)
+# @param environment_scope - environment scope of the variable (default: '*')
+# If "unique", it will create a unique environment_scope per variable.
+# @param prefix - prefix of the variable key (default: 'VAR_')
+#
+# @example
+# bundle exec rake "gitlab:seed:ci_variables_project[root/paper, 5, production, prod_var_]"
+#
+namespace :gitlab do
+ namespace :seed do
+ desc 'Seed project with CI Variables'
+ task :ci_variables_project,
+ [:project_path, :seed_count, :environment_scope, :prefix] => :gitlab_environment do |_t, args|
+ Gitlab::Seeders::Ci::VariablesProjectSeeder.new(
+ project_path: args.project_path,
+ seed_count: args.seed_count&.to_i,
+ prefix: args&.prefix,
+ environment_scope: args&.environment_scope
+ ).seed
+ puts "Task finished!"
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/seed/project_environments.rake b/lib/tasks/gitlab/seed/project_environments.rake
new file mode 100644
index 00000000000..8557c130297
--- /dev/null
+++ b/lib/tasks/gitlab/seed/project_environments.rake
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Seed project with environments
+#
+# @param project_path - path of the project to add environments to
+# @param seed_count - total number of environments to create (default: 10)
+# @param prefix - prefix used for the environment name (default: 'ENV_')
+#
+# @example
+# bundle exec rake "gitlab:seed:project_environments[root/paper, 5, staging_]"
+#
+namespace :gitlab do
+ namespace :seed do
+ desc 'Seed project with environments'
+ task :project_environments, [:project_path, :seed_count, :prefix] => :gitlab_environment do |_t, args|
+ Gitlab::Seeders::ProjectEnvironmentSeeder.new(
+ project_path: args.project_path,
+ seed_count: args.seed_count&.to_i,
+ prefix: args&.prefix
+ ).seed
+ puts "Task finished!"
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver_spec.rb b/spec/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver_spec.rb
new file mode 100644
index 00000000000..b5280365794
--- /dev/null
+++ b/spec/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Clusters::Agents::Authorizations::CiAccessResolver,
+ feature_category: :deployment_management do
+ include GraphqlHelpers
+
+ it { expect(described_class.type).to eq(Types::Clusters::Agents::Authorizations::CiAccessType) }
+ it { expect(described_class.null).to be_truthy }
+
+ describe '#resolve' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:ctx) { { current_user: user } }
+
+ subject { resolve(described_class, obj: project, ctx: ctx) }
+
+ it 'calls the finder' do
+ expect_next_instance_of(::Clusters::Agents::Authorizations::CiAccess::Finder, project) do |finder|
+ expect(finder).to receive(:execute)
+ end
+
+ subject
+ end
+ end
+end
diff --git a/spec/graphql/types/clusters/agents/authorizations/ci_access_type_spec.rb b/spec/graphql/types/clusters/agents/authorizations/ci_access_type_spec.rb
new file mode 100644
index 00000000000..17725ec11e0
--- /dev/null
+++ b/spec/graphql/types/clusters/agents/authorizations/ci_access_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ClusterAgentAuthorizationCiAccess'],
+ feature_category: :deployment_management do
+ let(:fields) { %i[agent config] }
+
+ it { expect(described_class.graphql_name).to eq('ClusterAgentAuthorizationCiAccess') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 2e42cd2daad..4cae970de64 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe GitlabSchema.types['Project'] do
issue_status_counts terraform_states alert_management_integrations
container_repositories container_repositories_count
pipeline_analytics squash_read_only sast_ci_configuration
- cluster_agent cluster_agents agent_configurations
+ cluster_agent cluster_agents agent_configurations ci_access_authorized_agents
ci_template timelogs merge_commit_template squash_commit_template work_item_types
recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables
timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 66b57deb643..658e20608b1 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -508,6 +508,7 @@ project:
- cluster
- clusters
- cluster_agents
+- ci_access_project_authorizations
- cluster_project
- creator
- cycle_analytics_stages
diff --git a/spec/lib/gitlab/seeders/ci/variables_group_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/variables_group_seeder_spec.rb
new file mode 100644
index 00000000000..52898cb17a5
--- /dev/null
+++ b/spec/lib/gitlab/seeders/ci/variables_group_seeder_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Seeders::Ci::VariablesGroupSeeder, feature_category: :secrets_management do
+ let_it_be(:group) { create(:group) }
+
+ let(:seeder) { described_class.new(name: group.name) }
+
+ let(:custom_seeder) do
+ described_class.new(
+ name: group.name,
+ seed_count: 2,
+ environment_scope: 'staging',
+ prefix: 'STAGING_'
+ )
+ end
+
+ let(:unique_env_seeder) do
+ described_class.new(
+ name: group.name,
+ seed_count: 2,
+ environment_scope: 'unique'
+ )
+ end
+
+ let(:invalid_group_name_seeder) do
+ described_class.new(
+ name: 'nonexistent_group',
+ seed_count: 1
+ )
+ end
+
+ describe '#seed' do
+ it 'creates group-level CI variables with default values' do
+ expect { seeder.seed }.to change {
+ group.variables.count
+ }.by(Gitlab::Seeders::Ci::VariablesGroupSeeder::DEFAULT_SEED_COUNT)
+
+ ci_variable = group.reload.variables.last
+
+ expect(ci_variable.key.include?('GROUP_VAR_')).to eq true
+ expect(ci_variable.environment_scope).to eq '*'
+ end
+
+ it 'creates group-level CI variables with custom arguments' do
+ expect { custom_seeder.seed }.to change {
+ group.variables.count
+ }.by(2)
+
+ ci_variable = group.reload.variables.last
+
+ expect(ci_variable.key.include?('STAGING_')).to eq true
+ expect(ci_variable.environment_scope).to eq 'staging'
+ end
+
+ it 'creates group-level CI variables with unique environment scopes' do
+ unique_env_seeder.seed
+
+ ci_variable_first_env = group.reload.variables.first.environment_scope
+ ci_variable_last_env = group.reload.variables.last.environment_scope
+
+ expect(ci_variable_first_env).not_to eq ci_variable_last_env
+ end
+
+ it 'skips seeding when group name is invalid' do
+ expect { invalid_group_name_seeder.seed }.to change {
+ group.variables.count
+ }.by(0)
+ end
+
+ it 'skips CI variable creation if CI variable already exists' do
+ group.variables.create!(
+ environment_scope: '*',
+ key: "GROUP_VAR_#{group.variables.maximum(:id).to_i}",
+ value: SecureRandom.hex(32)
+ )
+
+ # first id is assigned randomly, so we're creating a new variable
+ # based on that id that is sure to be skipped during seed
+ group.variables.create!(
+ environment_scope: '*',
+ key: "GROUP_VAR_#{group.variables.maximum(:id).to_i + 2}",
+ value: SecureRandom.hex(32)
+ )
+
+ expect { seeder.seed }.to change {
+ group.variables.count
+ }.by(Gitlab::Seeders::Ci::VariablesGroupSeeder::DEFAULT_SEED_COUNT - 1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/seeders/ci/variables_instance_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/variables_instance_seeder_spec.rb
new file mode 100644
index 00000000000..5b6d2471edd
--- /dev/null
+++ b/spec/lib/gitlab/seeders/ci/variables_instance_seeder_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Seeders::Ci::VariablesInstanceSeeder, feature_category: :secrets_management do
+ let(:seeder) { described_class.new }
+
+ let(:custom_seeder) do
+ described_class.new(
+ seed_count: 2,
+ prefix: 'STAGING_'
+ )
+ end
+
+ describe '#seed' do
+ it 'creates instance-level CI variables with default values' do
+ expect { seeder.seed }.to change {
+ Ci::InstanceVariable.all.count
+ }.by(Gitlab::Seeders::Ci::VariablesInstanceSeeder::DEFAULT_SEED_COUNT)
+
+ ci_variable = Ci::InstanceVariable.last
+
+ expect(ci_variable.key.include?('INSTANCE_VAR_')).to eq true
+ end
+
+ it 'creates instance-level CI variables with custom arguments' do
+ expect { custom_seeder.seed }.to change {
+ Ci::InstanceVariable.all.count
+ }.by(2)
+
+ ci_variable = Ci::InstanceVariable.last
+
+ expect(ci_variable.key.include?('STAGING_')).to eq true
+ end
+
+ it 'skips CI variable creation if CI variable already exists' do
+ ::Ci::InstanceVariable.new(
+ key: "INSTANCE_VAR_#{::Ci::InstanceVariable.maximum(:id).to_i}",
+ value: SecureRandom.hex(32)
+ ).save!
+
+ # first id is assigned randomly, so we're creating a new variable
+ # based on that id that is sure to be skipped during seed
+ ::Ci::InstanceVariable.new(
+ key: "INSTANCE_VAR_#{::Ci::InstanceVariable.maximum(:id).to_i + 2}",
+ value: SecureRandom.hex(32)
+ ).save!
+
+ expect { seeder.seed }.to change {
+ Ci::InstanceVariable.all.count
+ }.by(Gitlab::Seeders::Ci::VariablesInstanceSeeder::DEFAULT_SEED_COUNT - 1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/seeders/ci/variables_project_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/variables_project_seeder_spec.rb
new file mode 100644
index 00000000000..45b6a0a51fd
--- /dev/null
+++ b/spec/lib/gitlab/seeders/ci/variables_project_seeder_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Seeders::Ci::VariablesProjectSeeder, feature_category: :secrets_management do
+ let_it_be(:project) { create(:project) }
+
+ let(:seeder) { described_class.new(project_path: project.full_path) }
+
+ let(:custom_seeder) do
+ described_class.new(
+ project_path: project.full_path,
+ seed_count: 2,
+ environment_scope: 'staging',
+ prefix: 'STAGING_'
+ )
+ end
+
+ let(:unique_env_seeder) do
+ described_class.new(
+ project_path: project.full_path,
+ seed_count: 2,
+ environment_scope: 'unique'
+ )
+ end
+
+ let(:invalid_project_path_seeder) do
+ described_class.new(
+ project_path: 'invalid_path',
+ seed_count: 1
+ )
+ end
+
+ describe '#seed' do
+ it 'creates project-level CI variables with default values' do
+ expect { seeder.seed }.to change {
+ project.variables.count
+ }.by(Gitlab::Seeders::Ci::VariablesProjectSeeder::DEFAULT_SEED_COUNT)
+
+ ci_variable = project.reload.variables.last
+
+ expect(ci_variable.key.include?('VAR_')).to eq true
+ expect(ci_variable.environment_scope).to eq '*'
+ end
+
+ it 'creates project-level CI variables with custom arguments' do
+ expect { custom_seeder.seed }.to change {
+ project.variables.count
+ }.by(2)
+
+ ci_variable = project.reload.variables.last
+
+ expect(ci_variable.key.include?('STAGING_')).to eq true
+ expect(ci_variable.environment_scope).to eq 'staging'
+ end
+
+ it 'creates project-level CI variables with unique environment scopes' do
+ unique_env_seeder.seed
+
+ ci_variable_first_env = project.reload.variables.first.environment_scope
+ ci_variable_last_env = project.reload.variables.last.environment_scope
+
+ expect(ci_variable_first_env).not_to eq ci_variable_last_env
+ end
+
+ it 'skips seeding when project path is invalid' do
+ expect { invalid_project_path_seeder.seed }.to change {
+ project.variables.count
+ }.by(0)
+ end
+
+ it 'skips CI variable creation if CI variable already exists' do
+ project.variables.create!(
+ environment_scope: '*',
+ key: "VAR_#{project.variables.maximum(:id).to_i}",
+ value: SecureRandom.hex(32)
+ )
+
+ # first id is assigned randomly, so we're creating a new variable
+ # based on that id that is sure to be skipped during seed
+ project.variables.create!(
+ environment_scope: '*',
+ key: "VAR_#{project.variables.maximum(:id).to_i + 2}",
+ value: SecureRandom.hex(32)
+ )
+
+ expect { seeder.seed }.to change {
+ project.variables.count
+ }.by(Gitlab::Seeders::Ci::VariablesProjectSeeder::DEFAULT_SEED_COUNT - 1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/seeders/project_environment_seeder_spec.rb b/spec/lib/gitlab/seeders/project_environment_seeder_spec.rb
new file mode 100644
index 00000000000..8401d189373
--- /dev/null
+++ b/spec/lib/gitlab/seeders/project_environment_seeder_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Seeders::ProjectEnvironmentSeeder, feature_category: :secrets_management do
+ let_it_be(:project) { create(:project) }
+
+ let(:seeder) { described_class.new(project_path: project.full_path) }
+ let(:custom_seeder) do
+ described_class.new(project_path: project.full_path, seed_count: 2, prefix: 'staging_')
+ end
+
+ let(:invalid_project_path_seeder) do
+ described_class.new(project_path: 'invalid_path', seed_count: 1)
+ end
+
+ describe '#seed' do
+ it 'creates environments for the project' do
+ expect { seeder.seed }.to change {
+ project.environments.count
+ }.by(Gitlab::Seeders::ProjectEnvironmentSeeder::DEFAULT_SEED_COUNT)
+ end
+
+ it 'creates environments with custom arguments' do
+ expect { custom_seeder.seed }.to change {
+ project.environments.count
+ }.by(2)
+
+ env = project.environments.last
+
+ expect(env.name.include?('staging_')).to eq true
+ end
+
+ it 'skips seeding when project path is invalid' do
+ expect { invalid_project_path_seeder.seed }.to change {
+ project.environments.count
+ }.by(0)
+ end
+
+ it 'skips environment creation if environment already exists' do
+ project.environments.create!(name: "ENV_#{project.environments.maximum(:id).to_i}")
+
+ # first id is assigned randomly, so we're creating a new variable
+ # based on that id that is sure to be skipped during seed
+ project.environments.create!(name: "ENV_#{project.environments.maximum(:id).to_i + 2}")
+
+ expect { seeder.seed }.to change {
+ project.environments.count
+ }.by(Gitlab::Seeders::ProjectEnvironmentSeeder::DEFAULT_SEED_COUNT - 1)
+ end
+ end
+end
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index df8ad861aff..2c560b992c1 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Agent do
+RSpec.describe Clusters::Agent, feature_category: :deployment_management do
subject { create(:cluster_agent) }
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
@@ -163,4 +163,75 @@ RSpec.describe Clusters::Agent do
it { is_expected.to be_like_time(event2.recorded_at) }
end
+
+ describe '#ci_access_authorized_for?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:agent_management_project) { create(:project, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
+ let_it_be(:deployment_project) { create(:project, group: organization) }
+
+ let(:user) { create(:user) }
+
+ subject { agent.ci_access_authorized_for?(user) }
+
+ it { is_expected.to eq(false) }
+
+ context 'with project-level authorization' do
+ let!(:authorization) { create(:agent_ci_access_project_authorization, agent: agent, project: deployment_project) }
+
+ where(:user_role, :allowed) do
+ :guest | false
+ :reporter | false
+ :developer | true
+ :maintainer | true
+ :owner | true
+ end
+
+ with_them do
+ before do
+ deployment_project.add_member(user, user_role)
+ end
+
+ it { is_expected.to eq(allowed) }
+ end
+
+ context 'when expose_authorized_cluster_agents feature flag is disabled' do
+ before do
+ stub_feature_flags(expose_authorized_cluster_agents: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'with group-level authorization' do
+ let!(:authorization) { create(:agent_ci_access_group_authorization, agent: agent, group: organization) }
+
+ where(:user_role, :allowed) do
+ :guest | false
+ :reporter | false
+ :developer | true
+ :maintainer | true
+ :owner | true
+ end
+
+ with_them do
+ before do
+ organization.add_member(user, user_role)
+ end
+
+ it { is_expected.to eq(allowed) }
+ end
+
+ context 'when expose_authorized_cluster_agents feature flag is disabled' do
+ before do
+ stub_feature_flags(expose_authorized_cluster_agents: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
end
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 2f1edf9ab94..cb14d98da72 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -265,6 +265,7 @@ RSpec.describe EnvironmentStatus do
context 'when environment is stopped' do
before do
+ stub_feature_flags(review_apps_redeploy_mr_widget: false)
environment.stop!
end
@@ -272,6 +273,17 @@ RSpec.describe EnvironmentStatus do
expect(subject.count).to eq(0)
end
end
+
+ context 'when environment is stopped and review_apps_redeploy_mr_widget is turned on' do
+ before do
+ stub_feature_flags(review_apps_redeploy_mr_widget: true)
+ environment.stop!
+ end
+
+ it 'returns environment regardless of status' do
+ expect(subject.count).to eq(1)
+ end
+ end
end
end
end
diff --git a/spec/policies/clusters/agent_policy_spec.rb b/spec/policies/clusters/agent_policy_spec.rb
index 8f778d318ed..200cb8ae99b 100644
--- a/spec/policies/clusters/agent_policy_spec.rb
+++ b/spec/policies/clusters/agent_policy_spec.rb
@@ -24,5 +24,13 @@ RSpec.describe Clusters::AgentPolicy do
it { expect(policy).to be_allowed :admin_cluster }
end
+
+ context 'when agent is ci_access authorized for project members' do
+ before do
+ allow(cluster_agent).to receive(:ci_access_authorized_for?).with(user).and_return(true)
+ end
+
+ it { expect(policy).to be_allowed :read_cluster_agent }
+ end
end
end
diff --git a/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
new file mode 100644
index 00000000000..dd76f6425fe
--- /dev/null
+++ b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project.ci_access_authorized_agents', feature_category: :deployment_management do
+ include GraphqlHelpers
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:agent_management_project) { create(:project, :private, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
+
+ let_it_be(:deployment_project) { create(:project, :private, group: organization) }
+ let_it_be(:deployment_developer) { create(:user).tap { |u| deployment_project.add_developer(u) } }
+ let_it_be(:deployment_reporter) { create(:user).tap { |u| deployment_project.add_reporter(u) } }
+
+ let(:user) { deployment_developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{deployment_project.full_path}") {
+ ciAccessAuthorizedAgents {
+ nodes {
+ agent {
+ id
+ name
+ project {
+ name
+ }
+ }
+ config
+ }
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'with project authorization' do
+ let!(:ci_access) { create(:agent_ci_access_project_authorization, agent: agent, project: deployment_project) }
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({ "default_namespace" => "production" })
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['ciAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'with group authorization' do
+ let!(:ci_access) { create(:agent_ci_access_group_authorization, agent: agent, group: organization) }
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({ "default_namespace" => "production" })
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['ciAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'when deployment project is not authorized to ci_access to the agent' do
+ it 'returns empty' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents).to be_empty
+ end
+ end
+end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index c81c5c41844..3b35b43d25c 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -2713,7 +2713,6 @@
- './ee/spec/services/epics/related_epic_links/list_service_spec.rb'
- './ee/spec/services/epics/reopen_service_spec.rb'
- './ee/spec/services/epics/transfer_service_spec.rb'
-- './ee/spec/services/epics/tree_reorder_service_spec.rb'
- './ee/spec/services/epics/update_dates_service_spec.rb'
- './ee/spec/services/epics/update_service_spec.rb'
- './ee/spec/services/external_status_checks/create_service_spec.rb'