Skip to content

Commit 447f8e3

Browse files
author
Shahzaib
committed
[TBT-381] Integrated assembla JWT login endpoint
1 parent 577cab5 commit 447f8e3

File tree

4 files changed

+180
-1
lines changed

4 files changed

+180
-1
lines changed

lib/travis/api/app/endpoint.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,4 @@ def auth_for_repo(id, type)
149149
require 'travis/api/app/endpoint/slow'
150150
require 'travis/api/app/endpoint/uptime'
151151
require 'travis/api/app/endpoint/users'
152+
require 'travis/api/app/endpoint/assembla'
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
require 'travis/api/app'
2+
require 'jwt'
3+
require 'travis/remote_vcs/user'
4+
require 'travis/remote_vcs/repository'
5+
6+
class Travis::Api::App
7+
class Endpoint
8+
class Assembla < Endpoint
9+
set prefix: '/assembla'
10+
set :check_auth, false
11+
12+
before do
13+
halt 403, { error: 'Deep integration not enabled' } unless deep_integration_enabled?
14+
halt 403, { error: 'Invalid ASM cluster' } unless valid_asm_cluster?
15+
@jwt_payload = verify_jwt
16+
end
17+
18+
# POST /assembla/login
19+
# Accepts a JWT, finds or creates a user, and signs them in
20+
post '/login' do
21+
user = find_or_create_user(@jwt_payload)
22+
begin
23+
Travis::RemoteVCS::User.new.sync(user_id: user.id)
24+
rescue => e
25+
halt 500, { error: 'User sync failed', details: e.message }.to_json
26+
end
27+
{ user_id: user.id, login: user.login, token: user.token, status: 'signed_in' }.to_json
28+
end
29+
30+
private
31+
32+
def verify_jwt
33+
token = extract_jwt_token
34+
halt 401, { error: 'Missing JWT' } unless token
35+
secret = Travis.config.assembla_jwt_secret
36+
begin
37+
decoded, = JWT.decode(token, secret, true, { algorithm: 'HS256' })
38+
decoded
39+
rescue JWT::DecodeError => e
40+
halt 401, { error: 'Invalid JWT', details: e.message }
41+
end
42+
end
43+
44+
def extract_jwt_token
45+
request.env['HTTP_AUTHORIZATION']&.split(' ')&.last
46+
end
47+
48+
def deep_integration_enabled?
49+
Travis.config.deep_integration_enabled
50+
end
51+
52+
def valid_asm_cluster?
53+
allowed = Array(Travis.config.assembla_clusters)
54+
cluster = request.env['HTTP_X_ASSEMBLA_CLUSTER']
55+
allowed.include?(cluster)
56+
end
57+
58+
# Finds or creates a user based on the payload
59+
def find_or_create_user(payload)
60+
required_fields = %w[name email login space_id]
61+
missing = required_fields.select { |f| payload[f].nil? || payload[f].to_s.strip.empty? }
62+
unless missing.empty?
63+
halt 400, { error: 'Missing required fields', missing: missing }.to_json
64+
end
65+
attrs = {
66+
name: payload['name'],
67+
email: payload['email'],
68+
login: payload['login'],
69+
org_id: payload['space_id'],
70+
vcs_type: 'AssemblaUser'
71+
}
72+
::User.first_or_create!(attrs)
73+
end
74+
end
75+
end
76+
end

lib/travis/config/defaults.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ def fallback_logs_api_auth_token
106106
recaptcha: { endpoint: 'https://www.google.com', secret: ENV['RECAPTCHA_SECRET_KEY'] || '' },
107107
antifraud: { captcha_max_failed_attempts: 3, captcha_block_duration: 24, credit_card_max_failed_attempts: 3, credit_card_block_duration: 24 },
108108
legacy_roles: false,
109-
internal_users: [{id: 0, login: 'cron'}]
109+
internal_users: [{id: 0, login: 'cron'}],
110+
deep_integration_enabled: ENV['DEEP_INTEGRATION_ENABLED'],
111+
assembla_clusters: ENV['ASSEMBLA_CLUSTERS'].split(','),
112+
assembla_jwt_secret: ENV['ASSEMBLA_JWT_SECRET']
110113

111114
default :_access => [:key]
112115

spec/unit/endpoint/assembla_spec.rb

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
require 'spec_helper'
2+
require 'rack/test'
3+
require 'jwt'
4+
5+
RSpec.describe Travis::Api::App::Endpoint::Assembla, set_app: true do
6+
include Rack::Test::Methods
7+
8+
let(:jwt_secret) { 'testsecret' }
9+
let(:payload) do
10+
{
11+
'name' => 'Test User',
12+
'email' => '[email protected]',
13+
'login' => 'testuser',
14+
'space_id' => 'space123'
15+
}
16+
end
17+
let(:token) { JWT.encode(payload, jwt_secret, 'HS256') }
18+
19+
before do
20+
Travis.config[:deep_integration_enabled] = true
21+
Travis.config[:assembla_clusters] = ['cluster1']
22+
Travis.config[:assembla_jwt_secret] = jwt_secret
23+
24+
header 'X_ASSEMBLA_CLUSTER', 'cluster1'
25+
end
26+
27+
describe 'POST /assembla/login' do
28+
context 'with valid JWT' do
29+
before do
30+
allow(::User).to receive(:first_or_create!).and_return(double('User', id: 1, login: 'testuser', token: 'abc123'))
31+
allow_any_instance_of(Travis::RemoteVCS::User).to receive(:sync).and_return(true)
32+
end
33+
34+
it 'returns user info and token' do
35+
binding.pry
36+
header 'Authorization', "Bearer #{token}"
37+
post '/assembla/login'
38+
expect(last_response.status).to eq(200)
39+
body = JSON.parse(last_response.body)
40+
expect(body['user_id']).to eq(1)
41+
expect(body['login']).to eq('testuser')
42+
expect(body['token']).to eq('abc123')
43+
expect(body['status']).to eq('signed_in')
44+
end
45+
end
46+
47+
context 'with missing JWT' do
48+
it 'returns 401' do
49+
post '/assembla/login'
50+
expect(last_response.status).to eq(401)
51+
expect(last_response.body).to include('Missing JWT')
52+
end
53+
end
54+
55+
context 'with invalid JWT' do
56+
it 'returns 401' do
57+
header 'Authorization', 'Bearer invalidtoken'
58+
post '/assembla/login'
59+
expect(last_response.status).to eq(401)
60+
expect(last_response.body).to include('Invalid JWT')
61+
end
62+
end
63+
64+
context 'when user sync fails' do
65+
before do
66+
allow(::User).to receive(:first_or_create!).and_return(double('User', id: 1, login: 'testuser', token: 'abc123'))
67+
allow_any_instance_of(Travis::RemoteVCS::User).to receive(:sync).and_raise(StandardError.new('sync error'))
68+
end
69+
70+
it 'returns 500 with error message' do
71+
header 'Authorization', "Bearer #{token}"
72+
post '/assembla/login'
73+
expect(last_response.status).to eq(500)
74+
expect(last_response.body).to include('User sync failed')
75+
expect(last_response.body).to include('sync error')
76+
end
77+
end
78+
79+
context 'when integration is not enabled' do
80+
before { Travis.config[:deep_integration_enabled] = false }
81+
it 'returns 403' do
82+
header 'Authorization', "Bearer #{token}"
83+
post '/assembla/login'
84+
expect(last_response.status).to eq(403)
85+
expect(last_response.body).to include('Deep integration not enabled')
86+
end
87+
end
88+
89+
context 'when cluster is invalid' do
90+
before { header 'X_ASSEMBLA_CLUSTER', 'invalid-cluster' }
91+
it 'returns 403' do
92+
header 'Authorization', "Bearer #{token}"
93+
post '/assembla/login'
94+
expect(last_response.status).to eq(403)
95+
expect(last_response.body).to include('Invalid ASM cluster')
96+
end
97+
end
98+
end
99+
end

0 commit comments

Comments
 (0)