Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
447f8e3
[TBT-381] Integrated assembla JWT login endpoint
Jul 6, 2025
1934be6
[TBT-381] Fixed assembla spec
mshahzaib-travis Jul 6, 2025
e461de2
[TBT-381] Fixed build failure Env issue
mshahzaib-travis Jul 7, 2025
0526c13
[TBT-381] Create beta_plan for org
mshahzaib-travis Jul 7, 2025
7b5948a
[TBT-381] - Sync and Create Subscription
mshahzaib-travis Jul 10, 2025
ab5b727
[TBT-381] Feedback incorporated
mshahzaib-travis Jul 11, 2025
a1bf5c3
[TBT-381] Feedback incorporated
mshahzaib-travis Jul 11, 2025
1a2c602
[TBT-381] used default values in spec
mshahzaib-travis Jul 11, 2025
5f79e3b
[TBT-381] changed config to use array
mshahzaib-travis Jul 11, 2025
fb82a6a
Sync only passed space and repository
Jul 11, 2025
daaafb7
[TBT-382] specs fixed
mshahzaib-travis Jul 14, 2025
d95c12f
Merge branch 'TBT-381-assembla-jwt-login-endpoint' into deploy-tbt-38…
mshahzaib-travis Jul 16, 2025
9efcdbb
Merge branch 'TBT-382-sync-org-and-repo' into deploy-tbt-381-382
mshahzaib-travis Jul 16, 2025
592a991
Returned authentication token
mshahzaib-travis Aug 4, 2025
2cb62d5
Created repo in login endpoint
mshahzaib-travis Aug 4, 2025
78b1070
Removed repo creation
mshahzaib-travis Aug 4, 2025
8011cb6
Generate access token
mshahzaib-travis Aug 5, 2025
54382f2
use app_id 0
mshahzaib-travis Aug 5, 2025
213d0c2
Added debug for debugging
mshahzaib-travis Aug 5, 2025
db9cadb
Generated access token
mshahzaib-travis Aug 6, 2025
bf4eb60
Confirm user and fix spec
mshahzaib-travis Aug 6, 2025
29fccc1
Merge branch 'TBT-381-assembla-jwt-login-endpoint' into deploy-tbt-38…
mshahzaib-travis Aug 7, 2025
461f0b1
Revert "Added debug for debugging"
mshahzaib-travis Aug 7, 2025
cd2904e
Merge branch 'deploy-tbt-381-382' of github.com-work:travis-ci/travis…
mshahzaib-travis Aug 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ config/travis.yml
config/travis/*
config/database.yml
config/nginx.conf

vendor
db/

tmp/
Expand Down
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ RUN ( \
&& rm -rf /var/lib/apt/lists/* \
)

ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
# ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2

# throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1
RUN bundle config set deployment 'true'
RUN bundle config set without 'development test'

RUN mkdir -p /app
WORKDIR /app
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ gem 'hashr'
gem 'pusher', '~> 2.0.3'
gem 'multi_json'
gem 'closeio', '~> 3.15'
gem 'pry-rails'

group :test do
gem 'rspec'
Expand Down
7 changes: 6 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ GEM
google-cloud-trace-v2 (0.6.1)
gapic-common (>= 0.19.1, < 2.a)
google-cloud-errors (~> 1.0)
google-protobuf (3.23.4-aarch64-linux)
google-protobuf (3.23.4-arm64-darwin)
google-protobuf (3.23.4-x86_64-linux)
googleapis-common-protos (1.4.0)
Expand Down Expand Up @@ -386,6 +387,8 @@ GEM
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
pry-rails (0.3.11)
pry (>= 0.13.0)
public_suffix (5.0.3)
pusher (2.0.3)
httpclient (~> 2.8)
Expand Down Expand Up @@ -547,6 +550,7 @@ GEM
zeitwerk (2.6.8)

PLATFORMS
aarch64-linux
arm64-darwin-22
x86_64-linux

Expand Down Expand Up @@ -591,6 +595,7 @@ DEPENDENCIES
pg (~> 1.5)
pry
pry-byebug
pry-rails
pusher (~> 2.0.3)
rack (~> 2.2)
rack-attack (~> 6)
Expand Down Expand Up @@ -639,4 +644,4 @@ RUBY VERSION
ruby 3.2.2p53

BUNDLED WITH
2.4.14
2.6.9
2 changes: 1 addition & 1 deletion config/puma-config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
bind "unix://#{tmp_dir}/nginx.socket"
environment ENV['RACK_ENV'] || 'development'

threads 0, 16
threads 1, 1
2 changes: 1 addition & 1 deletion config/unicorn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
listen File.expand_path("nginx.socket", tmp_dir), backlog: 1024
else
if ENV['DOCKER']
listen "#{Integer(ENV.fetch('PORT'))}", backlog: 1024
listen "0.0.0.0:#{Integer(ENV.fetch('PORT'))}", backlog: 1024
else
listen "127.0.0.1:#{Integer(ENV.fetch('PORT'))}", backlog: 1024
end
Expand Down
1 change: 1 addition & 0 deletions lib/travis/api/app/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,4 @@ def auth_for_repo(id, type)
require 'travis/api/app/endpoint/slow'
require 'travis/api/app/endpoint/uptime'
require 'travis/api/app/endpoint/users'
require 'travis/api/app/endpoint/assembla'
70 changes: 70 additions & 0 deletions lib/travis/api/app/endpoint/assembla.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require 'travis/api/app'
require 'jwt'
require 'travis/remote_vcs/user'
require 'travis/remote_vcs/repository'
require 'travis/api/v3/billing_client'
require 'travis/services/assembla_user_service'
require_relative '../jwt_utils'

class Travis::Api::App
class Endpoint
# Assembla integration endpoint for handling user authentication and organization setup
class Assembla < Endpoint
include Travis::Api::App::JWTUtils

REQUIRED_JWT_FIELDS = %w[name email login space_id repository_id id refresh_token].freeze
CLUSTER_HEADER = 'HTTP_X_ASSEMBLA_CLUSTER'.freeze

set prefix: '/assembla'
set :check_auth, false

before do
validate_request!
end

post '/login' do
service = Travis::Services::AssemblaUserService.new(@jwt_payload)

user = service.find_or_create_user
org = service.find_or_create_organization(user)
service.create_org_subscription(user, org.id)
access_token = generate_access_token(user: user, app_id: 0)

{
user_id: user.id,
login: user.login,
token: access_token
}
end

private

def generate_access_token(options)
AccessToken.create(options).token
end

def validate_request!
halt 403, { error: 'Deep integration not enabled' } unless deep_integration_enabled?
halt 403, { error: 'Invalid ASM cluster' } unless valid_asm_cluster?
@jwt_payload = verify_jwt(request)
check_required_fields
end

def check_required_fields
missing = REQUIRED_JWT_FIELDS.select { |f| @jwt_payload[f].nil? || @jwt_payload[f].to_s.strip.empty? }
unless missing.empty?
halt 400, { error: 'Missing required fields', missing: missing }
end
end

def deep_integration_enabled?
Travis.config.deep_integration_enabled
end

def valid_asm_cluster?
allowed = Travis.config.assembla_clusters
allowed.include?(request.env[CLUSTER_HEADER])
end
end
end
end
79 changes: 79 additions & 0 deletions lib/travis/api/app/jwt_utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
class Travis::Api::App
module JWTUtils
def extract_jwt_token(request)
request.env['HTTP_AUTHORIZATION']&.split&.last
end

def verify_jwt(request)
# payload = {
# email: '[email protected]',
# login: 'viktorijaTravisAssembla380',
# id: 'cs0JUKBgSr8ioDLJtkgGFV',
# name: 'viktorija devtactics',
# space_id: 'crofDwBRKr8kdd0NKLjkMA',
# repository_id: 'cajjvcB_qr8iot_O0clYmL',
# access_token: 'e6dea10f2ea91562f8fcf44eff6588e9',
# refresh_token: '5dfe4814cea729ea4b0ce97ba8d4aa41',
# exp: Time.now.to_i + (3600 * 100) # 7 days expiration
# }

# secret = Travis.config.assembla_jwt_secret
# token = JWT.encode(payload, secret, 'HS256')
# puts "Generated JWT: #{token}"

# Generated JWT at 3rd aug 2025 at 4:15 am For staging secret:
# eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InZpa3RvcmlqYS5rcml2b2thcGljMSsxQGdtYWlsLmNvbSIsImxvZ2luIjoidmlrdG9yaWphVHJhdmlzQXNzZW1ibGEzODAiLCJpZCI6ImNzMEpVS0JnU3I4aW9ETEp0a2dHRlYiLCJuYW1lIjoidmlrdG9yaWphIGRldnRhY3RpY3MiLCJzcGFjZV9pZCI6ImNyb2ZEd0JSS3I4a2RkME5LTGprTUEiLCJyZXBvc2l0b3J5X2lkIjoiY2FqanZjQl9xcjhpb3RfTzBjbFltTCIsImFjY2Vzc190b2tlbiI6ImU2ZGVhMTBmMmVhOTE1NjJmOGZjZjQ0ZWZmNjU4OGU5IiwicmVmcmVzaF90b2tlbiI6IjVkZmU0ODE0Y2VhNzI5ZWE0YjBjZTk3YmE4ZDRhYTQxIiwiZXhwIjoxNzU0NTM2NTc0fQ.IoNhjpl3DwK5HVqO5FiQiylqKkKiRDV8qaLiFgcv01k


# payload = {
# email: '[email protected]',
# login: 'viktorijaTravisAssembla380',
# id: 'cs0JUKBgSr8ioDLJtkgGFV',
# name: 'viktorija devtactics',
# space_id: 'crofDwBRKr8kdd0NKLjkMA',
# repository_id: 'cajjvcB_qr8iot_O0clYmL',
# access_token: 'e6dea10f2ea91562f8fcf44eff6588e9',
# refresh_token: '5dfe4814cea729ea4b0ce97ba8d4aa41',
# exp: Time.now.to_i + (3600 * 100) # 7 days expiration
# }

# secret = 'N3jANvlyDRvzYPAXlZi90zlow8kzgmMKFCBnZ0sB7mxGmmyVYF0vF0V7Go23Of4T'
# token = JWT.encode(payload, secret, 'HS256')
# puts "Generated JWT: #{token}"
# Generated JWT at 3rd aug 2025 at 4:15 am For staging dev secret:
# eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InZpa3RvcmlqYS5rcml2b2thcGljMSsxQGdtYWlsLmNvbSIsImxvZ2luIjoidmlrdG9yaWphVHJhdmlzQXNzZW1ibGEzODAiLCJpZCI6ImNzMEpVS0JnU3I4aW9ETEp0a2dHRlYiLCJuYW1lIjoidmlrdG9yaWphIGRldnRhY3RpY3MiLCJzcGFjZV9pZCI6ImNyb2ZEd0JSS3I4a2RkME5LTGprTUEiLCJyZXBvc2l0b3J5X2lkIjoiY2FqanZjQl9xcjhpb3RfTzBjbFltTCIsImFjY2Vzc190b2tlbiI6ImU2ZGVhMTBmMmVhOTE1NjJmOGZjZjQ0ZWZmNjU4OGU5IiwicmVmcmVzaF90b2tlbiI6IjVkZmU0ODE0Y2VhNzI5ZWE0YjBjZTk3YmE4ZDRhYTQxIiwiZXhwIjoxNzU0NTM3MTUyfQ.CnEsqTvFrSSfW23D6qdDynCRB51ea2kTXNiHLPh8w-I



# for staging dev, user ID:125840 on 5th Aug 2025
# payload = {
# email: '[email protected]',
# login: 'user-0408',
# id: 'aZjjsYCrir8ikSdMBSqNIq',
# name: 'user-0408 test',
# space_id: 'bkNxmOCC0r8j7dtK6LbPF5',
# repository_id: 'cldIxeCC0r8j7dtK6LbPF5',
# refresh_token: '1adee72d172bd230203188153dca302d',
# exp: Time.now.to_i + (3600 * 100) # 7 days expiration
# }

# secret = 'N3jANvlyDRvzYPAXlZi90zlow8kzgmMKFCBnZ0sB7mxGmmyVYF0vF0V7Go23Of4T'
# token = JWT.encode(payload, secret, 'HS256')
# puts "Generated JWT: #{token}"
# Generated JWT: eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im9rc2FuYS5oaW5haWxvK3VzZXItMDQwOEBkZXZ0YWN0aWNzLm5ldCIsImxvZ2luIjoidXNlci0wNDA4IiwiaWQiOiJhWmpqc1lDcmlyOGlrU2RNQlNxTklxIiwibmFtZSI6InVzZXItMDQwOCB0ZXN0Iiwic3BhY2VfaWQiOiJia054bU9DQzByOGo3ZHRLNkxiUEY1IiwicmVwb3NpdG9yeV9pZCI6ImNsZEl4ZUNDMHI4ajdkdEs2TGJQRjUiLCJyZWZyZXNoX3Rva2VuIjoiMWFkZWU3MmQxNzJiZDIzMDIwMzE4ODE1M2RjYTMwMmQiLCJleHAiOjE3NTQ3NDc4NDd9.ArsRunhJTHsC6KeRjvThC4gynSWtvPsnqqxgn7Eorf4



token = extract_jwt_token(request)

halt 401, { error: "Missing JWT" } unless token

begin
decoded, = JWT.decode(token, Travis.config.assembla_jwt_secret, true, algorithm: 'HS256')
decoded
rescue JWT::DecodeError => e
halt 401, { error: "Invalid JWT: #{e.message}" }
end
end
end
end
14 changes: 9 additions & 5 deletions lib/travis/config/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def fallback_logs_api_auth_token
amqp: { username: 'guest', password: 'guest', host: 'localhost', prefetch: 1 },
closeio: { key: 'key' },
gdpr: {},
database: { adapter: 'postgresql', database: "travis_#{Travis.env}", encoding: 'unicode', min_messages: 'warning', variables: { statement_timeout: ENV['TRAVIS_DB_STATEMENT_TIMEOUT'] || 10000 } },
database: {host: 'localhost', port: 5432, username: 'postgres', password: 'postgres', adapter: 'postgresql', database: "travis_#{Travis.env}", encoding: 'unicode', min_messages: 'warning', variables: { statement_timeout: ENV['TRAVIS_DB_STATEMENT_TIMEOUT'] || 10000 } },
db: { max_statement_timeout_in_seconds: ENV['TRAVIS_MAX_DB_STATEMENT_TIMEOUT'] || 15, slow_host_max_statement_timeout_in_seconds: ENV['TRAVIS_MAX_DB_STATEMENT_TIMEOUT'] || 60},
log_options: { s3: { access_key_id: '', secret_access_key: ''}},
s3: { access_key_id: '', secret_access_key: ''},
Expand Down Expand Up @@ -95,18 +95,22 @@ def fallback_logs_api_auth_token
force_authentication: false,
read_only: ENV['READ_ONLY'] || false,
job_log_access_permissions: { time_based_limit: false, access_based_limit: false, older_than_days: 365, max_days_value: 730, min_days_value: 30 },
billing: {},
vcs: {},
billing: {url: 'http://0.0.0.0:9292', auth_key: 'billing-auth-key'},
vcs: {url: 'http://localhost:4000', token: 'secure-vcs-token', auth_key: 'vcs'},
yml: { url: 'https://yml.travis-ci.org', token: 'secret', auth_key: 'abc123' },
logs_api: { url: logs_api_url, token: logs_api_auth_token },
fallback_logs_api: { url: fallback_logs_api_auth_url, token: fallback_logs_api_auth_token },
scanner: {},
insights: { endpoint: 'https://insights.travis-ci.dev/', auth_token: 'secret' },
authorizer: { url: 'http://authorizer', auth_key: 'secret' },
authorizer: { url: 'http://0.0.0.0:3434/', auth_key: 'secret' },
recaptcha: { endpoint: 'https://www.google.com', secret: ENV['RECAPTCHA_SECRET_KEY'] || '' },
antifraud: { captcha_max_failed_attempts: 3, captcha_block_duration: 24, credit_card_max_failed_attempts: 3, credit_card_block_duration: 24 },
legacy_roles: false,
internal_users: [{id: 0, login: 'cron'}]
internal_users: [{id: 0, login: 'cron'}],
assembla_clusters: ['eu', 'us', 'cluster1'],
deep_integration_enabled: true,
assembla_jwt_secret: 'assembla_jwt_secret',
deep_integration_plan_name: 'beta_plan'

default :_access => [:key]

Expand Down
4 changes: 3 additions & 1 deletion lib/travis/model/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ class User < Travis::Model
after_create :create_the_tokens
before_save :track_previous_changes

serialize :github_scopes
alias_attribute :vcs_oauth_token, :github_oauth_token

serialize :github_scopes
serialize :vcs_oauth_token, EncryptedColumn.new
serialize :github_oauth_token, Travis::Model::EncryptedColumn.new

before_save do
Expand Down
4 changes: 3 additions & 1 deletion lib/travis/remote_vcs/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ def generate_token(provider: :github, token:, app_id: 1)
end
end

def sync(user_id:)
def sync(user_id:, space_id: nil, repository_id: nil)
request(:post, __method__) do |req|
req.url "users/#{user_id}/sync_data"
req.params['space_id'] = space_id if space_id
req.params['repository_id'] = repository_id if repository_id
end && true
end

Expand Down
69 changes: 69 additions & 0 deletions lib/travis/services/assembla_user_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module Travis
module Services
class AssemblaUserService
class SyncError < StandardError; end

def initialize(payload)
@payload = payload
end

def find_or_create_user
user = ::User.find_or_initialize_by(
name: @payload['name'],
vcs_id: @payload['id'],
email: @payload['email'],
login: @payload['login'],
vcs_type: 'AssemblaUser'
)
user.vcs_oauth_token = @payload['refresh_token']
user.confirmed_at = DateTime.now if user.confirmed_at.nil?
user.save!
sync_user(user.id)
user
end

def find_or_create_organization(user)
user.organizations.find_or_create_by!(
vcs_id: @payload['space_id'],
vcs_type: 'AssemblaOrganization'
)
end

def create_org_subscription(user, organization_id)
billing_client = Travis::API::V3::BillingClient.new(user.id)
billing_client.create_v2_subscription(subscription_params(user, organization_id))
rescue => e
{ error: true, details: e.message }
end

private

def sync_user(user_id)
Travis::RemoteVCS::User.new.sync(user_id: user_id, space_id: @payload['space_id'], repository_id: @payload['repository_id'])
rescue => e
raise SyncError, "Failed to sync user: #{e.message}"
end

def subscription_params(user, organization_id)
{
'plan' => Travis.config.deep_integration_plan_name,
'organization_id' => organization_id,
'billing_info' => billing_info(user),
'credit_card_info' => { 'token' => nil }
}
end

def billing_info(user)
{
'address' => 'Dummy Address',
'city' => 'Dummy City',
'country' => 'Dummy Country',
'first_name' => user.name&.split&.first,
'last_name' => user.name&.split&.last,
'zip_code' => 'DUMMY ZIP',
'billing_email' => user.email
}
end
end
end
end
3 changes: 3 additions & 0 deletions script/server-buildpacks
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# $PGBOUNCER_ENABLED variable is set to '1' or 'true'
# or if the space-delimited list
# $PGBOUNCER_ENABLED_FOR_DYNOS contains $DYNO
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/travis_development
export TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/travis_test

cd "$(dirname "$0")/.."

Expand Down
Loading