gitlab-ce/spec/lib/ci/job_token/jwt_spec.rb

279 lines
7.7 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::JobToken::Jwt, feature_category: :secrets_management do
let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
let_it_be(:user) { create(:user) }
let_it_be(:job) { create(:ci_build, user: user) }
let(:cell_id) { 1 }
before do
allow(Gitlab::CurrentSettings)
.to receive(:ci_job_token_signing_key)
.and_return(rsa_key.to_pem)
end
describe '.encode' do
subject(:encoded_token) { described_class.encode(job) }
context 'when all conditions are met' do
it 'returns a valid JWT token' do
expect(encoded_token).to be_present
expect(encoded_token).to start_with(Ci::Build::TOKEN_PREFIX)
end
end
context 'when job is not a Ci::Build' do
let(:job) { Object.new }
it { is_expected.to be_nil }
end
context 'when job is not persisted' do
let(:job) { build(:ci_build) }
it { is_expected.to be_nil }
end
context 'when signing key is not available' do
before do
allow(Gitlab::CurrentSettings)
.to receive(:ci_job_token_signing_key)
.and_return(nil)
end
it 'raises an error' do
expect { encoded_token }.to raise_error(RuntimeError, 'CI job token signing key is not set')
end
end
context 'when signing key results in error' do
before do
allow(described_class).to receive(:key).and_return(nil)
end
it { is_expected.to be_nil }
end
end
describe '.decode' do
let(:encoded_token) { described_class.encode(job) }
subject(:decoded_token) { described_class.decode(encoded_token) }
before do
allow(Gitlab.config.cell).to receive(:id).and_return(cell_id)
end
context 'with a valid token' do
let(:decoded_payload) { decoded_token.instance_variable_get(:@jwt).payload }
let(:expected_payload) do
{
"c" => cell_id.to_s(36),
"o" => job.project.organization_id.to_s(36),
"u" => user.id.to_s(36),
"p" => job.project_id.to_s(36)
}
end
it 'successfully decodes the token with subject' do
expect(decoded_token).to be_present
expect(decoded_token.job).to eq(job)
end
it 'successfully decodes the token with routable payload' do
expect(decoded_payload).to match(a_hash_including(expected_payload))
end
context 'when project belongs to a group' do
let_it_be(:job) { create(:ci_build, user: user, project: create(:project, :in_group)) }
it 'includes group id in routable payload' do
expect(decoded_payload)
.to match(a_hash_including(expected_payload.merge("g" => job.project.group.id.to_s(36))))
end
end
end
context 'when signing key is not available' do
before do
allow(Gitlab::CurrentSettings)
.to receive(:ci_job_token_signing_key)
.and_return(nil)
end
it 'raises an error' do
expect { decoded_token }.to raise_error(RuntimeError, 'CI job token signing key is not set')
end
end
context 'when signing key results in errors' do
before do
allow(described_class).to receive(:key).and_return(nil)
end
it { is_expected.to be_nil }
end
context 'when token is unknown' do
let(:encoded_token) { 'unknown-token' }
it { is_expected.to be_nil }
end
end
describe '.expire_time' do
subject(:expire_time) { described_class.expire_time(job) }
it 'returns expiration time with leeway' do
freeze_time do
allow(job).to receive(:metadata_timeout).and_return(2.hours)
expected_time = Time.current + 2.hours + described_class::LEEWAY
expect(expire_time).to eq(expected_time)
end
end
it 'uses default expire time when metadata_timeout is smaller' do
freeze_time do
allow(job).to receive(:metadata_timeout).and_return(1.minute)
expected_time = Time.current +
::JSONWebToken::Token::DEFAULT_EXPIRE_TIME +
described_class::LEEWAY
expect(expire_time).to eq(expected_time)
end
end
end
describe '.key' do
subject(:key) { described_class.key }
context 'with valid RSA key' do
it 'returns an RSA key instance' do
expect(key).to be_a(OpenSSL::PKey::RSA)
end
end
context 'with invalid RSA key' do
before do
allow(Gitlab::CurrentSettings)
.to receive(:ci_job_token_signing_key)
.and_return('invalid_key')
end
it 'returns nil and tracks error' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(instance_of(OpenSSL::PKey::RSAError))
expect(key).to be_nil
end
end
context 'when signing key is not set' do
before do
allow(Gitlab::CurrentSettings)
.to receive(:ci_job_token_signing_key)
.and_return(nil)
end
it 'raises an error' do
expect { key }
.to raise_error('CI job token signing key is not set')
end
end
end
describe '#scoped_user' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
let_it_be(:scoped_user) { create(:user) }
context 'when the job does not have scoped user' do
it 'does not encode the scoped user in the JWT payload' do
expect(decoded_token.scoped_user).to be_nil
end
end
context 'when the job has scoped user' do
before do
allow(job).to receive(:scoped_user).and_return(scoped_user)
end
it 'encodes the scoped user in the JWT payload' do
expect(decoded_token.scoped_user).to eq(scoped_user)
end
end
end
describe '#cell_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
before do
allow(Gitlab.config.cell).to receive(:id).and_return(cell_id)
end
it 'encodes the cell_id in the JWT payload' do
expect(decoded_token.cell_id).to eq(cell_id)
end
end
describe '#organization_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
it 'encodes the organization_id in the JWT payload' do
expect(decoded_token.organization_id).to eq(job.project.organization_id)
end
end
describe '#project_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
it 'encodes the project_id in the JWT payload' do
expect(decoded_token.project_id).to eq(job.project_id)
end
end
describe '#user_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
it 'encodes the user_id in the JWT payload' do
expect(decoded_token.user_id).to eq(job.user_id)
end
end
describe '#group_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
context 'when project belongs to a group' do
let_it_be(:job) { create(:ci_build, user: user, project: create(:project, :in_group)) }
it 'encodes the group_id in the JWT payload' do
expect(decoded_token.group_id).to eq(job.project.group.id)
end
end
context 'when project belongs to a personal namespace' do
it 'does not encode the group_id in the JWT payload' do
expect(decoded_token.group_id).to be_nil
end
end
end
describe '#job' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
it 'is encoded with the job as subject' do
expect(decoded_token.job).to eq(job)
end
end
end