diff --git a/lib/ci/job_token/jwt.rb b/lib/ci/job_token/jwt.rb index ab973de0ba79261428c69fdd27ee74313a9882d2..09a7c11e89b984a997161972fd86d483e54782b9 100644 --- a/lib/ci/job_token/jwt.rb +++ b/lib/ci/job_token/jwt.rb @@ -49,15 +49,19 @@ def decode(token) end def build_payload(job) - base_payload = { cell_id: Gitlab.config.cell.id } - base_payload.merge(extra_payload(job)).compact_blank + base_payload = { scoped_user_id: job.scoped_user&.id }.compact_blank + base_payload.merge(routable_payload(job)) end - def extra_payload(job) + # Creating routing information for routable tokens https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/routable_tokens/ + def routable_payload(job) { - scoped_user_id: job.scoped_user&.id, - organization_id: job.project.organization_id - } + c: Gitlab.config.cell.id, + o: job.project.organization_id, + u: job.user_id, + p: job.project_id, + g: job.project.group&.id + }.compact_blank.transform_values { |id| id.to_s(36) } end def token_prefix @@ -101,14 +105,30 @@ def scoped_user strong_memoize_attr :scoped_user def cell_id - @jwt.payload['cell_id'] + decode(@jwt.payload['c']) end - strong_memoize_attr :cell_id - def organization - job&.project&.organization + def organization_id + decode(@jwt.payload['o']) + end + + def project_id + decode(@jwt.payload['p']) + end + + def user_id + decode(@jwt.payload['u']) + end + + def group_id + decode(@jwt.payload['g']) + end + + private + + def decode(encoded_value) + encoded_value&.to_i(36) end - strong_memoize_attr :organization end end end diff --git a/spec/lib/ci/job_token/jwt_spec.rb b/spec/lib/ci/job_token/jwt_spec.rb index 77eb0d7f3b49a5942498552cb0cca1b6c1410b6b..99319389fbd634bb3dbfec5b62abc8cfeeabd227 100644 --- a/spec/lib/ci/job_token/jwt_spec.rb +++ b/spec/lib/ci/job_token/jwt_spec.rb @@ -6,6 +6,7 @@ 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) @@ -61,11 +62,38 @@ 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 @@ -184,17 +212,58 @@ 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(Gitlab.config.cell.id) + 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 '#organization' do + describe '#project_id' do let(:encoded_token) { described_class.encode(job) } let(:decoded_token) { described_class.decode(encoded_token) } - it 'encodes the organization in the JWT payload' do - expect(decoded_token.organization).to eq(job.project.organization) + 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