Commit 4140f6b0 authored by Steve Abrams's avatar Steve Abrams

Allow CI_JOB_TOKENs for Conan packages

Allow job tokens for conan api endpoints
This includes an override of
find_user_from_job_token in conan_packages
parent 978c3763
---
title: Allow CI_JOB_TOKENS for Conan package registry authentication
merge_request: 22184
author:
type: added
...@@ -263,3 +263,25 @@ The GitLab Conan repository supports the following Conan CLI commands: ...@@ -263,3 +263,25 @@ The GitLab Conan repository supports the following Conan CLI commands:
- `conan search`: Search the GitLab Package Registry for public packages, and private packages you have permission to view. - `conan search`: Search the GitLab Package Registry for public packages, and private packages you have permission to view.
- `conan info`: View the info on a given package from the GitLab Package Registry. - `conan info`: View the info on a given package from the GitLab Package Registry.
- `conan remove`: Delete the package from the GitLab Package Registry. - `conan remove`: Delete the package from the GitLab Package Registry.
## Using GitLab CI with Conan packages
To work with Conan commands within [GitLab CI](./../../../ci/README.md), you can use
`CI_JOB_TOKEN` in place of the personal access token in your commands.
It is easiest to provide the `CONAN_LOGIN_USERNAME` and `CONAN_PASSWORD` with each
Conan command in your `.gitlab-ci.yml` file:
```yml
image: conanio/gcc7
create_package:
stage: deploy
script:
- conan remote add gitlab https://gitlab.example.com/api/v4/packages/conan
- conan create . my-group+my-project/beta
- CONAN_LOGIN_USERNAME=ci_user CONAN_PASSWORD=${CI_JOB_TOKEN} conan upload Hello/0.1@root+ci-conan/beta1 --all --remote=gitlab
```
You can find additional Conan images to use as the base of your CI file
in the [Conan docs](https://docs.conan.io/en/latest/howtos/run_conan_in_docker.html#available-docker-images).
...@@ -30,7 +30,7 @@ module API ...@@ -30,7 +30,7 @@ module API
require_packages_enabled! require_packages_enabled!
# Personal access token will be extracted from Bearer or Basic authorization # Personal access token will be extracted from Bearer or Basic authorization
# in the overridden find_personal_access_token helper # in the overridden find_personal_access_token or find_user_from_job_token helpers
authenticate! authenticate!
end end
...@@ -38,6 +38,7 @@ module API ...@@ -38,6 +38,7 @@ module API
desc 'Ping the Conan API' do desc 'Ping the Conan API' do
detail 'This feature was introduced in GitLab 12.2' detail 'This feature was introduced in GitLab 12.2'
end end
route_setting :authentication, job_token_allowed: true
get 'ping' do get 'ping' do
header 'X-Conan-Server-Capabilities', [].join(',') header 'X-Conan-Server-Capabilities', [].join(',')
end end
...@@ -48,6 +49,7 @@ module API ...@@ -48,6 +49,7 @@ module API
params do params do
requires :q, type: String, desc: 'Search query' requires :q, type: String, desc: 'Search query'
end end
route_setting :authentication, job_token_allowed: true
get 'conans/search' do get 'conans/search' do
service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute
service.payload service.payload
...@@ -59,14 +61,21 @@ module API ...@@ -59,14 +61,21 @@ module API
desc 'Authenticate user against conan CLI' do desc 'Authenticate user against conan CLI' do
detail 'This feature was introduced in GitLab 12.2' detail 'This feature was introduced in GitLab 12.2'
end end
route_setting :authentication, job_token_allowed: true
get 'authenticate' do get 'authenticate' do
token = ::Gitlab::ConanToken.from_personal_access_token(access_token) token = if access_token
::Gitlab::ConanToken.from_personal_access_token(access_token)
else
::Gitlab::ConanToken.from_job(find_job_from_token)
end
token.to_jwt token.to_jwt
end end
desc 'Check for valid user credentials per conan CLI' do desc 'Check for valid user credentials per conan CLI' do
detail 'This feature was introduced in GitLab 12.4' detail 'This feature was introduced in GitLab 12.4'
end end
route_setting :authentication, job_token_allowed: true
get 'check_credentials' do get 'check_credentials' do
authenticate! authenticate!
:ok :ok
...@@ -88,6 +97,7 @@ module API ...@@ -88,6 +97,7 @@ module API
desc 'Package Snapshot' do desc 'Package Snapshot' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
end end
route_setting :authentication, job_token_allowed: true
get 'packages/:conan_package_reference' do get 'packages/:conan_package_reference' do
authorize!(:read_package, project) authorize!(:read_package, project)
...@@ -99,6 +109,7 @@ module API ...@@ -99,6 +109,7 @@ module API
desc 'Recipe Snapshot' do desc 'Recipe Snapshot' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
end end
route_setting :authentication, job_token_allowed: true
get do get do
authorize!(:read_package, project) authorize!(:read_package, project)
...@@ -115,6 +126,7 @@ module API ...@@ -115,6 +126,7 @@ module API
desc 'Package Digest' do desc 'Package Digest' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
end end
route_setting :authentication, job_token_allowed: true
get 'packages/:conan_package_reference/digest' do get 'packages/:conan_package_reference/digest' do
present_package_download_urls present_package_download_urls
end end
...@@ -122,6 +134,7 @@ module API ...@@ -122,6 +134,7 @@ module API
desc 'Recipe Digest' do desc 'Recipe Digest' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
end end
route_setting :authentication, job_token_allowed: true
get 'digest' do get 'digest' do
present_recipe_download_urls present_recipe_download_urls
end end
...@@ -135,6 +148,7 @@ module API ...@@ -135,6 +148,7 @@ module API
desc 'Package Download Urls' do desc 'Package Download Urls' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
end end
route_setting :authentication, job_token_allowed: true
get 'packages/:conan_package_reference/download_urls' do get 'packages/:conan_package_reference/download_urls' do
present_package_download_urls present_package_download_urls
end end
...@@ -142,6 +156,7 @@ module API ...@@ -142,6 +156,7 @@ module API
desc 'Recipe Download Urls' do desc 'Recipe Download Urls' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
end end
route_setting :authentication, job_token_allowed: true
get 'download_urls' do get 'download_urls' do
present_recipe_download_urls present_recipe_download_urls
end end
...@@ -159,6 +174,7 @@ module API ...@@ -159,6 +174,7 @@ module API
params do params do
requires :conan_package_reference, type: String, desc: 'Conan package ID' requires :conan_package_reference, type: String, desc: 'Conan package ID'
end end
route_setting :authentication, job_token_allowed: true
post 'packages/:conan_package_reference/upload_urls' do post 'packages/:conan_package_reference/upload_urls' do
authorize!(:read_package, project) authorize!(:read_package, project)
...@@ -171,6 +187,7 @@ module API ...@@ -171,6 +187,7 @@ module API
desc 'Recipe Upload Urls' do desc 'Recipe Upload Urls' do
detail 'This feature was introduced in GitLab 12.4' detail 'This feature was introduced in GitLab 12.4'
end end
route_setting :authentication, job_token_allowed: true
post 'upload_urls' do post 'upload_urls' do
authorize!(:read_package, project) authorize!(:read_package, project)
...@@ -183,6 +200,7 @@ module API ...@@ -183,6 +200,7 @@ module API
desc 'Delete Package' do desc 'Delete Package' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
end end
route_setting :authentication, job_token_allowed: true
delete do delete do
authorize!(:destroy_package, project) authorize!(:destroy_package, project)
...@@ -211,6 +229,7 @@ module API ...@@ -211,6 +229,7 @@ module API
desc 'Download recipe files' do desc 'Download recipe files' do
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
route_setting :authentication, job_token_allowed: true
get do get do
download_package_file(:recipe_file) download_package_file(:recipe_file)
end end
...@@ -221,6 +240,7 @@ module API ...@@ -221,6 +240,7 @@ module API
params do params do
use :workhorse_upload_params use :workhorse_upload_params
end end
route_setting :authentication, job_token_allowed: true
put do put do
upload_package_file(:recipe_file) upload_package_file(:recipe_file)
end end
...@@ -228,6 +248,7 @@ module API ...@@ -228,6 +248,7 @@ module API
desc 'Workhorse authorize the conan recipe file' do desc 'Workhorse authorize the conan recipe file' do
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
route_setting :authentication, job_token_allowed: true
put 'authorize' do put 'authorize' do
authorize_workhorse!(project) authorize_workhorse!(project)
end end
...@@ -242,6 +263,7 @@ module API ...@@ -242,6 +263,7 @@ module API
desc 'Download package files' do desc 'Download package files' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
end end
route_setting :authentication, job_token_allowed: true
get do get do
download_package_file(:package_file) download_package_file(:package_file)
end end
...@@ -249,6 +271,7 @@ module API ...@@ -249,6 +271,7 @@ module API
desc 'Workhorse authorize the conan package file' do desc 'Workhorse authorize the conan package file' do
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
route_setting :authentication, job_token_allowed: true
put 'authorize' do put 'authorize' do
authorize_workhorse!(project) authorize_workhorse!(project)
end end
...@@ -259,6 +282,7 @@ module API ...@@ -259,6 +282,7 @@ module API
params do params do
use :workhorse_upload_params use :workhorse_upload_params
end end
route_setting :authentication, job_token_allowed: true
put do put do
upload_package_file(:package_file) upload_package_file(:package_file)
end end
...@@ -386,7 +410,21 @@ module API ...@@ -386,7 +410,21 @@ module API
personal_access_token = find_personal_access_token_from_conan_jwt || personal_access_token = find_personal_access_token_from_conan_jwt ||
find_personal_access_token_from_http_basic_auth find_personal_access_token_from_http_basic_auth
personal_access_token || unauthorized! personal_access_token
end
def find_user_from_job_token
return unless route_authentication_setting[:job_token_allowed]
job = find_job_from_token
raise ::Gitlab::Auth::UnauthorizedError unless job
job.user
end
def find_job_from_token
find_job_from_conan_jwt || find_job_from_http_basic_auth
end end
# We need to override this one because it # We need to override this one because it
...@@ -395,13 +433,31 @@ module API ...@@ -395,13 +433,31 @@ module API
end end
def find_personal_access_token_from_conan_jwt def find_personal_access_token_from_conan_jwt
token = decode_oauth_token_from_jwt
return unless token
PersonalAccessToken.find_by_id_and_user_id(token.access_token_id, token.user_id)
end
def find_job_from_conan_jwt
token = decode_oauth_token_from_jwt
return unless token
::Ci::Build.find_by_token(token.access_token_id.to_s)
end
def decode_oauth_token_from_jwt
jwt = Doorkeeper::OAuth::Token.from_bearer_authorization(current_request) jwt = Doorkeeper::OAuth::Token.from_bearer_authorization(current_request)
return unless jwt return unless jwt
token = ::Gitlab::ConanToken.decode(jwt) token = ::Gitlab::ConanToken.decode(jwt)
return unless token&.personal_access_token_id && token&.user_id
PersonalAccessToken.find_by_id_and_user_id(token.personal_access_token_id, token.user_id) return unless token && token.access_token_id && token.user_id
token
end end
end end
end end
......
...@@ -19,19 +19,35 @@ module API ...@@ -19,19 +19,35 @@ module API
def find_personal_access_token_from_http_basic_auth def find_personal_access_token_from_http_basic_auth
return unless headers return unless headers
encoded_credentials = headers['Authorization'].to_s.split('Basic ', 2).second token = decode_token
token = Base64.decode64(encoded_credentials || '').split(':', 2).second
return unless token return unless token
PersonalAccessToken.find_by_token(token) PersonalAccessToken.find_by_token(token)
end end
def find_job_from_http_basic_auth
return unless headers
token = decode_token
return unless token
::Ci::Build.find_by_token(token)
end
def uploaded_package_file def uploaded_package_file
uploaded_file = UploadedFile.from_params(params, :file, ::Packages::PackageFileUploader.workhorse_local_upload_path) uploaded_file = UploadedFile.from_params(params, :file, ::Packages::PackageFileUploader.workhorse_local_upload_path)
bad_request!('Missing package file!') unless uploaded_file bad_request!('Missing package file!') unless uploaded_file
uploaded_file uploaded_file
end end
private
def decode_token
encoded_credentials = headers['Authorization'].to_s.split('Basic ', 2).second
Base64.decode64(encoded_credentials || '').split(':', 2).second
end
end end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
# The Conan client uses a JWT for authenticating with remotes.
# This class encodes and decodes a user's personal access token or
# CI_JOB_TOKEN into a JWT that is used by the Conan client to
# authenticate with GitLab
module Gitlab module Gitlab
class ConanToken class ConanToken
HMAC_KEY = 'gitlab-conan-packages'.freeze HMAC_KEY = 'gitlab-conan-packages'.freeze
attr_reader :personal_access_token_id, :user_id attr_reader :access_token_id, :user_id
class << self class << self
def from_personal_access_token(personal_access_token) def from_personal_access_token(access_token)
new(personal_access_token_id: personal_access_token.id, user_id: personal_access_token.user_id) new(access_token_id: access_token.id, user_id: access_token.user_id)
end
def from_job(job)
new(access_token_id: job.token, user_id: job.user.id)
end end
def decode(jwt) def decode(jwt)
payload = JSONWebToken::HMACToken.decode(jwt, secret).first payload = JSONWebToken::HMACToken.decode(jwt, secret).first
new(personal_access_token_id: payload['pat'], user_id: payload['u']) new(access_token_id: payload['access_token'], user_id: payload['user_id'])
rescue JWT::DecodeError rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
# we return on expired and errored tokens because the Conan client
# will request a new token automatically.
end end
def secret def secret
...@@ -27,8 +38,8 @@ module Gitlab ...@@ -27,8 +38,8 @@ module Gitlab
end end
end end
def initialize(personal_access_token_id:, user_id:) def initialize(access_token_id:, user_id:)
@personal_access_token_id = personal_access_token_id @access_token_id = access_token_id
@user_id = user_id @user_id = user_id
end end
...@@ -40,8 +51,8 @@ module Gitlab ...@@ -40,8 +51,8 @@ module Gitlab
def hmac_token def hmac_token
JSONWebToken::HMACToken.new(self.class.secret).tap do |token| JSONWebToken::HMACToken.new(self.class.secret).tap do |token|
token['pat'] = personal_access_token_id token['access_token'] = access_token_id
token['u'] = user_id token['user_id'] = user_id
token.expire_time = token.issued_at + 1.hour token.expire_time = token.issued_at + 1.hour
end end
end end
......
...@@ -42,6 +42,44 @@ describe API::Helpers::PackagesManagerClientsHelpers do ...@@ -42,6 +42,44 @@ describe API::Helpers::PackagesManagerClientsHelpers do
end end
end end
describe '#find_job_from_http_basic_auth' do
let_it_be(:user) { personal_access_token.user }
let(:job) { create(:ci_build, user: user) }
let(:password) { job.token }
let(:headers) { { Authorization: basic_http_auth(username, password) } }
subject { helper.find_job_from_http_basic_auth }
before do
allow(helper).to receive(:headers).and_return(headers&.with_indifferent_access)
end
context 'with a valid Authorization header' do
it { is_expected.to eq job }
end
context 'with an invalid Authorization header' do
where(:headers) do
[
[{ Authorization: 'Invalid' }],
[{}],
[nil]
]
end
with_them do
it { is_expected.to be nil }
end
end
context 'with an unknown Authorization header' do
let(:password) { 'Unknown' }
it { is_expected.to be nil }
end
end
describe '#uploaded_package_file' do describe '#uploaded_package_file' do
let_it_be(:params) { {} } let_it_be(:params) { {} }
......
...@@ -16,38 +16,58 @@ describe Gitlab::ConanToken do ...@@ -16,38 +16,58 @@ describe Gitlab::ConanToken do
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret) allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
end end
def build_jwt(personal_access_token_id:, user_id:) def build_jwt(access_token_id:, user_id:, expire_time: nil)
JSONWebToken::HMACToken.new(jwt_secret).tap do |jwt| JSONWebToken::HMACToken.new(jwt_secret).tap do |jwt|
jwt['pat'] = personal_access_token_id jwt['access_token'] = access_token_id
jwt['u'] = user_id || user_id jwt['user_id'] = user_id || user_id
jwt.expire_time = jwt.issued_at + 1.hour jwt.expire_time = expire_time || jwt.issued_at + 1.hour
end end
end end
describe '.from_personal_access_token' do describe '.from_personal_access_token' do
it 'sets personal access token id and user id' do it 'sets access token id and user id' do
personal_access_token = double(id: 123, user_id: 456) access_token = double(id: 123, user_id: 456)
token = described_class.from_personal_access_token(personal_access_token) token = described_class.from_personal_access_token(access_token)
expect(token.personal_access_token_id).to eq(123) expect(token.access_token_id).to eq(123)
expect(token.user_id).to eq(456)
end
end
describe '.from_job' do
it 'sets access token id and user id' do
user = double(id: 456)
job = double(token: 123, user: user)
token = described_class.from_job(job)
expect(token.access_token_id).to eq(123)
expect(token.user_id).to eq(456) expect(token.user_id).to eq(456)
end end
end end
describe '.decode' do describe '.decode' do
it 'sets personal access token id and user id' do it 'sets access token id and user id' do
jwt = build_jwt(personal_access_token_id: 123, user_id: 456) jwt = build_jwt(access_token_id: 123, user_id: 456)
token = described_class.decode(jwt.encoded) token = described_class.decode(jwt.encoded)
expect(token.personal_access_token_id).to eq(123) expect(token.access_token_id).to eq(123)
expect(token.user_id).to eq(456) expect(token.user_id).to eq(456)
end end
it 'returns nil for invalid JWT' do it 'returns nil for invalid JWT' do
expect(described_class.decode('invalid-jwt')).to be_nil expect(described_class.decode('invalid-jwt')).to be_nil
end end
it 'returns nil for expired JWT' do
jwt = build_jwt(access_token_id: 123,
user_id: 456,
expire_time: Time.zone.now - 2.hours)
expect(described_class.decode(jwt.encoded)).to be_nil
end
end end
describe '#to_jwt' do describe '#to_jwt' do
...@@ -55,9 +75,9 @@ describe Gitlab::ConanToken do ...@@ -55,9 +75,9 @@ describe Gitlab::ConanToken do
allow(SecureRandom).to receive(:uuid).and_return('u-u-i-d') allow(SecureRandom).to receive(:uuid).and_return('u-u-i-d')
Timecop.freeze do Timecop.freeze do
jwt = build_jwt(personal_access_token_id: 123, user_id: 456) jwt = build_jwt(access_token_id: 123, user_id: 456)
token = described_class.new(personal_access_token_id: 123, user_id: 456) token = described_class.new(access_token_id: 123, user_id: 456)
expect(token.to_jwt).to eq(jwt.encoded) expect(token.to_jwt).to eq(jwt.encoded)
end end
......
...@@ -12,6 +12,8 @@ describe API::ConanPackages do ...@@ -12,6 +12,8 @@ describe API::ConanPackages do
let(:base_secret) { SecureRandom.base64(64) } let(:base_secret) { SecureRandom.base64(64) }
let(:auth_token) { personal_access_token.token } let(:auth_token) { personal_access_token.token }
let(:job) { create(:ci_build, user: user) }
let(:job_token) { job.token }
let(:headers) do let(:headers) do
{ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) } { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) }
...@@ -46,6 +48,14 @@ describe API::ConanPackages do ...@@ -46,6 +48,14 @@ describe API::ConanPackages do
expect(response.headers['X-Conan-Server-Capabilities']).to eq("") expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
end end
it 'responds with 200 OK when valid job token is provided' do
jwt = build_jwt_from_job(job)
get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
end
it 'responds with 401 Unauthorized when invalid access token ID is provided' do it 'responds with 401 Unauthorized when invalid access token ID is provided' do
jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id) jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
...@@ -137,22 +147,42 @@ describe API::ConanPackages do ...@@ -137,22 +147,42 @@ describe API::ConanPackages do
payload = JSONWebToken::HMACToken.decode( payload = JSONWebToken::HMACToken.decode(
response.body, jwt_secret).first response.body, jwt_secret).first
expect(payload['pat']).to eq(personal_access_token.id) expect(payload['access_token']).to eq(personal_access_token.id)
expect(payload['u']).to eq(personal_access_token.user_id) expect(payload['user_id']).to eq(personal_access_token.user_id)
duration = payload['exp'] - payload['iat'] duration = payload['exp'] - payload['iat']
expect(duration).to eq(1.hour) expect(duration).to eq(1.hour)
end end
end end
end end
context 'with valid job token' do
let(:auth_token) { job_token }
it 'responds with 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
end end
describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do
it 'responds with a 200 OK' do it 'responds with a 200 OK with PAT' do
get api('/packages/conan/v1/users/check_credentials'), headers: headers
expect(response).to have_gitlab_http_status(:ok)
end
context 'with job token' do
let(:auth_token) { job_token }
it 'responds with a 200 OK with job token' do
get api('/packages/conan/v1/users/check_credentials'), headers: headers get api('/packages/conan/v1/users/check_credentials'), headers: headers
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
end
it 'responds with a 401 Unauthorized when an invalid token is used' do it 'responds with a 401 Unauthorized when an invalid token is used' do
get api('/packages/conan/v1/users/check_credentials'), headers: build_token_auth_header('invalid-token') get api('/packages/conan/v1/users/check_credentials'), headers: build_token_auth_header('invalid-token')
......
...@@ -16,8 +16,15 @@ module EE ...@@ -16,8 +16,15 @@ module EE
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil) def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt| JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id jwt['access_token'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id jwt['user_id'] = user_id || personal_access_token.user_id
end
end
def build_jwt_from_job(job, secret: jwt_secret)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['access_token'] = job.token
jwt['user_id'] = job.user.id
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