diff --git a/app/graphql/types/ci/variable_type.rb b/app/graphql/types/ci/variable_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..5d2acfb9c9fc63162d8116bb5cbc5c25c9eda2e8 --- /dev/null +++ b/app/graphql/types/ci/variable_type.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class VariableType < BaseObject + graphql_name 'CiVariable' + + field :id, GraphQL::Types::ID, null: false, + description: 'ID of the variable.' + + field :key, GraphQL::Types::String, null: true, + description: 'Name of the variable.' + + field :value, GraphQL::Types::String, null: true, + description: 'Value of the variable.' + + field :variable_type, ::Types::Ci::VariableTypeEnum, null: true, + description: 'Type of the variable.' + + field :protected, GraphQL::Types::Boolean, null: true, + description: 'Indicates whether the variable is protected.' + + field :masked, GraphQL::Types::Boolean, null: true, + description: 'Indicates whether the variable is masked.' + + field :raw, GraphQL::Types::Boolean, null: true, + description: 'Indicates whether the variable is raw.' + end + end +end diff --git a/app/graphql/types/ci/variable_type_enum.rb b/app/graphql/types/ci/variable_type_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..44430754a2efd02fc7872265f198663a584ea3fc --- /dev/null +++ b/app/graphql/types/ci/variable_type_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Ci + class VariableTypeEnum < BaseEnum + graphql_name 'CiVariableType' + + ::Ci::Variable.variable_types.keys.each do |variable_type| + value variable_type.upcase, value: variable_type, description: "#{variable_type.humanize} type." + end + end + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 49971d52a30944e66feb4eb674db64359b7bba8d..52e9f8080666749e3aa540af174aa9f263d3f9c6 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -194,6 +194,13 @@ class GroupType < NamespaceType complexity: 5, resolver: Resolvers::GroupsResolver + field :ci_variables, + Types::Ci::VariableType.connection_type, + null: true, + description: "List of the group's CI/CD variables.", + authorize: :admin_group, + method: :variables + field :runners, Types::Ci::RunnerType.connection_type, null: true, resolver: Resolvers::Ci::GroupRunnersResolver, diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 603d5ead5409814e806d24c316552ea1cac26d10..c2e47e063616e46d598a500463c1b53e074d8786 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -220,6 +220,13 @@ class ProjectType < BaseObject description: 'Build pipeline counts of the project.', resolver: Resolvers::Ci::ProjectPipelineCountsResolver + field :ci_variables, + Types::Ci::VariableType.connection_type, + null: true, + description: "List of the project's CI/CD variables.", + authorize: :admin_build, + method: :variables + field :ci_cd_settings, Types::Ci::CiCdSettingType, null: true, diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 46d121f6552f5aa39fb56528e030ebd1bce5fe46..f23d37b29aa1e3a407fc27282760402289174539 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -123,6 +123,11 @@ class QueryType < ::Types::BaseObject resolver: Resolvers::Ci::RunnersResolver, description: "Find runners visible to the current user." + field :ci_variables, + Types::Ci::VariableType.connection_type, + null: true, + description: "List of the instance's CI/CD variables." + field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_MAX_COMPLEXITY / 2 + 1 field :timelogs, Types::TimelogType.connection_type, @@ -174,6 +179,12 @@ def ci_application_settings application_settings end + def ci_variables + return unless current_user.can_admin_all_resources? + + ::Ci::InstanceVariable.all + end + def application_settings Gitlab::CurrentSettings.current_application_settings end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index af91c842838ecfbf5f0d0829b381f093d7403c77..1fdb2b2f18b249a10637412e46519e80c460de43 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -87,6 +87,16 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="queryciminutesusagenamespaceid"></a>`namespaceId` | [`NamespaceID`](#namespaceid) | Global ID of the Namespace for the monthly CI/CD minutes usage. | +### `Query.ciVariables` + +List of the instance's CI/CD variables. + +Returns [`CiVariableConnection`](#civariableconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + ### `Query.containerRepository` Find a container repository. @@ -6235,6 +6245,29 @@ The edge type for [`CiStage`](#cistage). | <a id="cistageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="cistageedgenode"></a>`node` | [`CiStage`](#cistage) | The item at the end of the edge. | +#### `CiVariableConnection` + +The connection type for [`CiVariable`](#civariable). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="civariableconnectionedges"></a>`edges` | [`[CiVariableEdge]`](#civariableedge) | A list of edges. | +| <a id="civariableconnectionnodes"></a>`nodes` | [`[CiVariable]`](#civariable) | A list of nodes. | +| <a id="civariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `CiVariableEdge` + +The edge type for [`CiVariable`](#civariable). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="civariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | +| <a id="civariableedgenode"></a>`node` | [`CiVariable`](#civariable) | The item at the end of the edge. | + #### `ClusterAgentActivityEventConnection` The connection type for [`ClusterAgentActivityEvent`](#clusteragentactivityevent). @@ -9979,6 +10012,20 @@ GitLab CI/CD configuration template. | <a id="citemplatecontent"></a>`content` | [`String!`](#string) | Contents of the CI template. | | <a id="citemplatename"></a>`name` | [`String!`](#string) | Name of the CI template. | +### `CiVariable` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="civariableid"></a>`id` | [`ID!`](#id) | ID of the variable. | +| <a id="civariablekey"></a>`key` | [`String`](#string) | Name of the variable. | +| <a id="civariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. | +| <a id="civariableprotected"></a>`protected` | [`Boolean`](#boolean) | Indicates whether the variable is protected. | +| <a id="civariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. | +| <a id="civariablevalue"></a>`value` | [`String`](#string) | Value of the variable. | +| <a id="civariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. | + ### `ClusterAgent` #### Fields @@ -11734,6 +11781,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="groupallowstalerunnerpruning"></a>`allowStaleRunnerPruning` | [`Boolean!`](#boolean) | Indicates whether to regularly prune stale group runners. Defaults to false. | | <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. | | <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. | +| <a id="groupcivariables"></a>`ciVariables` | [`CiVariableConnection`](#civariableconnection) | List of the group's CI/CD variables. (see [Connections](#connections)) | | <a id="groupclusteragents"></a>`clusterAgents` | [`ClusterAgentConnection`](#clusteragentconnection) | Cluster agents associated with projects in the group and its subgroups. (see [Connections](#connections)) | | <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. | | <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. | @@ -14990,6 +15038,7 @@ Represents vulnerability finding of a security report on the pipeline. | <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. | +| <a id="projectcivariables"></a>`ciVariables` | [`CiVariableConnection`](#civariableconnection) | List of the project's CI/CD variables. (see [Connections](#connections)) | | <a id="projectclusteragents"></a>`clusterAgents` | [`ClusterAgentConnection`](#clusteragentconnection) | Cluster agents associated with the project. (see [Connections](#connections)) | | <a id="projectcodecoveragesummary"></a>`codeCoverageSummary` | [`CodeCoverageSummary`](#codecoveragesummary) | Code coverage summary associated with the project. | | <a id="projectcomplianceframeworks"></a>`complianceFrameworks` | [`ComplianceFrameworkConnection`](#complianceframeworkconnection) | Compliance frameworks associated with the project. (see [Connections](#connections)) | @@ -18645,6 +18694,13 @@ Values for sorting runners. | <a id="cirunnerupgradestatustyperecommended"></a>`RECOMMENDED` | Upgrade is available and recommended for the runner. | | <a id="cirunnerupgradestatustypeunknown"></a>`UNKNOWN` | Upgrade status is unknown. | +### `CiVariableType` + +| Value | Description | +| ----- | ----------- | +| <a id="civariabletypeenv_var"></a>`ENV_VAR` | Env var type. | +| <a id="civariabletypefile"></a>`FILE` | File type. | + ### `CodeQualityDegradationSeverity` | Value | Description | diff --git a/spec/graphql/types/ci/variable_type_enum_spec.rb b/spec/graphql/types/ci/variable_type_enum_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5604caebffff0f22a58deb2d8476bd0a841d468a --- /dev/null +++ b/spec/graphql/types/ci/variable_type_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CiVariableType'] do + it 'matches the keys of Ci::Variable.variable_types' do + expect(described_class.values.keys).to contain_exactly('ENV_VAR', 'FILE') + end +end diff --git a/spec/graphql/types/ci/variables_type_spec.rb b/spec/graphql/types/ci/variables_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0a97a0f72f39f581628247bff845943511a0ffe8 --- /dev/null +++ b/spec/graphql/types/ci/variables_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CiVariable'] do + it 'contains attributes related to CI variables' do + expect(described_class).to have_graphql_fields( + :id, :key, :value, :variable_type, :protected, :masked, :raw + ) + end +end diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb index 82703948cea6d32f1f48d0f5f78d17faa0b62a01..69c7eaf111fd5ad2e9ba5af761341776680802bf 100644 --- a/spec/graphql/types/group_type_spec.rb +++ b/spec/graphql/types/group_type_spec.rb @@ -23,7 +23,7 @@ dependency_proxy_blob_count dependency_proxy_total_size dependency_proxy_image_prefix dependency_proxy_image_ttl_policy shared_runners_setting timelogs organizations contacts work_item_types - recent_issue_boards + recent_issue_boards ci_variables ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 2e994bf78201edcde779cd1a49f851d72f5db786..ed93d31da0f78d7b77e5f3bc3abe2dd8aeb3e7e9 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -36,7 +36,8 @@ pipeline_analytics squash_read_only sast_ci_configuration cluster_agent cluster_agents agent_configurations ci_template timelogs merge_commit_template squash_commit_template work_item_types - recent_issue_boards ci_config_path_or_default packages_cleanup_policy + recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables + recent_issue_boards ci_config_path_or_default ci_variables ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 8b8c44c10f6ac77e12460f5bbfe0963f8ab9329c..514d24a209e5f44144265a1ecf8d26492a82461f 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -30,6 +30,7 @@ board_list topics gitpod_enabled + ci_variables ] expect(described_class).to have_graphql_fields(*expected_fields).at_least diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f0a571f1fef27f3491246e84d01ea778b0013061 --- /dev/null +++ b/spec/requests/api/graphql/ci/group_variables_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.group(fullPath).ciVariables' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + let(:query) do + %( + query { + group(fullPath: "#{group.full_path}") { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + } + } + } + } + ) + end + + context 'when the user can administer the group' do + before do + group.add_owner(user) + end + + it "returns the group's CI variables" do + variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true + }) + end + end + + context 'when the user cannot administer the group' do + it 'returns nothing' do + create(:ci_group_variable, group: group, value: 'verysecret', masked: true) + + group.add_developer(user) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('group', 'ciVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1faa4289029674ebd862109ab5cb61e1e98ef2b0 --- /dev/null +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.ciVariables' do + include GraphqlHelpers + + let(:query) do + %( + query { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + } + } + } + ) + end + + context 'when the user is an admin' do + let_it_be(:user) { create(:admin) } + + it "returns the instance's CI variables" do + variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true + }) + end + end + + context 'when the user is not an admin' do + let_it_be(:user) { create(:user) } + + it 'returns nothing' do + create(:ci_instance_variable, value: 'verysecret', masked: true) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('ciVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a4c1ef9c6504c0eacec2cf6d36c2b0a2674e2cb4 --- /dev/null +++ b/spec/requests/api/graphql/ci/project_variables_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).ciVariables' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + } + } + } + } + ) + end + + context 'when the user can administer builds' do + before do + project.add_maintainer(user) + end + + it "returns the project's CI variables" do + variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true + }) + end + end + + context 'when the user cannot administer builds' do + it 'returns nothing' do + create(:ci_variable, project: project, value: 'verysecret', masked: true) + + project.add_developer(user) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciVariables')).to be_nil + end + end +end