Commit 6c99dd3a authored by Sri's avatar Sri

Track `Projects::GoogleCloud` events with Snowplow

- Use snowplow to track all success and error events
in the backend related to `Projects::GoogleCloud`

- Update tests to verify tracking of events
parent 9adfd084
......@@ -10,18 +10,25 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
private
def admin_project_google_cloud!
access_denied! unless can?(current_user, :admin_project_google_cloud, project)
unless can?(current_user, :admin_project_google_cloud, project)
track_event('admin_project_google_cloud!', 'access_denied', 'invalid_user')
access_denied!
end
end
def google_oauth2_enabled!
config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
if config.app_id.blank? || config.app_secret.blank?
track_event('google_oauth2_enabled!', 'access_denied', { reason: 'google_oauth2_not_configured', config: config })
access_denied! 'This GitLab instance not configured for Google Oauth2.'
end
end
def feature_flag_enabled!
access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud, project)
unless Feature.enabled?(:incubation_5mp_google_cloud, project)
track_event('feature_flag_enabled!', 'access_denied', 'feature_flag_not_enabled')
access_denied!
end
end
def validate_gcp_token!
......@@ -53,9 +60,21 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def handle_gcp_error(error, project)
Gitlab::ErrorTracking.track_exception(error, project_id: project.id)
def handle_gcp_error(action, error)
track_event(action, 'gcp_error', error)
@js_data = { screen: 'gcp_error', error: error.to_s }.to_json
render status: :unauthorized, template: 'projects/google_cloud/errors/gcp_error'
end
def track_event(action, label, property)
options = { label: label, project: project, user: current_user }
if property.is_a?(String)
options[:property] = property
else
options[:extra] = property
end
Gitlab::Tracking.event('Projects::GoogleCloud', action, **options)
end
end
......@@ -9,6 +9,7 @@ class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::Base
.new(project, current_user, params).execute
if enable_cloud_run_response[:status] == :error
track_event('deployments#cloud_run', 'enable_cloud_run_error', enable_cloud_run_response)
flash[:error] = enable_cloud_run_response[:message]
redirect_to project_google_cloud_index_path(project)
else
......@@ -17,15 +18,17 @@ class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::Base
.new(project, current_user, params).execute
if generate_pipeline_response[:status] == :error
track_event('deployments#cloud_run', 'generate_pipeline_error', generate_pipeline_response)
flash[:error] = 'Failed to generate pipeline'
redirect_to project_google_cloud_index_path(project)
else
cloud_run_mr_params = cloud_run_mr_params(generate_pipeline_response[:branch_name])
track_event('deployments#cloud_run', 'cloud_run_success', cloud_run_mr_params)
redirect_to project_new_merge_request_path(project, merge_request: cloud_run_mr_params)
end
end
rescue Google::Apis::ClientError => error
handle_gcp_error(error, project)
handle_gcp_error('deployments#cloud_run', error)
end
def cloud_storage
......
......@@ -18,13 +18,13 @@ class Projects::GoogleCloud::GcpRegionsController < Projects::GoogleCloud::BaseC
refs: refs,
cancelPath: project_google_cloud_index_path(project)
}.to_json
track_event('gcp_regions#index', 'form_render', @js_data)
end
def create
permitted_params = params.permit(:ref, :gcp_region)
GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:ref], permitted_params[:gcp_region])
response = GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:ref], permitted_params[:gcp_region])
track_event('gcp_regions#create', 'form_submit', response)
redirect_to project_google_cloud_index_path(project), notice: _('GCP region configured')
end
end
......@@ -10,6 +10,7 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
if gcp_projects.empty?
@js_data = { screen: 'no_gcp_projects' }.to_json
track_event('service_accounts#index', 'form_error', 'no_gcp_projects')
render status: :unauthorized, template: 'projects/google_cloud/errors/no_gcp_projects'
else
params = { per_page: 50 }
......@@ -22,9 +23,11 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
refs: refs,
cancelPath: project_google_cloud_index_path(project)
}.to_json
track_event('service_accounts#index', 'form_success', @js_data)
end
rescue Google::Apis::ClientError => error
handle_gcp_error(error, project)
handle_gcp_error('service_accounts#index', error)
end
def create
......@@ -38,8 +41,9 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
environment_name: permitted_params[:ref]
).execute
track_event('service_accounts#create', 'form_submit', response)
redirect_to project_google_cloud_index_path(project), notice: response.message
rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error
handle_gcp_error(error, project)
handle_gcp_error('service_accounts#create', error)
end
end
......@@ -14,6 +14,7 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
gcpRegions: gcp_regions
}.to_json
track_event('google_cloud#index', 'index', @js_data)
end
private
......
......@@ -22,13 +22,21 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
project.add_maintainer(user_maintainer)
end
describe "Routes must be restricted behind Google OAuth2" do
describe "Routes must be restricted behind Google OAuth2", :snowplow do
context 'when a public request is made' do
it 'returns not found on GET request' do
urls_list.each do |url|
get url
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: nil
)
end
end
end
......@@ -40,6 +48,14 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
get url
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: nil
)
end
end
end
......@@ -60,7 +76,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
end
end
describe 'Authorized GET project/-/google_cloud/deployments/cloud_run' do
describe 'Authorized GET project/-/google_cloud/deployments/cloud_run', :snowplow do
let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
before do
......@@ -72,25 +88,39 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
end
it 'redirects to google_cloud home on enable service error' do
# since GPC_PROJECT_ID is not set, enable cloud run service should return an error
get url
expect(response).to redirect_to(project_google_cloud_index_path(project))
# since GPC_PROJECT_ID is not set, enable cloud run service should return an error
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'deployments#cloud_run',
label: 'enable_cloud_run_error',
extra: { message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.',
status: :error },
project: project,
user: user_maintainer
)
end
it 'tracks error and redirects to gcp_error' do
mock_google_error = Google::Apis::ClientError.new('some_error')
it 'redirects to gcp_error' do
mock_gcp_error = Google::Apis::ClientError.new('some_error')
allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
allow(service).to receive(:execute).and_raise(mock_google_error)
allow(service).to receive(:execute).and_raise(mock_gcp_error)
end
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(mock_google_error, { project_id: project.id })
get url
expect(response).to render_template(:gcp_error)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'deployments#cloud_run',
label: 'gcp_error',
extra: mock_gcp_error,
project: project,
user: user_maintainer
)
end
context 'GCP_PROJECT_IDs are defined' do
......@@ -106,6 +136,14 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
get url
expect(response).to redirect_to(project_google_cloud_index_path(project))
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'deployments#cloud_run',
label: 'generate_pipeline_error',
extra: { status: :error },
project: project,
user: user_maintainer
)
end
it 'redirects to create merge request form' do
......@@ -121,11 +159,24 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to include(project_new_merge_request_path(project))
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'deployments#cloud_run',
label: 'cloud_run_success',
extra: { "title": "Enable deployments to Cloud Run",
"description": "This merge request includes a Cloud Run deployment job in the pipeline definition (.gitlab-ci.yml).\n\nThe `deploy-to-cloud-run` job:\n* Requires the following environment variables\n * `GCP_PROJECT_ID`\n * `GCP_SERVICE_ACCOUNT_KEY`\n* Job definition can be found at: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library\n\nThis pipeline definition has been committed to the branch ``.\nYou may modify the pipeline definition further or accept the changes as-is if suitable.\n",
"source_project_id": project.id,
"target_project_id": project.id,
"source_branch": nil,
"target_branch": project.default_branch },
project: project,
user: user_maintainer
)
end
end
end
describe 'Authorized GET project/-/google_cloud/deployments/cloud_storage' do
describe 'Authorized GET project/-/google_cloud/deployments/cloud_storage', :snowplow do
let_it_be(:url) { "#{project_google_cloud_deployments_cloud_storage_path(project)}" }
before do
......
......@@ -6,6 +6,65 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:repository) { project.repository }
let(:user_guest) { create(:user) }
let(:user_maintainer) { create(:user) }
RSpec.shared_examples "should track not_found event" do
it "tracks event" do
is_expected.to be(404)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: nil
)
end
end
RSpec.shared_examples "should track access_denied event" do
it "tracks event" do
is_expected.to be(404)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: nil
)
end
end
RSpec.shared_examples "should track feature_flag_disabled event" do |user|
it "tracks event" do
is_expected.to be(404)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'feature_flag_enabled!',
label: 'access_denied',
property: 'feature_flag_not_enabled',
project: project,
user: user_maintainer
)
end
end
RSpec.shared_examples "should track gcp_error event" do |config|
it "tracks event" do
is_expected.to be(403)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'google_oauth2_enabled!',
label: 'access_denied',
extra: { reason: 'google_oauth2_not_configured', config: config },
project: project,
user: user_maintainer
)
end
end
RSpec.shared_examples "should be not found" do
it 'returns not found' do
is_expected.to be(404)
......@@ -20,27 +79,25 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
RSpec.shared_examples "public request should 404" do
it_behaves_like "should be not found"
it_behaves_like "should track not_found event"
end
RSpec.shared_examples "unauthorized access should 404" do
let(:user_guest) { create(:user) }
before do
project.add_guest(user_guest)
end
it_behaves_like "should be not found"
it_behaves_like "should track access_denied event"
end
describe 'GET #index' do
describe 'GET #index', :snowplow do
subject { get project_google_cloud_gcp_regions_path(project) }
it_behaves_like "public request should 404"
it_behaves_like "unauthorized access should 404"
context 'when authorized members make requests' do
let(:user_maintainer) { create(:user) }
before do
project.add_maintainer(user_maintainer)
sign_in(user_maintainer)
......@@ -51,15 +108,17 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
end
context 'but gitlab instance is not configured for google oauth2' do
unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret)
.new('', '')
before do
unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret)
.new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)
end
it_behaves_like "should be forbidden"
it_behaves_like "should track gcp_error event", unconfigured_google_oauth2
end
context 'but feature flag is disabled' do
......@@ -68,19 +127,18 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
end
it_behaves_like "should be not found"
it_behaves_like "should track feature_flag_disabled event"
end
end
end
describe 'POST #index' do
describe 'POST #index', :snowplow do
subject { post project_google_cloud_gcp_regions_path(project), params: { gcp_region: 'region1', environment: 'env1' } }
it_behaves_like "public request should 404"
it_behaves_like "unauthorized access should 404"
context 'when authorized members make requests' do
let(:user_maintainer) { create(:user) }
before do
project.add_maintainer(user_maintainer)
sign_in(user_maintainer)
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
let_it_be(:project) { create(:project, :public) }
describe 'GET index' do
describe 'GET index', :snowplow do
let_it_be(:url) { "#{project_google_cloud_service_accounts_path(project)}" }
let(:user_guest) { create(:user) }
......@@ -27,6 +27,14 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
get url
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: nil
)
end
it 'returns not found on POST request' do
......@@ -42,6 +50,14 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
sign_in(unauthorized_member)
get url
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: unauthorized_member
)
expect(response).to have_gitlab_http_status(:not_found)
end
......@@ -52,6 +68,14 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
sign_in(unauthorized_member)
post url
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: unauthorized_member
)
expect(response).to have_gitlab_http_status(:not_found)
end
......@@ -80,42 +104,75 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
end
context 'and user has successfully completed the google oauth2 flow' do
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
mock_service_account = Struct.new(:project_id, :unique_id, :email).new(123, 456, 'em@ai.l')
allow(client).to receive(:validate_token).and_return(true)
allow(client).to receive(:list_projects).and_return([{}, {}, {}])
allow(client).to receive(:create_service_account).and_return(mock_service_account)
allow(client).to receive(:create_service_account_key).and_return({})
allow(client).to receive(:grant_service_account_roles)
context 'but the user does not have gcp projects' do
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
mock_service_account = Struct.new(:project_id, :unique_id, :email).new(123, 456, 'em@ai.l')
allow(client).to receive(:list_projects).and_return([])
allow(client).to receive(:validate_token).and_return(true)
allow(client).to receive(:create_service_account).and_return(mock_service_account)
allow(client).to receive(:create_service_account_key).and_return({})
allow(client).to receive(:grant_service_account_roles)
end
end
end
it 'returns success on GET' do
authorized_members.each do |authorized_member|
allow_next_instance_of(BranchesFinder) do |branches_finder|
allow(branches_finder).to receive(:execute).and_return([])
it 'renders no_gcp_projects' do
authorized_members.each do |authorized_member|
allow_next_instance_of(BranchesFinder) do |branches_finder|
allow(branches_finder).to receive(:execute).and_return([])
end
allow_next_instance_of(TagsFinder) do |branches_finder|
allow(branches_finder).to receive(:execute).and_return([])
end
sign_in(authorized_member)
get url
expect(response).to render_template('projects/google_cloud/errors/no_gcp_projects')
end
end
end
allow_next_instance_of(TagsFinder) do |branches_finder|
allow(branches_finder).to receive(:execute).and_return([])
context 'user has three gcp_projects' do
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
mock_service_account = Struct.new(:project_id, :unique_id, :email).new(123, 456, 'em@ai.l')
allow(client).to receive(:list_projects).and_return([{}, {}, {}])
allow(client).to receive(:validate_token).and_return(true)
allow(client).to receive(:create_service_account).and_return(mock_service_account)
allow(client).to receive(:create_service_account_key).and_return({})
allow(client).to receive(:grant_service_account_roles)
end
end
sign_in(authorized_member)
it 'returns success on GET' do
authorized_members.each do |authorized_member|
allow_next_instance_of(BranchesFinder) do |branches_finder|
allow(branches_finder).to receive(:execute).and_return([])
end
get url
allow_next_instance_of(TagsFinder) do |branches_finder|
allow(branches_finder).to receive(:execute).and_return([])
end
sign_in(authorized_member)
get url
expect(response).to have_gitlab_http_status(:ok)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
it 'returns success on POST' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
it 'returns success on POST' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
post url, params: { gcp_project: 'prj1', ref: 'env1' }
post url, params: { gcp_project: 'prj1', ref: 'env1' }
expect(response).to redirect_to(project_google_cloud_index_path(project))
expect(response).to redirect_to(project_google_cloud_index_path(project))
end
end
end
end
......
......@@ -8,7 +8,7 @@ MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
RSpec.describe Projects::GoogleCloudController do
let_it_be(:project) { create(:project, :public) }
describe 'GET index' do
describe 'GET index', :snowplow do
let_it_be(:url) { "#{project_google_cloud_index_path(project)}" }
context 'when a public request is made' do
......@@ -16,6 +16,13 @@ RSpec.describe Projects::GoogleCloudController do
get url
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: nil)
end
end
......@@ -29,6 +36,14 @@ RSpec.describe Projects::GoogleCloudController do
get url
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: user
)
end
end
......@@ -42,6 +57,14 @@ RSpec.describe Projects::GoogleCloudController do
get url
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'access_denied',
property: 'invalid_user',
project: project,
user: user
)
end
end
......@@ -74,19 +97,26 @@ RSpec.describe Projects::GoogleCloudController do
let(:user) { project.creator }
context 'but gitlab instance is not configured for google oauth2' do
before do
it 'returns forbidden' do
unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)
end
it 'returns forbidden' do
sign_in(user)
get url
expect(response).to have_gitlab_http_status(:forbidden)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'google_oauth2_enabled!',
label: 'access_denied',
extra: { reason: 'google_oauth2_not_configured',
config: unconfigured_google_oauth2 },
project: project,
user: user
)
end
end
......@@ -101,6 +131,14 @@ RSpec.describe Projects::GoogleCloudController do
get url
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'feature_flag_enabled!',
label: 'access_denied',
property: 'feature_flag_not_enabled',
project: project,
user: user
)
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