start working on API v3
This commit is contained in:
parent
ed46d49f24
commit
a65792ee49
|
@ -68,15 +68,15 @@ GIT
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: git://github.com/travis-ci/travis-support.git
|
remote: git://github.com/travis-ci/travis-support.git
|
||||||
revision: 40365216662f639d36fc3a0463c4e189ee1563dd
|
revision: 4fdd220ed7b06a12951e5d74a763c05a80eb0d20
|
||||||
specs:
|
specs:
|
||||||
travis-support (0.0.1)
|
travis-support (0.0.1)
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: git://github.com/travis-ci/travis-yaml.git
|
remote: git://github.com/travis-ci/travis-yaml.git
|
||||||
revision: 08ec0c4d0cf3366cd971d4acd9aadbc0db68f85d
|
revision: f3aa306016a08b66a487f966eb8aa3a60ee9b319
|
||||||
specs:
|
specs:
|
||||||
travis-yaml (0.1.0)
|
travis-yaml (0.2.0)
|
||||||
|
|
||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
|
|
|
@ -20,6 +20,7 @@ require 'metriks/librato_metrics_reporter'
|
||||||
require 'travis/support/log_subscriber/active_record_metrics'
|
require 'travis/support/log_subscriber/active_record_metrics'
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
require 'travis/api/v2/http'
|
require 'travis/api/v2/http'
|
||||||
|
require 'travis/api/v3'
|
||||||
|
|
||||||
# Rack class implementing the HTTP API.
|
# Rack class implementing the HTTP API.
|
||||||
# Instances respond to #call.
|
# Instances respond to #call.
|
||||||
|
@ -110,11 +111,17 @@ module Travis::Api
|
||||||
env['travis.global_prefix'] = env['SCRIPT_NAME']
|
env['travis.global_prefix'] = env['SCRIPT_NAME']
|
||||||
end
|
end
|
||||||
|
|
||||||
use Travis::Api::App::Middleware::ScopeCheck
|
|
||||||
use Travis::Api::App::Middleware::Logging
|
use Travis::Api::App::Middleware::Logging
|
||||||
use Travis::Api::App::Middleware::Metriks
|
use Travis::Api::App::Middleware::ScopeCheck
|
||||||
use Travis::Api::App::Middleware::Rewrite
|
|
||||||
use Travis::Api::App::Middleware::UserAgentTracker
|
use Travis::Api::App::Middleware::UserAgentTracker
|
||||||
|
use Travis::Api::App::Middleware::Metriks
|
||||||
|
|
||||||
|
# if this is a v3 API request, ignore everything after
|
||||||
|
use Travis::API::V3::OptIn
|
||||||
|
|
||||||
|
# rewrite should come after V3 hook
|
||||||
|
use Travis::Api::App::Middleware::Rewrite
|
||||||
|
|
||||||
SettingsEndpoint.subclass :env_vars
|
SettingsEndpoint.subclass :env_vars
|
||||||
if Travis.config.endpoints.ssh_key
|
if Travis.config.endpoints.ssh_key
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Travis::Api::App
|
||||||
# Landing point. Redirects web browsers to [API documentation](#/docs/).
|
# Landing point. Redirects web browsers to [API documentation](#/docs/).
|
||||||
get '/' do
|
get '/' do
|
||||||
pass if settings.disable_root_endpoint?
|
pass if settings.disable_root_endpoint?
|
||||||
redirect to('/docs/') if request.preferred_type('application/json', 'text/html') == 'text/html'
|
redirect to('/docs/') if request.preferred_type('application/json', 'application/json-home', 'text/html') == 'text/html'
|
||||||
{ 'hello' => 'world' }
|
{ 'hello' => 'world' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
13
lib/travis/api/v3.rb
Normal file
13
lib/travis/api/v3.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Travis
|
||||||
|
module API
|
||||||
|
module V3
|
||||||
|
def load_dir(dir, recursive: true)
|
||||||
|
Dir.glob("#{dir}/*.rb").each { |f| require f[%r[(?<=lib/).+(?=\.rb$)]] }
|
||||||
|
Dir.glob("#{dir}/*").each { |dir| load_dir(dir) } if recursive
|
||||||
|
end
|
||||||
|
|
||||||
|
extend self
|
||||||
|
load_dir("#{__dir__}/v3")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
lib/travis/api/v3/access_control.rb
Normal file
15
lib/travis/api/v3/access_control.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
module AccessControl
|
||||||
|
REGISTER = {}
|
||||||
|
|
||||||
|
def self.new(env)
|
||||||
|
type, payload = env['HTTP_AUTHORIZATION'.freeze].to_s.split(" ", 2)
|
||||||
|
payload &&= payload.unpack(?m.freeze).first if type == 'basic'.freeze
|
||||||
|
payload &&= type == 'token'.freeze ? payload.gsub(/^"(.+)"$/, '\1'.freeze) : payload.split(?:.freeze)
|
||||||
|
modes = REGISTER.fetch(type, [])
|
||||||
|
access_control = modes.inject(nil) { |current, mode| current || mode.for_request(type, payload, env) }
|
||||||
|
raise WrongCredentials unless access_control
|
||||||
|
access_control
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
12
lib/travis/api/v3/access_control/anonymous.rb
Normal file
12
lib/travis/api/v3/access_control/anonymous.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
require 'travis/api/v3/access_control/generic'
|
||||||
|
|
||||||
|
module Travis::API::V3
|
||||||
|
class AccessControl::Anonymous < AccessControl::Generic
|
||||||
|
# use when Authorization header is not set
|
||||||
|
auth_type(nil)
|
||||||
|
|
||||||
|
def self.for_request(*)
|
||||||
|
new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
lib/travis/api/v3/access_control/application.rb
Normal file
25
lib/travis/api/v3/access_control/application.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
require 'travis/api/v3/access_control/generic'
|
||||||
|
|
||||||
|
module Travis::API::V3
|
||||||
|
class AccessControl::Application < AccessControl::Generic
|
||||||
|
attr_reader :application_name, :config, :user
|
||||||
|
|
||||||
|
def initialize(application_name, user: nil)
|
||||||
|
@application_name = application_name
|
||||||
|
@config = Travis.config.applications[application_name]
|
||||||
|
@user = user
|
||||||
|
raise ArgumentError, 'unknown application %p' % application_name unless config
|
||||||
|
raise ArgumentError, 'cannot use %p without a user' % application_name if config.requires_user and not user
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def logged_in?
|
||||||
|
!!user
|
||||||
|
end
|
||||||
|
|
||||||
|
def full_access?
|
||||||
|
config.full_access
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
48
lib/travis/api/v3/access_control/generic.rb
Normal file
48
lib/travis/api/v3/access_control/generic.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
class AccessControl::Generic
|
||||||
|
def self.for_request(type, payload, env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.auth_type(*list)
|
||||||
|
list.each { |e| (AccessControl::REGISTER[e] ||= []) << self }
|
||||||
|
end
|
||||||
|
|
||||||
|
def visible?(object)
|
||||||
|
full_access? or dispatch(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def repository_visible?(repository)
|
||||||
|
return true if unrestricted_api? and not repository.private?
|
||||||
|
private_repository_visible?(repository)
|
||||||
|
end
|
||||||
|
|
||||||
|
def private_repository_visible?(repository)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def full_access?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def logged_in?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_api?
|
||||||
|
Travis.config.public_api
|
||||||
|
end
|
||||||
|
|
||||||
|
def unrestricted_api?
|
||||||
|
full_access? or logged_in? or public_api?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def dispatch(object, method = caller_locations.first.base_label)
|
||||||
|
method = object.class.name.underscore + ?_.freeze + method
|
||||||
|
public_send(method) if respond_to?(method)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
lib/travis/api/v3/access_control/legacy_token.rb
Normal file
27
lib/travis/api/v3/access_control/legacy_token.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
require 'travis/api/app/access_token'
|
||||||
|
require 'travis/api/v3/access_control/user'
|
||||||
|
|
||||||
|
module Travis::API::V3
|
||||||
|
# Using v2 API tokens to access v3 API.
|
||||||
|
# Allows us to later introduce a new way of storing tokens with more capabilities without API users having to know.
|
||||||
|
class AccessControl::LegacyToken < AccessControl::User
|
||||||
|
auth_type('token', 'basic')
|
||||||
|
|
||||||
|
def self.for_request(type, payload, env)
|
||||||
|
payload = paylad.first if payload.is_a? Array
|
||||||
|
token = Travis::Api::App::AccessToken.find_by_token(payload)
|
||||||
|
new(token) if token
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(token)
|
||||||
|
@token = token
|
||||||
|
super(token.user)
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def permission?(action, id)
|
||||||
|
super if token.scopes.include? :private
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
lib/travis/api/v3/access_control/scoped.rb
Normal file
19
lib/travis/api/v3/access_control/scoped.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
require 'travis/api/v3/access_control/generic'
|
||||||
|
|
||||||
|
module Travis::API::V3
|
||||||
|
class AccessControl::Scoped < AccessControl::Generic
|
||||||
|
attr_accessor :unscoped, :owner_name, :name
|
||||||
|
|
||||||
|
def initialize(scope, unscoped)
|
||||||
|
@owner_name, @name = scope.split(?/.freeze, 2)
|
||||||
|
@unscoped = unscoped
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def private_repository_visible?(repository)
|
||||||
|
return false if name and repository.name != name
|
||||||
|
unscoped.private_repository_visible?(repository) if repository.owner_name == owner_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
55
lib/travis/api/v3/access_control/signature.rb
Normal file
55
lib/travis/api/v3/access_control/signature.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
require 'travis/api/v3/access_control/generic'
|
||||||
|
require 'travis/api/app/access_token'
|
||||||
|
require 'digest/sha1'
|
||||||
|
require 'openssl'
|
||||||
|
|
||||||
|
module Travis::API::V3
|
||||||
|
# Support signed requests to not expose the secret to an untrusted environment.
|
||||||
|
class AccessControl::Signature < AccessControl::Generic
|
||||||
|
auth_type('signature')
|
||||||
|
|
||||||
|
def self.for_request(type, payload, env)
|
||||||
|
*args, signature = payload
|
||||||
|
options = Hash[args.map { |a| a.split(?=.freeze, 2) }]
|
||||||
|
challenge = ""
|
||||||
|
|
||||||
|
if github_id = options[?u.freeze]
|
||||||
|
return unless user = ::User.find_by_github_id(github_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
if application = options[?a.freeze]
|
||||||
|
return unless Travis.config.application and app_config = Travis.config.applications[application]
|
||||||
|
end
|
||||||
|
|
||||||
|
challenge << env['REQUEST_METHOD'.freeze] << "\n".freeze if options[?c.freeeze].include?(?m.freeze)
|
||||||
|
challenge << env['SCRIPT_NAME'.freeze] << env['PATH_INFO'.freeze] << "\n" if options[?c.freeeze].include?(?p.freeze)
|
||||||
|
challenge << app_config[:secret] if app_config and user
|
||||||
|
challenge << args.join(?:.freeze)
|
||||||
|
|
||||||
|
if app_config
|
||||||
|
control = AccessControl::Application.new(application, user: user)
|
||||||
|
secrets = user ? secrets_for(user) : [app_config[:secret]]
|
||||||
|
else
|
||||||
|
control = AccessControl::User.new(user)
|
||||||
|
secrets = secrets_for(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
if scope = options[?s.freeze]
|
||||||
|
control &&= Scoped.new(scope, control)
|
||||||
|
end
|
||||||
|
|
||||||
|
control if secrets.any? { |secret| signed(challenge, secret) == signature }
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.secrets_for(user)
|
||||||
|
[
|
||||||
|
Travis::Api::App::AccessToken.new(user: user, app_id: 1), # generated from github token
|
||||||
|
Travis::Api::App::AccessToken.new(user: user, app_id: 0) # used by web
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed(challenge, secret)
|
||||||
|
OpenSSL::HMAC.hexdigest('sha256'.freeze, secret, challenge)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
24
lib/travis/api/v3/access_control/user.rb
Normal file
24
lib/travis/api/v3/access_control/user.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
require 'travis/api/v3/access_control/generic'
|
||||||
|
|
||||||
|
module Travis::API::V3
|
||||||
|
class AccessControl::User < AccessControl::Generic
|
||||||
|
attr_reader :user, :permissions
|
||||||
|
|
||||||
|
def initialize(user)
|
||||||
|
@user = user
|
||||||
|
@permissions = user.permissions.where(user_id: user.id)
|
||||||
|
super()
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def private_repository_visible?(repository)
|
||||||
|
permissions?(:pull, repository)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permission?(type, id)
|
||||||
|
id = id.id if id.is_a? ::Repository
|
||||||
|
permissions.where(type => trye, :repository_id => id).any?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
64
lib/travis/api/v3/opt_in.rb
Normal file
64
lib/travis/api/v3/opt_in.rb
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
class OptIn
|
||||||
|
attr_reader :legacy_stack, :prefix, :router, :accept, :version_header
|
||||||
|
|
||||||
|
def initialize(legacy_stack, prefix: '/v3/', router: Router.new, accept: 'application/vnd.travis-ci.3+', version_header: 'Travis-API-Version')
|
||||||
|
@legacy_stack = legacy_stack
|
||||||
|
@prefix = prefix
|
||||||
|
@router = router
|
||||||
|
@accept = accept
|
||||||
|
@version_header = "HTTP_#{version_header.upcase.gsub(/\W/, '_')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
return redirect(env) if redirect?(env)
|
||||||
|
|
||||||
|
if matched = matching_env(env)
|
||||||
|
result = @router.call(matched)
|
||||||
|
result, missing = nil, result if cascade?(*result)
|
||||||
|
end
|
||||||
|
|
||||||
|
result = result || legacy_stack.call(env)
|
||||||
|
pick(result, missing)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pick(result, missing)
|
||||||
|
return result if missing.nil?
|
||||||
|
return result if result[0] != 404
|
||||||
|
missing
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect?(env)
|
||||||
|
env['PATH_INFO'.freeze] + ?/.freeze == prefix
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect(env)
|
||||||
|
[307, {'Location'.freeze => env['SCRIPT_NAME'.freeze] + prefix, 'Conent-Type'.freeze => 'text/plain'.freeze}, []]
|
||||||
|
end
|
||||||
|
|
||||||
|
def cascade?(status, headers, body)
|
||||||
|
status % 100 == 4 and headers['X-Cascade'.freeze] == 'pass'.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def matching_env(env)
|
||||||
|
for_v3 = from_prefix(env) || from_accept(env) || from_version_header(env)
|
||||||
|
for_v3 == true ? env : for_v3
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_prefix(env)
|
||||||
|
return unless prefix and env['PATH_INFO'.freeze].start_with?(prefix)
|
||||||
|
env.merge({
|
||||||
|
'SCRIPT_NAME'.freeze => env['SCRIPT_NAME'.freeze] + prefix,
|
||||||
|
'PATH_INFO'.freeze => env['PATH_INFO'.freeze][prefix.size..-1]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_accept(env)
|
||||||
|
env['HTTP_ACCEPT'.freeze].include?(accept) if accept and env.include?('HTTP_ACCEPT'.freeze)
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_version_header(env)
|
||||||
|
env[version_header] == '3'.freeze if version_header and env.include?(version_header)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
9
lib/travis/api/v3/result.rb
Normal file
9
lib/travis/api/v3/result.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
class Result
|
||||||
|
attr_accessor :resource
|
||||||
|
|
||||||
|
def initialize(resource = nil)
|
||||||
|
@resource = resource
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
20
lib/travis/api/v3/router.rb
Normal file
20
lib/travis/api/v3/router.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
class Router
|
||||||
|
not_found = '{"error":{"message":"not found"}}'.freeze
|
||||||
|
headers = { 'Content-Type'.freeze => 'application/json'.freeze, 'X-Cascade'.freeze => 'pass'.freeze, 'Content-Length'.freeze => not_found.bytesize }
|
||||||
|
NOT_FOUND = [ 404, headers, not_found ]
|
||||||
|
|
||||||
|
attr_accessor :routs, :not_found
|
||||||
|
|
||||||
|
def initialize(routes = Routes, not_found: NOT_FOUND)
|
||||||
|
@routes = routes
|
||||||
|
@not_found = not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
access_control = AccessControl.new(env)
|
||||||
|
p access_control
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
lib/travis/api/v3/routes.rb
Normal file
10
lib/travis/api/v3/routes.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
module Routes
|
||||||
|
require 'travis/api/v3/routes/dsl'
|
||||||
|
extend DSL
|
||||||
|
|
||||||
|
resource :repository do
|
||||||
|
get :find_repository
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
lib/travis/api/v3/routes/dsl.rb
Normal file
5
lib/travis/api/v3/routes/dsl.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
module Routes::DSL
|
||||||
|
def resource(*) end
|
||||||
|
end
|
||||||
|
end
|
6
lib/travis/api/v3/service.rb
Normal file
6
lib/travis/api/v3/service.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
class Service
|
||||||
|
def self.params(*)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
4
lib/travis/api/v3/services.rb
Normal file
4
lib/travis/api/v3/services.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
module Services
|
||||||
|
end
|
||||||
|
end
|
22
lib/travis/api/v3/services/find_repository.rb
Normal file
22
lib/travis/api/v3/services/find_repository.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module Travis::API::V3
|
||||||
|
class Services::FindRepository < Service
|
||||||
|
params :id, :github_id, :slug, optional: true
|
||||||
|
|
||||||
|
def run
|
||||||
|
raise NotFound unless repository and access_control.visible? repository
|
||||||
|
Result.new(repository)
|
||||||
|
end
|
||||||
|
|
||||||
|
def repository
|
||||||
|
raise EntityMissing if defined?(@repository) and @repository.nil?
|
||||||
|
@repository ||= find_repository
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_repository
|
||||||
|
return ::Repository.find_by_id(id) if id
|
||||||
|
return ::Repository.find_by_github_id(github_id) if github_id
|
||||||
|
return ::Repository.by_slug(slug).first if slug
|
||||||
|
raise WrongParams
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user