Skip to content

Tbt 381 382 deploy #1390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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'
66 changes: 66 additions & 0 deletions lib/travis/api/app/endpoint/assembla.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 = AccessToken.create(user: user, app_id: 0).token

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

private

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
20 changes: 20 additions & 0 deletions lib/travis/api/app/jwt_utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class Travis::Api::App
module JWTUtils
def extract_jwt_token(request)
request.env['HTTP_AUTHORIZATION']&.split&.last
end

def verify_jwt(request)
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
6 changes: 5 additions & 1 deletion lib/travis/config/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ def fallback_logs_api_auth_token
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'],
deep_integration_enabled: false,
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
72 changes: 72 additions & 0 deletions lib/travis/services/assembla_user_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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)
org = user.organizations.find_or_create_by!(
vcs_id: @payload['space_id'],
vcs_type: 'AssemblaOrganization'
)
membership = org.memberships.find_or_create_by(user: user)
membership.update(role: 'admin')
org
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
95 changes: 95 additions & 0 deletions spec/lib/services/assembla_user_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require 'spec_helper'

RSpec.describe Travis::Services::AssemblaUserService do
let(:payload) do
{
'id' => '12345',
'name' => 'Test User',
'email' => '[email protected]',
'login' => 'testuser',
'refresh_token' => 'refresh123',
'space_id' => '67890'
}
end

let(:service) { described_class.new(payload) }
let(:user) { FactoryBot.create(:user, vcs_id: payload['id'], email: payload['email'], login: payload['login'], name: payload['name']) }
let(:organization) { FactoryBot.create(:org, vcs_id: payload['space_id'], vcs_type: 'AssemblaOrganization') }

describe '#find_or_create_user' do
let(:expected_attrs) do
{
vcs_id: payload['id'],
email: payload['email'],
name: payload['name'],
login: payload['login'],
vcs_type: 'AssemblaUser'
}
end

before do
allow(Travis::RemoteVCS::User).to receive(:new).and_return(double(sync: true))
end

it 'finds or creates a user with correct attributes' do
service_user = service.find_or_create_user
expect(service_user.login).to eq(expected_attrs[:login])
expect(service_user.email).to eq(expected_attrs[:email])
expect(service_user.name).to eq(expected_attrs[:name])
expect(service_user.vcs_id).to eq(expected_attrs[:vcs_id])
expect(service_user.confirmed_at).to be_present
end

context 'when sync fails' do
it 'raises SyncError' do
allow(Travis::RemoteVCS::User).to receive(:new).and_raise(StandardError.new('Sync failed'))

expect { service.find_or_create_user }.to raise_error(
Travis::Services::AssemblaUserService::SyncError,
'Failed to sync user: Sync failed'
)
end
end
end

describe '#find_or_create_organization' do
let(:expected_attrs) do
{
vcs_id: payload['space_id'],
vcs_type: 'AssemblaOrganization'
}
end

it 'finds or creates organization with correct attributes' do
service_org = service.find_or_create_organization(user)

expect(service_org.vcs_type).to eq(expected_attrs[:vcs_type])
expect(service_org.vcs_id).to eq(expected_attrs[:vcs_id])
end

it 'has admin membership' do
service_org = service.find_or_create_organization(user)
expect(service_org.memberships.find_by(user: user).role).to eq('admin')
end
end

describe '#create_org_subscription' do
let(:billing_client) { double('BillingClient') }

before do
allow(Travis::API::V3::BillingClient).to receive(:new).with(user.id).and_return(billing_client)
end

context 'when billing client raises an error' do
let(:error) { StandardError.new('Billing error') }

it 'returns error hash' do
allow(billing_client).to receive(:create_v2_subscription).and_raise(error)

result = service.create_org_subscription(user, organization.id)
expect(result[:error]).to be_truthy
expect(result[:details]).to eq(error.message)
end
end
end
end
26 changes: 26 additions & 0 deletions spec/travis/remote_vcs/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@
end
end

describe '#sync' do
let(:user_id) { 123 }
let(:space_id) { 456 }
let(:repository_id) { 789 }
let(:instance) { described_class.new }
let(:req) { double(:request) }
let(:params) { double(:params) }

subject { instance.sync(user_id: user_id, space_id: space_id, repository_id: repository_id) }

before do
allow(req).to receive(:url)
allow(req).to receive(:params).and_return(params)
allow(params).to receive(:[]=)
end

it 'performs POST to VCS with proper params' do
expect(instance).to receive(:request).with(:post, :sync).and_yield(req)
expect(req).to receive(:url).with("users/#{user_id}/sync_data")
expect(params).to receive(:[]=).with('space_id', space_id)
expect(params).to receive(:[]=).with('repository_id', repository_id)

expect(subject).to be true
end
end

describe '#authenticate' do
let(:user) { described_class.new }
let(:provider) { 'assembla' }
Expand Down
Loading