diff --git a/config/feature_flags/beta/google_cloud_integration_onboarding.yml b/config/feature_flags/beta/google_cloud_integration_onboarding.yml new file mode 100644 index 0000000000000000000000000000000000000000..d4935c603c196649bbe6b8368e334470df521bc0 --- /dev/null +++ b/config/feature_flags/beta/google_cloud_integration_onboarding.yml @@ -0,0 +1,9 @@ +--- +name: google_cloud_integration_onboarding +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/437288 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141870 +rollout_issue_url: +milestone: '16.9' +group: group::authentication +type: beta +default_enabled: false diff --git a/ee/lib/api/project_google_cloud_integration.rb b/ee/lib/api/project_google_cloud_integration.rb new file mode 100644 index 0000000000000000000000000000000000000000..9a10ea28d2e0707e50eb4234d0d76642d3b65047 --- /dev/null +++ b/ee/lib/api/project_google_cloud_integration.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module API + class ProjectGoogleCloudIntegration < ::API::Base + feature_category :integrations + + before { authorize_admin_project } + before do + unless ::Feature.enabled?(:google_cloud_integration_onboarding, user_project.root_namespace, type: :beta) + not_found! + end + end + + desc 'Get shell script to create and configure Workload Identity Federation' do + detail 'This feature is experimental.' + end + params do + requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace ':id/scripts/google_cloud/' do + params do + requires :google_cloud_project_id, types: String + optional( + :google_cloud_workload_identity_pool_id, + { types: String, default: 'gitlab-wlif' }) + optional( + :google_cloud_workload_identity_pool_display_name, + { types: String, default: 'WLIF for GitLab integration' }) + optional( + :google_cloud_workload_identity_pool_provider_id, + { types: String, default: 'gitlab-wlif-oidc-provider' }) + optional( + :google_cloud_workload_identity_pool_provider_display_name, + { types: String, default: 'GitLab OIDC provider' }) + end + get '/create_wlif' do + env['api.format'] = :binary + content_type 'text/plain' + + template_path = File.join( + 'ee', 'lib', 'api', 'templates', 'google_cloud_integration_wlif_create.sh.erb') + template = ERB.new(File.read(template_path)) + + locals = { + google_cloud_project_id: + declared_params[:google_cloud_project_id], + google_cloud_workload_identity_pool_id: + declared_params[:google_cloud_workload_identity_pool_id], + google_cloud_workload_identity_pool_display_name: + declared_params[:google_cloud_workload_identity_pool_display_name], + google_cloud_workload_identity_pool_provider_id: + declared_params[:google_cloud_workload_identity_pool_provider_id], + google_cloud_workload_identity_pool_provider_display_name: + declared_params[:google_cloud_workload_identity_pool_provider_display_name], + google_cloud_workload_identity_pool_provider_issuer_uri: + ::GoogleCloudPlatform::GLGO_BASE_URL + } + + template.result_with_hash(locals) + end + + desc 'Get shell script to create IAM policy for the Workload Identity Federation principal' do + detail 'This feature is experimental.' + end + params do + requires :google_cloud_project_id, types: String + requires :google_cloud_workload_identity_pool_id, types: String + requires :oidc_claim_name, types: String + requires :oidc_claim_value, types: String + requires :google_cloud_iam_role, types: String + end + get '/create_iam_policy' do + env['api.format'] = :binary + content_type 'text/plain' + + template_path = File.join( + 'ee', 'lib', 'api', 'templates', 'google_cloud_integration_iam_policy_create.sh.erb') + template = ERB.new(File.read(template_path)) + + locals = { + google_cloud_project_id: + declared_params[:google_cloud_project_id], + google_cloud_workload_identity_pool_id: + declared_params[:google_cloud_workload_identity_pool_id], + oidc_claim_name: declared_params[:oidc_claim_name], + oidc_claim_value: declared_params[:oidc_claim_value], + google_cloud_iam_role: declared_params[:google_cloud_iam_role] + } + + template.result_with_hash(locals) + end + end + end + end +end diff --git a/ee/lib/api/templates/google_cloud_integration_iam_policy_create.sh.erb b/ee/lib/api/templates/google_cloud_integration_iam_policy_create.sh.erb new file mode 100644 index 0000000000000000000000000000000000000000..7c2b603b35cf5a27e719ffe722cdbb69370d8d4f --- /dev/null +++ b/ee/lib/api/templates/google_cloud_integration_iam_policy_create.sh.erb @@ -0,0 +1,15 @@ +#!/bin/bash + +set -eux +set -o pipefail + +gcloud config set project '<%= google_cloud_project_id %>' + +GOOGLE_CLOUD_PROJECT_NUMBER=$(gcloud projects describe '<%= google_cloud_project_id %>' --format='value(projectNumber)') + +PRINCIPAL="principalSet://iam.googleapis.com/projects/$GOOGLE_CLOUD_PROJECT_NUMBER/\ +locations/global/workloadIdentityPools/<%= google_cloud_workload_identity_pool_id %>/\ +attribute.<%= oidc_claim_name %>/<%= oidc_claim_value %>" + +gcloud projects add-iam-policy-binding '<%= google_cloud_project_id %>' \ + --member=$PRINCIPAL --role='<%= google_cloud_iam_role %>' diff --git a/ee/lib/api/templates/google_cloud_integration_wlif_create.sh.erb b/ee/lib/api/templates/google_cloud_integration_wlif_create.sh.erb new file mode 100644 index 0000000000000000000000000000000000000000..b44e064aaf3bd9d63a1b4307562eb5decca84507 --- /dev/null +++ b/ee/lib/api/templates/google_cloud_integration_wlif_create.sh.erb @@ -0,0 +1,25 @@ +#!/bin/bash + +set -eux +set -o pipefail + +gcloud config set project '<%= google_cloud_project_id %>' + +gcloud iam workload-identity-pools create <%= google_cloud_workload_identity_pool_id %> \ + --location="global" \ + --display-name="<%= google_cloud_workload_identity_pool_display_name %>" + +# Verify the created Workload Identity Pool +gcloud iam workload-identity-pools describe <%= google_cloud_workload_identity_pool_id %> --location="global" + +gcloud iam workload-identity-pools providers create-oidc "<%= google_cloud_workload_identity_pool_provider_id %>" \ + --location="global" \ + --workload-identity-pool="<%= google_cloud_workload_identity_pool_id %>" \ + --issuer-uri="<%= google_cloud_workload_identity_pool_provider_issuer_uri %>" \ + --display-name="<%= google_cloud_workload_identity_pool_provider_display_name %>" \ + --attribute-mapping="google.subject=assertion.sub" + +# Verify the created OIDC provider for the Workload Identity Pool +gcloud iam workload-identity-pools providers list \ + --workload-identity-pool="<%= google_cloud_workload_identity_pool_id %>" \ + --location=global diff --git a/ee/lib/ee/api/api.rb b/ee/lib/ee/api/api.rb index 04610bf1b4345d9a31bbbd60f6a21c8f31663d60..cf70903b4e8063a2d53944aaa72ba7845d9c32fd 100644 --- a/ee/lib/ee/api/api.rb +++ b/ee/lib/ee/api/api.rb @@ -46,6 +46,7 @@ module API mount ::API::ServiceAccounts mount ::API::ManagedLicenses mount ::API::ProjectApprovals + mount ::API::ProjectGoogleCloudIntegration mount ::API::Vulnerabilities mount ::API::VulnerabilityFindings mount ::API::VulnerabilityIssueLinks diff --git a/ee/spec/requests/api/project_google_cloud_integration_spec.rb b/ee/spec/requests/api/project_google_cloud_integration_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..61fa0cde88f6fc79c51eb5bce238405471ca3f32 --- /dev/null +++ b/ee/spec/requests/api/project_google_cloud_integration_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ProjectGoogleCloudIntegration, feature_category: :integrations do + let_it_be(:owner) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, namespace: group) } + + let(:google_cloud_project_id) { 'google-cloud-project-id' } + + before_all do + group.add_owner(owner) + end + + shared_examples 'an endpoint generating a bash script for Google Cloud' do + it 'generates the script' do + get(api(path, owner), params: params) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.content_type).to eql('text/plain') + expect(response.body).to include("gcloud config set project '#{google_cloud_project_id}'") + end + + context 'when required param is missing' do + let(:params) { {} } + + it 'returns error' do + get(api(path, owner), params: params) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when user do not have project admin access' do + let_it_be(:user) { create(:user) } + + before_all do + group.add_developer(user) + end + + it 'returns error' do + get(api(path, user), params: params) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(google_cloud_integration_onboarding: false) + end + + it 'returns error' do + get(api(path, owner), params: params) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET /projects/:id/scripts/google_cloud/create_wlif' do + let(:path) { "/projects/#{project.id}/scripts/google_cloud/create_wlif" } + let(:params) { { google_cloud_project_id: google_cloud_project_id } } + + it_behaves_like 'an endpoint generating a bash script for Google Cloud' + end + + describe 'GET /projects/:id/scripts/google_cloud/create_iam_policy' do + let(:path) { "/projects/#{project.id}/scripts/google_cloud/create_iam_policy" } + let(:params) do + { + google_cloud_project_id: google_cloud_project_id, + google_cloud_workload_identity_pool_id: 'wlif-gitlab', + oidc_claim_name: 'username', + oidc_claim_value: 'user@example.com', + google_cloud_iam_role: 'roles/compute.admin' + } + end + + it_behaves_like 'an endpoint generating a bash script for Google Cloud' + end +end