Commit 3ab78d13 authored by Sri's avatar Sri

Button to revoke authorizations

This commit adds a button to revoke authorizations
in the `Projects :: Infra :: Google Cloud` section of
a project

Changes include:
- Frontend component for the revoke button and
  associated texts
- Controller to handle revoke route
- Associated specs and tests
parent 77d3ce8f
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import DeploymentsServiceTable from './deployments_service_table.vue';
import RevokeOauth from './revoke_oauth.vue';
import ServiceAccountsList from './service_accounts_list.vue';
import GcpRegionsList from './gcp_regions_list.vue';
......@@ -9,6 +10,7 @@ export default {
GlTabs,
GlTab,
DeploymentsServiceTable,
RevokeOauth,
ServiceAccountsList,
GcpRegionsList,
},
......@@ -41,6 +43,10 @@ export default {
type: Array,
required: true,
},
revokeOauthUrl: {
type: String,
required: true,
},
},
};
</script>
......@@ -61,6 +67,8 @@ export default {
:create-url="configureGcpRegionsUrl"
:list="gcpRegions"
/>
<hr v-if="revokeOauthUrl" />
<revoke-oauth v-if="revokeOauthUrl" :url="revokeOauthUrl" />
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployments-service-table
......
<script>
import { GlButton, GlForm } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { s__ } from '~/locale';
export const GOOGLE_CLOUD_REVOKE_TITLE = s__('GoogleCloud|Revoke authorizations');
export const GOOGLE_CLOUD_REVOKE_DESCRIPTION = s__(
'GoogleCloud|Revoke authorizations granted to GitLab. This does not invalidate service accounts.',
);
export default {
components: { GlButton, GlForm },
csrf,
props: {
url: {
type: String,
required: true,
},
},
i18n: {
title: GOOGLE_CLOUD_REVOKE_TITLE,
description: GOOGLE_CLOUD_REVOKE_DESCRIPTION,
},
};
</script>
<template>
<div class="gl-mx-4">
<h2 class="gl-font-size-h2">{{ $options.i18n.title }}</h2>
<p>{{ $options.i18n.description }}</p>
<gl-form :action="url" method="post">
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<gl-button category="secondary" variant="warning" type="submit">
{{ $options.i18n.title }}
</gl-button>
</gl-form>
</div>
</template>
......@@ -12,13 +12,14 @@ class Projects::GoogleCloud::GcpRegionsController < Projects::GoogleCloud::BaseC
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
tags = TagsFinder.new(project.repository, params).execute(gitaly_pagination: true)
refs = (branches + tags).map(&:name)
@js_data = {
js_data = {
screen: 'gcp_regions_form',
availableRegions: AVAILABLE_REGIONS,
refs: refs,
cancelPath: project_google_cloud_index_path(project)
}.to_json
track_event('gcp_regions#index', 'form_render', @js_data)
}
@js_data = js_data.to_json
track_event('gcp_regions#index', 'form_render', js_data)
end
def create
......
# frozen_string_literal: true
class Projects::GoogleCloud::RevokeOauthController < Projects::GoogleCloud::BaseController
before_action :validate_gcp_token!
def create
google_api_client = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
response = google_api_client.revoke_authorizations
if response.success?
status = 'success'
redirect_message = { notice: s_('GoogleCloud|Google OAuth2 token revocation requested') }
else
status = 'failed'
redirect_message = { alert: s_('GoogleCloud|Google OAuth2 token revocation request failed') }
end
session.delete(GoogleApi::CloudPlatform::Client.session_key_for_token)
track_event('revoke_oauth#create', 'create', status)
redirect_to project_google_cloud_index_path(project), redirect_message
end
end
......@@ -17,14 +17,15 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
tags = TagsFinder.new(project.repository, params).execute(gitaly_pagination: true)
refs = (branches + tags).map(&:name)
@js_data = {
js_data = {
screen: 'service_accounts_form',
gcpProjects: gcp_projects,
refs: refs,
cancelPath: project_google_cloud_index_path(project)
}.to_json
}
@js_data = js_data.to_json
track_event('service_accounts#index', 'form_success', @js_data)
track_event('service_accounts#index', 'form_success', js_data)
end
rescue Google::Apis::ClientError => error
handle_gcp_error('service_accounts#index', error)
......
......@@ -4,7 +4,7 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
GCP_REGION_CI_VAR_KEY = 'GCP_REGION'
def index
@js_data = {
js_data = {
screen: 'home',
serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project,
createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
......@@ -12,9 +12,11 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project),
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
gcpRegions: gcp_regions
}.to_json
track_event('google_cloud#index', 'index', @js_data)
gcpRegions: gcp_regions,
revokeOauthUrl: revoke_oauth_url
}
@js_data = js_data.to_json
track_event('google_cloud#index', 'index', js_data)
end
private
......@@ -23,4 +25,10 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
list = ::Ci::VariablesFinder.new(project, { key: GCP_REGION_CI_VAR_KEY }).execute
list.map { |variable| { gcp_region: variable.value, environment: variable.environment_scope } }
end
def revoke_oauth_url
google_token_valid = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
google_token_valid ? project_google_cloud_revoke_oauth_index_path(project) : nil
end
end
......@@ -322,6 +322,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :google_cloud, only: [:index]
namespace :google_cloud do
resources :revoke_oauth, only: [:create]
resources :service_accounts, only: [:index, :create]
resources :gcp_regions, only: [:index, :create]
......
......@@ -22,6 +22,7 @@ module GoogleApi
"https://www.googleapis.com/auth/monitoring"
].freeze
ROLES_LIST = %w[roles/iam.serviceAccountUser roles/artifactregistry.admin roles/cloudbuild.builds.builder roles/run.admin roles/storage.admin roles/cloudsql.admin roles/browser].freeze
REVOKE_URL = 'https://oauth2.googleapis.com/revoke'
class << self
def session_key_for_token
......@@ -146,6 +147,11 @@ module GoogleApi
enable_service(gcp_project_id, 'cloudbuild.googleapis.com')
end
def revoke_authorizations
uri = URI(REVOKE_URL)
Gitlab::HTTP.post(uri, body: { 'token' => access_token })
end
private
def enable_service(gcp_project_id, service_name)
......@@ -211,7 +217,7 @@ module GoogleApi
end
def cloud_resource_manager_service
@gpc_service ||= Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService.new.tap { |s| s. authorization = access_token }
@gpc_service ||= Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService.new.tap { |s| s.authorization = access_token }
end
end
end
......
......@@ -17093,6 +17093,12 @@ msgstr ""
msgid "GoogleCloud|Google Cloud project"
msgstr ""
msgid "GoogleCloud|Google OAuth2 token revocation request failed"
msgstr ""
msgid "GoogleCloud|Google OAuth2 token revocation requested"
msgstr ""
msgid "GoogleCloud|I understand the responsibilities involved with managing service account keys"
msgstr ""
......@@ -17102,6 +17108,12 @@ msgstr ""
msgid "GoogleCloud|Refs"
msgstr ""
msgid "GoogleCloud|Revoke authorizations"
msgstr ""
msgid "GoogleCloud|Revoke authorizations granted to GitLab. This does not invalidate service accounts."
msgstr ""
msgid "Got it"
msgstr ""
......
......@@ -28,6 +28,7 @@ const HOME_PROPS = {
emptyIllustrationUrl: '#url-empty-illustration',
enableCloudRunUrl: '#url-enable-cloud-run',
enableCloudStorageUrl: '#enableCloudStorageUrl',
revokeOauthUrl: '#revokeOauthUrl',
};
describe('google_cloud App component', () => {
......
......@@ -24,6 +24,7 @@ describe('google_cloud Home component', () => {
emptyIllustrationUrl: '#url-empty-illustration',
enableCloudRunUrl: '#url-enable-cloud-run',
enableCloudStorageUrl: '#enableCloudStorageUrl',
revokeOauthUrl: '#revokeOauthUrl',
};
beforeEach(() => {
......
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlForm } from '@gitlab/ui';
import RevokeOauth, {
GOOGLE_CLOUD_REVOKE_TITLE,
GOOGLE_CLOUD_REVOKE_DESCRIPTION,
} from '~/google_cloud/components/revoke_oauth.vue';
describe('RevokeOauth component', () => {
let wrapper;
const findTitle = () => wrapper.find('h2');
const findDescription = () => wrapper.find('p');
const findForm = () => wrapper.findComponent(GlForm);
const findButton = () => wrapper.findComponent(GlButton);
const propsData = {
url: 'url_general_feedback',
};
beforeEach(() => {
wrapper = shallowMount(RevokeOauth, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('contains title', () => {
const title = findTitle();
expect(title.text()).toContain('Revoke authorizations');
});
it('contains description', () => {
const description = findDescription();
expect(description.text()).toContain(GOOGLE_CLOUD_REVOKE_DESCRIPTION);
});
it('contains form', () => {
const form = findForm();
expect(form.attributes('action')).toBe(propsData.url);
expect(form.attributes('method')).toBe('post');
});
it('contains button', () => {
const button = findButton();
expect(button.text()).toContain(GOOGLE_CLOUD_REVOKE_TITLE);
});
});
......@@ -334,4 +334,20 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
is_expected.to eq(operation)
end
end
describe '#revoke_authorizations' do
subject { client.revoke_authorizations }
it 'calls the revoke endpoint' do
stub_request(:post, "https://oauth2.googleapis.com/revoke")
.with(
body: "token=token",
headers: {
'Accept' => '*/*',
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
'User-Agent' => 'Ruby'
})
.to_return(status: 200, body: "", headers: {})
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::GoogleCloud::RevokeOauthController do
include SessionHelpers
describe 'POST #create', :snowplow, :clean_gitlab_redis_sessions, :aggregate_failures do
let_it_be(:project) { create(:project, :public) }
let_it_be(:url) { project_google_cloud_revoke_oauth_index_path(project).to_s }
let(:user) { project.creator }
before do
sign_in(user)
stub_session(GoogleApi::CloudPlatform::Client.session_key_for_token => 'token')
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
end
end
context 'when GCP token is invalid' do
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(false)
end
end
it 'redirects to Google OAuth2 authorize URL' do
sign_in(user)
post url
expect(response).to redirect_to(assigns(:authorize_url))
end
end
context 'when revocation is successful' do
before do
stub_request(:post, "https://oauth2.googleapis.com/revoke")
.to_return(status: 200, body: "", headers: {})
end
it 'calls revoke endpoint and redirects' do
post url
expect(request.session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil
expect(response).to redirect_to(project_google_cloud_index_path(project))
expect(flash[:notice]).to eq('Google OAuth2 token revocation requested')
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'revoke_oauth#create',
label: 'create',
property: 'success',
project: project,
user: user
)
end
end
context 'when revocation fails' do
before do
stub_request(:post, "https://oauth2.googleapis.com/revoke")
.to_return(status: 400, body: "", headers: {})
end
it 'calls revoke endpoint and redirects' do
post url
expect(request.session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil
expect(response).to redirect_to(project_google_cloud_index_path(project))
expect(flash[:alert]).to eq('Google OAuth2 token revocation request failed')
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'revoke_oauth#create',
label: 'create',
property: 'failed',
project: project,
user: user
)
end
end
end
end
......@@ -141,6 +141,38 @@ RSpec.describe Projects::GoogleCloudController do
)
end
end
context 'but google oauth2 token is not valid' do
it 'does not return revoke oauth url' do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(false)
end
sign_in(user)
get url
expect(response).to be_successful
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'google_cloud#index',
label: 'index',
extra: {
screen: 'home',
serviceAccounts: [],
createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project),
enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project),
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
gcpRegions: [],
revokeOauthUrl: nil
},
project: project,
user: user
)
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment