summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-30 19:44:55 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-30 19:44:55 +0000
commitf526ce392a317feb5a125bb16adb2629a487ce70 (patch)
tree8ab2b3458181a6cce84b401ce7dc9326fc0f5384
parentf19a0fa10a0024fab5ef3c556612944f2a62c298 (diff)
downloadgitlab-ce-f526ce392a317feb5a125bb16adb2629a487ce70.tar.gz
Add latest changes from gitlab-org/security/gitlab@14-2-stable-ee
-rw-r--r--app/controllers/jira_connect/app_descriptor_controller.rb8
-rw-r--r--app/controllers/jira_connect/application_controller.rb24
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb6
-rw-r--r--app/models/jira_connect_installation.rb4
-rw-r--r--app/services/jira_connect_subscriptions/create_service.rb7
-rw-r--r--doc/integration/jira/connect-app.md7
-rw-r--r--lib/atlassian/jira_connect/client.rb20
-rw-r--r--lib/atlassian/jira_connect/jira_user.rb18
-rw-r--r--spec/controllers/jira_connect/app_descriptor_controller_spec.rb5
-rw-r--r--spec/controllers/jira_connect/subscriptions_controller_spec.rb37
-rw-r--r--spec/services/jira_connect_subscriptions/create_service_spec.rb28
11 files changed, 150 insertions, 14 deletions
diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb
index a0f387631dd..74fac6ff9bb 100644
--- a/app/controllers/jira_connect/app_descriptor_controller.rb
+++ b/app/controllers/jira_connect/app_descriptor_controller.rb
@@ -47,7 +47,13 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
postInstallPage: {
key: 'gitlab-configuration',
name: { value: 'GitLab Configuration' },
- url: relative_to_base_path(jira_connect_subscriptions_path)
+ url: relative_to_base_path(jira_connect_subscriptions_path),
+ conditions: [
+ {
+ condition: 'user_is_admin',
+ invert: false
+ }
+ ]
}
}
diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb
index a6529ecb4ce..352e78d6255 100644
--- a/app/controllers/jira_connect/application_controller.rb
+++ b/app/controllers/jira_connect/application_controller.rb
@@ -38,12 +38,30 @@ class JiraConnect::ApplicationController < ApplicationController
end
def installation_from_jwt
- return unless auth_token
-
strong_memoize(:installation_from_jwt) do
+ next unless claims['iss']
+
+ JiraConnectInstallation.find_by_client_key(claims['iss'])
+ end
+ end
+
+ def claims
+ strong_memoize(:claims) do
+ next {} unless auth_token
+
# Decode without verification to get `client_key` in `iss`
payload, _ = Atlassian::Jwt.decode(auth_token, nil, false)
- JiraConnectInstallation.find_by_client_key(payload['iss'])
+ payload
+ end
+ end
+
+ def jira_user
+ strong_memoize(:jira_user) do
+ next unless installation_from_jwt
+ next unless claims['sub']
+
+ # This only works for Jira Cloud installations.
+ installation_from_jwt.client.user_info(claims['sub'])
end
end
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
index a9c4dbf2b17..903ad395e44 100644
--- a/app/controllers/jira_connect/subscriptions_controller.rb
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -44,7 +44,9 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
def destroy
subscription = current_jira_installation.subscriptions.find(params[:id])
- if subscription.destroy
+ if !jira_user&.site_admin?
+ render json: { error: 'forbidden' }, status: :forbidden
+ elsif subscription.destroy
render json: { success: true }
else
render json: { error: subscription.errors.full_messages.join(', ') }, status: :unprocessable_entity
@@ -54,7 +56,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
private
def create_service
- JiraConnectSubscriptions::CreateService.new(current_jira_installation, current_user, namespace_path: params['namespace_path'])
+ JiraConnectSubscriptions::CreateService.new(current_jira_installation, current_user, namespace_path: params['namespace_path'], jira_user: jira_user)
end
def allow_rendering_in_iframe
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
index 759d44fb29e..e34543534f3 100644
--- a/app/models/jira_connect_installation.rb
+++ b/app/models/jira_connect_installation.rb
@@ -20,4 +20,8 @@ class JiraConnectInstallation < ApplicationRecord
id: JiraConnectSubscription.for_project(project)
})
}
+
+ def client
+ Atlassian::JiraConnect::Client.new(base_url, shared_secret)
+ end
end
diff --git a/app/services/jira_connect_subscriptions/create_service.rb b/app/services/jira_connect_subscriptions/create_service.rb
index 38e5fe7e690..2f31a3c8d4e 100644
--- a/app/services/jira_connect_subscriptions/create_service.rb
+++ b/app/services/jira_connect_subscriptions/create_service.rb
@@ -5,8 +5,11 @@ module JiraConnectSubscriptions
include Gitlab::Utils::StrongMemoize
MERGE_REQUEST_SYNC_BATCH_SIZE = 20
MERGE_REQUEST_SYNC_BATCH_DELAY = 1.minute.freeze
+ NOT_SITE_ADMIN = 'The Jira user is not a site administrator.'
def execute
+ return error(NOT_SITE_ADMIN, 403) unless can_administer_jira?
+
unless namespace && can?(current_user, :create_jira_connect_subscription, namespace)
return error('Invalid namespace. Please make sure you have sufficient permissions', 401)
end
@@ -16,6 +19,10 @@ module JiraConnectSubscriptions
private
+ def can_administer_jira?
+ @params[:jira_user]&.site_admin?
+ end
+
def create_subscription
subscription = JiraConnectSubscription.new(installation: jira_connect_installation, namespace: namespace)
diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md
index d8b1e9aa867..e32bd4559f9 100644
--- a/doc/integration/jira/connect-app.md
+++ b/doc/integration/jira/connect-app.md
@@ -6,6 +6,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# GitLab.com for Jira Cloud app **(FREE)**
+NOTE:
+Only Jira users with administrator level access are able to install or configure
+the GitLab app for Jira Cloud.
+
## GitLab.com for Jira Cloud app **(FREE SAAS)**
You can integrate GitLab.com and Jira Cloud using the
@@ -39,7 +43,8 @@ For a walkthrough of the integration with GitLab.com for Jira Cloud app, watch
![Sign in to GitLab.com in GitLab.com for Jira Cloud app](img/jira_dev_panel_setup_com_3_v13_9.png)
1. Select **Add namespace** to open the list of available namespaces.
-1. Identify the namespace you want to link, and select **Link**.
+1. Identify the namespace you want to link, and select **Link**. Only Jira site
+ administrators are permitted to add or remove namespaces for an installation.
![Link namespace in GitLab.com for Jira Cloud app](img/jira_dev_panel_setup_com_4_v13_9.png)
diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb
index 3e2e6f1b9ba..dc37465744b 100644
--- a/lib/atlassian/jira_connect/client.rb
+++ b/lib/atlassian/jira_connect/client.rb
@@ -30,8 +30,21 @@ module Atlassian
responses.compact
end
+ def user_info(account_id)
+ r = get('/rest/api/3/user', { accountId: account_id, expand: 'groups' })
+
+ JiraUser.new(r.parsed_response) if r.code == 200
+ end
+
private
+ def get(path, query_params)
+ uri = URI.join(@base_uri, path)
+ uri.query = URI.encode_www_form(query_params)
+
+ self.class.get(uri, headers: headers(uri, 'GET'))
+ end
+
def store_ff_info(project:, feature_flags:, **opts)
items = feature_flags.map { |flag| ::Atlassian::JiraConnect::Serializers::FeatureFlagEntity.represent(flag, opts) }
items.reject! { |item| item.issue_keys.empty? }
@@ -99,10 +112,11 @@ module Atlassian
self.class.post(uri, headers: headers(uri), body: metadata.merge(payload).to_json)
end
- def headers(uri)
+ def headers(uri, http_method = 'POST')
{
- 'Authorization' => "JWT #{jwt_token('POST', uri)}",
- 'Content-Type' => 'application/json'
+ 'Authorization' => "JWT #{jwt_token(http_method, uri)}",
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json'
}
end
diff --git a/lib/atlassian/jira_connect/jira_user.rb b/lib/atlassian/jira_connect/jira_user.rb
new file mode 100644
index 00000000000..57ceb8fdf13
--- /dev/null
+++ b/lib/atlassian/jira_connect/jira_user.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ class JiraUser
+ def initialize(data)
+ @data = data
+ end
+
+ def site_admin?
+ groups = @data.dig('groups', 'items')
+ return false unless groups
+
+ groups.any? { |g| g['name'] == 'site-admins' }
+ end
+ end
+ end
+end
diff --git a/spec/controllers/jira_connect/app_descriptor_controller_spec.rb b/spec/controllers/jira_connect/app_descriptor_controller_spec.rb
index 98f4db13a1d..25c11d92b4e 100644
--- a/spec/controllers/jira_connect/app_descriptor_controller_spec.rb
+++ b/spec/controllers/jira_connect/app_descriptor_controller_spec.rb
@@ -54,7 +54,10 @@ RSpec.describe JiraConnect::AppDescriptorController do
postInstallPage: {
key: 'gitlab-configuration',
name: { value: 'GitLab Configuration' },
- url: '/subscriptions'
+ url: '/subscriptions',
+ conditions: contain_exactly(
+ a_hash_including(condition: 'user_is_admin', invert: false)
+ )
},
jiraDevelopmentTool: {
actions: {
diff --git a/spec/controllers/jira_connect/subscriptions_controller_spec.rb b/spec/controllers/jira_connect/subscriptions_controller_spec.rb
index e32915d55a1..f548c1f399d 100644
--- a/spec/controllers/jira_connect/subscriptions_controller_spec.rb
+++ b/spec/controllers/jira_connect/subscriptions_controller_spec.rb
@@ -102,11 +102,17 @@ RSpec.describe JiraConnect::SubscriptionsController do
end
context 'with valid JWT' do
- let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key }, installation.shared_secret) }
+ let(:claims) { { iss: installation.client_key, sub: 1234 } }
+ let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) }
+ let(:jira_user) { { 'groups' => { 'items' => [{ 'name' => jira_group_name }] } } }
+ let(:jira_group_name) { 'site-admins' }
context 'signed in to GitLab' do
before do
sign_in(user)
+ WebMock
+ .stub_request(:get, "#{installation.base_url}/rest/api/3/user?accountId=1234&expand=groups")
+ .to_return(body: jira_user.to_json, status: 200, headers: { 'Content-Type' => 'application/json' })
end
context 'dev panel integration is available' do
@@ -120,6 +126,16 @@ RSpec.describe JiraConnect::SubscriptionsController do
expect(response).to have_gitlab_http_status(:ok)
end
end
+
+ context 'when the Jira user is not a site-admin' do
+ let(:jira_group_name) { 'some-other-group' }
+
+ it 'returns forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
end
context 'not signed in to GitLab' do
@@ -134,8 +150,14 @@ RSpec.describe JiraConnect::SubscriptionsController do
describe '#destroy' do
let(:subscription) { create(:jira_connect_subscription, installation: installation) }
+ let(:jira_user) { { 'groups' => { 'items' => [{ 'name' => jira_group_name }] } } }
+ let(:jira_group_name) { 'site-admins' }
before do
+ WebMock
+ .stub_request(:get, "#{installation.base_url}/rest/api/3/user?accountId=1234&expand=groups")
+ .to_return(body: jira_user.to_json, status: 200, headers: { 'Content-Type' => 'application/json' })
+
delete :destroy, params: { jwt: jwt, id: subscription.id }
end
@@ -148,12 +170,23 @@ RSpec.describe JiraConnect::SubscriptionsController do
end
context 'with valid JWT' do
- let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key }, installation.shared_secret) }
+ let(:claims) { { iss: installation.client_key, sub: 1234 } }
+ let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) }
it 'deletes the subscription' do
expect { subscription.reload }.to raise_error ActiveRecord::RecordNotFound
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'when the Jira user is not a site admin' do
+ let(:jira_group_name) { 'some-other-group' }
+
+ it 'does not delete the subscription' do
+ expect(response).to have_gitlab_http_status(:forbidden)
+
+ expect { subscription.reload }.not_to raise_error
+ end
+ end
end
end
end
diff --git a/spec/services/jira_connect_subscriptions/create_service_spec.rb b/spec/services/jira_connect_subscriptions/create_service_spec.rb
index 5f467a07a78..cde4753cde7 100644
--- a/spec/services/jira_connect_subscriptions/create_service_spec.rb
+++ b/spec/services/jira_connect_subscriptions/create_service_spec.rb
@@ -7,8 +7,10 @@ RSpec.describe JiraConnectSubscriptions::CreateService do
let(:current_user) { create(:user) }
let(:group) { create(:group) }
let(:path) { group.full_path }
+ let(:params) { { namespace_path: path, jira_user: jira_user } }
+ let(:jira_user) { double(:JiraUser, site_admin?: true) }
- subject { described_class.new(installation, current_user, namespace_path: path).execute }
+ subject { described_class.new(installation, current_user, params).execute }
before do
group.add_maintainer(current_user)
@@ -24,6 +26,30 @@ RSpec.describe JiraConnectSubscriptions::CreateService do
end
end
+ context 'remote user does not have access' do
+ let(:jira_user) { double(site_admin?: false) }
+
+ it 'does not create a subscription' do
+ expect { subject }.not_to change { installation.subscriptions.count }
+ end
+
+ it 'returns error' do
+ expect(subject[:status]).to eq(:error)
+ end
+ end
+
+ context 'remote user cannot be retrieved' do
+ let(:jira_user) { nil }
+
+ it 'does not create a subscription' do
+ expect { subject }.not_to change { installation.subscriptions.count }
+ end
+
+ it 'returns error' do
+ expect(subject[:status]).to eq(:error)
+ end
+ end
+
context 'when user does have access' do
it 'creates a subscription' do
expect { subject }.to change { installation.subscriptions.count }.from(0).to(1)