Commit b4658940 authored by Sean McGivern's avatar Sean McGivern

Merge branch '9643-atlassian-jwt-client' into 'master'

Create client class for connecting to Jira API

Closes #9643

See merge request gitlab-org/gitlab-ee!14163
parents b4dfc57f 87173504
......@@ -9,9 +9,9 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
def show
render json: {
name: "GitLab for Jira (#{Gitlab.config.gitlab.host})",
name: Atlassian::JiraConnect.app_name,
description: 'Integrate commits, branches and merge requests from GitLab into Jira',
key: "gitlab-jira-connect-#{Gitlab.config.gitlab.host}",
key: Atlassian::JiraConnect.app_key,
baseUrl: jira_connect_base_url(protocol: 'https'),
lifecycle: {
installed: relative_to_base_path(jira_connect_events_installed_path),
......
......@@ -26,7 +26,7 @@ class JiraConnect::ApplicationController < ApplicationController
payload, _ = decode_auth_token!
# Make sure `qsh` claim matches the current request
render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.method, request.url, jira_connect_base_url)
render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.method, request.url, base_uri: jira_connect_base_url)
rescue
render_403
end
......
# frozen_string_literal: true
module Atlassian
module JiraConnect
class << self
def app_name
"GitLab for Jira (#{gitlab_host})"
end
def app_key
"gitlab-jira-connect-#{gitlab_host}"
end
private
def gitlab_host
Gitlab.config.gitlab.host
end
end
end
end
# frozen_string_literal: true
module Atlassian
module JiraConnect
class Client < Gitlab::HTTP
def initialize(base_uri, shared_secret)
@base_uri = base_uri
@shared_secret = shared_secret
end
def store_dev_info(dev_info_json)
uri = URI.join(@base_uri, '/rest/devinfo/0.10/bulk')
headers = {
'Authorization' => "JWT #{jwt_token('POST', uri)}",
'Content-Type' => 'application/json'
}
self.class.post(uri, headers: headers, body: dev_info_json)
end
private
def jwt_token(http_method, uri)
claims = Atlassian::Jwt.build_claims(
issuer: Atlassian::JiraConnect.app_key,
method: http_method,
uri: uri,
base_uri: @base_uri
)
Atlassian::Jwt.encode(claims, @shared_secret)
end
end
end
end
# frozen_string_literal: true
require 'digest'
# This is based on https://bitbucket.org/atlassian/atlassian-jwt-ruby
# which is unmaintained and incompatible with later versions of jwt-ruby
......@@ -18,12 +20,26 @@ module Atlassian
::JWT.encode(payload, secret, algorithm, header_fields)
end
def create_query_string_hash(http_method, uri, base_uri = '')
def create_query_string_hash(http_method, uri, base_uri: '')
Digest::SHA256.hexdigest(
create_canonical_request(http_method, uri, base_uri)
)
end
def build_claims(issuer:, method:, uri:, base_uri: '', issued_at: nil, expires: nil, other_claims: {})
issued_at ||= Time.now.to_i
expires ||= issued_at + 60
qsh = create_query_string_hash(method, uri, base_uri: base_uri)
{
iss: issuer,
iat: issued_at,
exp: expires,
qsh: qsh
}.merge(other_claims)
end
private
def create_canonical_request(http_method, uri, base_uri)
......
# frozen_string_literal: true
require 'spec_helper'
describe Atlassian::JiraConnect::Client do
include StubRequests
subject { described_class.new('https://gitlab-test.atlassian.net', 'sample_secret') }
around do |example|
Timecop.freeze { example.run }
end
describe '#store_dev_info' do
it "calls the API with auth headers" do
dev_info_json = {
repositories: [
name: 'atlassian-connect-jira-example'
]
}.to_json
expected_jwt = Atlassian::Jwt.encode(
Atlassian::Jwt.build_claims(
issuer: Atlassian::JiraConnect.app_key,
method: 'POST',
uri: '/rest/devinfo/0.10/bulk'
),
'sample_secret'
)
stub_full_request('https://gitlab-test.atlassian.net/rest/devinfo/0.10/bulk', method: :post)
.with(
body: dev_info_json,
headers: {
'Authorization' => "JWT #{expected_jwt}",
'Content-Type' => 'application/json'
}
)
subject.store_dev_info(dev_info_json)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require 'fast_spec_helper'
require 'rspec-parameterized'
require 'timecop'
describe Atlassian::Jwt do
describe '#create_query_string_hash' do
......@@ -18,7 +20,7 @@ describe Atlassian::Jwt do
with_them do
it 'generates correct hash with base URI' do
hash = subject.create_query_string_hash(method, base_uri + path, base_uri)
hash = subject.create_query_string_hash(method, base_uri + path, base_uri: base_uri)
expect(hash).to eq(expected_hash)
end
......@@ -30,4 +32,60 @@ describe Atlassian::Jwt do
end
end
end
describe '#build_claims' do
let(:other_options) { {} }
subject { described_class.build_claims(issuer: 'gitlab', method: 'post', uri: '/rest/devinfo/0.10/bulk', **other_options) }
it 'sets the iss claim' do
expect(subject[:iss]).to eq('gitlab')
end
it 'sets qsh claim based on HTTP method and path' do
expect(subject[:qsh]).to eq(described_class.create_query_string_hash('post', '/rest/devinfo/0.10/bulk'))
end
describe 'iat claim' do
it 'sets default value to current time' do
Timecop.freeze do
expect(subject[:iat]).to eq(Time.now.to_i)
end
end
context do
let(:issued_time) { Time.now + 30.days }
let(:other_options) { { issued_at: issued_time.to_i } }
it 'allows overriding with option' do
expect(subject[:iat]).to eq(issued_time.to_i)
end
end
end
describe 'exp claim' do
it 'sets default value to 1 minute from now' do
Timecop.freeze do
expect(subject[:exp]).to eq(Time.now.to_i + 60)
end
end
context do
let(:expiry_time) { Time.now + 30.days }
let(:other_options) { { expires: expiry_time.to_i } }
it 'allows overriding with option' do
expect(subject[:exp]).to eq(expiry_time.to_i)
end
end
end
describe 'other claims' do
let(:other_options) { { other_claims: { some_claim: 'some_claim_value' } } }
it 'allows adding of additional claims' do
expect(subject[:some_claim]).to eq('some_claim_value')
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