Commit 68d9fb11 authored by Ryan Cobb's avatar Ryan Cobb

Implement JWT for customers-dot proxy

Initial implementation of JWT for customers-dot graphql proxy.

Changelog: changed
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65204
parent 0ede1d4b
......@@ -451,6 +451,9 @@ class ApplicationSetting < ApplicationRecord
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
validates :customers_dot_jwt_signing_key,
rsa_key: true, allow_nil: true
validates :rate_limiting_response_text,
length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
allow_blank: true
......@@ -554,6 +557,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :slack_app_secret, encryption_options_base_32_aes_256_gcm
attr_encrypted :slack_app_verification_token, encryption_options_base_32_aes_256_gcm
attr_encrypted :ci_jwt_signing_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :customers_dot_jwt_signing_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_32_aes_256_gcm
attr_encrypted :cloud_license_auth_token, encryption_options_base_32_aes_256_gcm
attr_encrypted :external_pipeline_validation_service_token, encryption_options_base_32_aes_256_gcm
......
# frozen_string_literal: true
class AddCustomersDotJwtSigningKeyToApplicationSettings < ActiveRecord::Migration[6.1]
DOWNTIME = false
def change
add_column :application_settings, :encrypted_customers_dot_jwt_signing_key, :binary
add_column :application_settings, :encrypted_customers_dot_jwt_signing_key_iv, :binary
end
end
# frozen_string_literal: true
class GenerateCustomersDotJwtSigningKey < ActiveRecord::Migration[6.1]
DOWNTIME = false
class ApplicationSetting < ActiveRecord::Base
self.table_name = 'application_settings'
attr_encrypted :customers_dot_jwt_signing_key, {
mode: :per_attribute_iv,
key: Gitlab::Utils.ensure_utf8_size(Rails.application.secrets.db_key_base, bytes: 32.bytes),
algorithm: 'aes-256-gcm',
encode: true
}
end
def up
ApplicationSetting.reset_column_information
ApplicationSetting.find_each do |application_setting|
application_setting.update(customers_dot_jwt_signing_key: OpenSSL::PKey::RSA.new(2048).to_pem)
end
end
def down
ApplicationSetting.reset_column_information
ApplicationSetting.find_each do |application_setting|
application_setting.update_columns(encrypted_customers_dot_jwt_signing_key: nil, encrypted_customers_dot_jwt_signing_key_iv: nil)
end
end
end
6cd7654e53bb3dd75118dd399473c98e9953cbb28eaed7a4e3a232de38ca72d1
\ No newline at end of file
570edf634eba17e5c7d388fdf7103acb857e477374763205535e280f72050f71
\ No newline at end of file
......@@ -9550,6 +9550,8 @@ CREATE TABLE application_settings (
encrypted_mailgun_signing_key_iv bytea,
mailgun_events_enabled boolean DEFAULT false NOT NULL,
usage_ping_features_enabled boolean DEFAULT false NOT NULL,
encrypted_customers_dot_jwt_signing_key bytea,
encrypted_customers_dot_jwt_signing_key_iv bytea,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
......@@ -12,10 +12,19 @@ module CustomersDot
def graphql
response = Gitlab::HTTP.post("#{BASE_URL}/graphql",
body: request.raw_post,
headers: { 'Content-Type' => 'application/json' }
headers: forward_headers
)
render json: response.body, status: response.code
end
private
def forward_headers
{}.tap do |headers|
headers['Content-Type'] = 'application/json'
headers['Authorization'] = "Bearer #{Gitlab::CustomersDot::Jwt.new(current_user).encoded}" if current_user
end
end
end
end
# frozen_string_literal: true
module Gitlab
module CustomersDot
class Jwt
DEFAULT_EXPIRE_TIME = 60 * 10
NoSigningKeyError = Class.new(StandardError)
def initialize(user)
@user = user
end
def encoded
headers = { typ: 'JWT' }
JWT.encode(payload, key, 'RS256', headers)
end
def payload
now = Time.now.to_i
{
jti: SecureRandom.uuid,
iss: Settings.gitlab.host,
iat: now,
exp: now + DEFAULT_EXPIRE_TIME,
sub: "gitlab_user_id_#{user.id}"
}
end
private
attr_reader :user
def key
key_data = Gitlab::CurrentSettings.customers_dot_jwt_signing_key
raise NoSigningKeyError unless key_data
OpenSSL::PKey::RSA.new(key_data)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::CustomersDot::Jwt do
let_it_be(:user) { create(:user) }
subject(:customers_dot_jwt) { described_class.new(user) }
describe '#payload' do
subject(:payload) { customers_dot_jwt.payload }
it 'has correct values for JWT attributes' do
freeze_time do
now = Time.now.to_i
aggregate_failures do
expect(payload[:iss]).to eq(Settings.gitlab.host)
expect(payload[:iat]).to eq(now)
expect(payload[:exp]).to eq(now + Gitlab::CustomersDot::Jwt::DEFAULT_EXPIRE_TIME)
expect(payload[:sub]).to eq("gitlab_user_id_#{user.id}")
end
end
end
end
describe '#encoded' do
let(:key) { OpenSSL::PKey::RSA.new(2048) }
before do
allow(Gitlab::CurrentSettings).to receive(:customers_dot_jwt_signing_key).and_return(key)
end
subject(:encoded) { customers_dot_jwt.encoded }
it 'generates encoded token' do
expect(encoded).to be_a String
end
context 'with no signing key' do
let(:key) { nil }
it 'raises error' do
expect { encoded }.to raise_error(Gitlab::CustomersDot::Jwt::NoSigningKeyError)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CustomersDot::ProxyController, type: :request do
describe 'POST graphql' do
let_it_be(:customers_dot) { "#{Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL}/graphql" }
let_it_be(:default_headers) { { 'Content-Type' => 'application/json' } }
shared_examples 'customersdot proxy' do
it 'forwards request body to customers dot' do
request_params = '{ "foo" => "bar" }'
stub_request(:post, customers_dot)
post customers_dot_proxy_graphql_path, params: request_params
expect(WebMock).to have_requested(:post, customers_dot).with(body: request_params, headers: headers)
end
it 'responds with customers dot status' do
stub_request(:post, customers_dot).to_return(status: 500)
post customers_dot_proxy_graphql_path
expect(response).to have_gitlab_http_status(:internal_server_error)
end
it 'responds with customers dot response body' do
customers_dot_response = 'foo'
stub_request(:post, customers_dot).to_return(body: customers_dot_response)
post customers_dot_proxy_graphql_path
expect(response.body).to eq(customers_dot_response)
end
end
context 'with user signed in' do
let(:headers) { default_headers.merge(auth_header) }
let(:auth_header) { { 'Authorization' => "Bearer #{jwt}" } }
let(:jwt) { Gitlab::CustomersDot::Jwt.new(user).encoded }
let(:user) { create(:user) }
let(:rsa_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:jwt_jti) { 'jwt_jti' }
before do
stub_application_setting(customers_dot_jwt_signing_key: rsa_key.to_s )
allow(SecureRandom).to receive(:uuid).and_return(jwt_jti)
sign_in(user)
freeze_time
end
it_behaves_like 'customersdot proxy'
end
context 'with no user signed in' do
let(:headers) { default_headers }
it_behaves_like 'customersdot proxy'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe GenerateCustomersDotJwtSigningKey do
let(:application_settings) do
Class.new(ActiveRecord::Base) do
self.table_name = 'application_settings'
attr_encrypted :customers_dot_jwt_signing_key, {
mode: :per_attribute_iv,
key: Gitlab::Utils.ensure_utf8_size(Rails.application.secrets.db_key_base, bytes: 32.bytes),
algorithm: 'aes-256-gcm',
encode: true
}
end
end
it 'generates JWT signing key' do
application_settings.create!
reversible_migration do |migration|
migration.before -> {
settings = application_settings.first
expect(settings.customers_dot_jwt_signing_key).to be_nil
expect(settings.encrypted_customers_dot_jwt_signing_key).to be_nil
expect(settings.encrypted_customers_dot_jwt_signing_key_iv).to be_nil
}
migration.after -> {
settings = application_settings.first
expect(settings.encrypted_customers_dot_jwt_signing_key).to be_present
expect(settings.encrypted_customers_dot_jwt_signing_key_iv).to be_present
expect { OpenSSL::PKey::RSA.new(settings.customers_dot_jwt_signing_key) }.not_to raise_error
}
end
end
end
......@@ -834,6 +834,23 @@ RSpec.describe ApplicationSetting do
end
end
describe '#customers_dot_jwt_signing_key' do
it { is_expected.not_to allow_value('').for(:customers_dot_jwt_signing_key) }
it { is_expected.not_to allow_value('invalid RSA key').for(:customers_dot_jwt_signing_key) }
it { is_expected.to allow_value(nil).for(:customers_dot_jwt_signing_key) }
it { is_expected.to allow_value(OpenSSL::PKey::RSA.new(1024).to_pem).for(:customers_dot_jwt_signing_key) }
it 'is encrypted' do
subject.customers_dot_jwt_signing_key = OpenSSL::PKey::RSA.new(1024).to_pem
aggregate_failures do
expect(subject.encrypted_customers_dot_jwt_signing_key).to be_present
expect(subject.encrypted_customers_dot_jwt_signing_key_iv).to be_present
expect(subject.encrypted_customers_dot_jwt_signing_key).not_to eq(subject.customers_dot_jwt_signing_key)
end
end
end
describe '#cloud_license_auth_token' do
it { is_expected.to allow_value(nil).for(:cloud_license_auth_token) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CustomersDot::ProxyController, type: :request do
describe 'POST graphql' do
let_it_be(:customers_dot) { "#{Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL}/graphql" }
it 'forwards request body to customers dot' do
request_params = '{ "foo" => "bar" }'
stub_request(:post, customers_dot)
post customers_dot_proxy_graphql_path, params: request_params
expect(WebMock).to have_requested(:post, customers_dot).with(body: request_params)
end
it 'responds with customers dot status' do
stub_request(:post, customers_dot).to_return(status: 500)
post customers_dot_proxy_graphql_path
expect(response).to have_gitlab_http_status(:internal_server_error)
end
it 'responds with customers dot response body' do
customers_dot_response = 'foo'
stub_request(:post, customers_dot).to_return(body: customers_dot_response)
post customers_dot_proxy_graphql_path
expect(response.body).to eq(customers_dot_response)
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