start working on API v3

This commit is contained in:
Konstantin Haase 2015-01-13 15:22:20 +01:00
parent ed46d49f24
commit a65792ee49
20 changed files with 392 additions and 7 deletions

View File

@ -68,15 +68,15 @@ GIT
GIT
remote: git://github.com/travis-ci/travis-support.git
revision: 40365216662f639d36fc3a0463c4e189ee1563dd
revision: 4fdd220ed7b06a12951e5d74a763c05a80eb0d20
specs:
travis-support (0.0.1)
GIT
remote: git://github.com/travis-ci/travis-yaml.git
revision: 08ec0c4d0cf3366cd971d4acd9aadbc0db68f85d
revision: f3aa306016a08b66a487f966eb8aa3a60ee9b319
specs:
travis-yaml (0.1.0)
travis-yaml (0.2.0)
PATH
remote: .

View File

@ -20,6 +20,7 @@ require 'metriks/librato_metrics_reporter'
require 'travis/support/log_subscriber/active_record_metrics'
require 'fileutils'
require 'travis/api/v2/http'
require 'travis/api/v3'
# Rack class implementing the HTTP API.
# Instances respond to #call.
@ -110,11 +111,17 @@ module Travis::Api
env['travis.global_prefix'] = env['SCRIPT_NAME']
end
use Travis::Api::App::Middleware::ScopeCheck
use Travis::Api::App::Middleware::Logging
use Travis::Api::App::Middleware::Metriks
use Travis::Api::App::Middleware::Rewrite
use Travis::Api::App::Middleware::ScopeCheck
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
if Travis.config.endpoints.ssh_key

View File

@ -17,7 +17,7 @@ class Travis::Api::App
# Landing point. Redirects web browsers to [API documentation](#/docs/).
get '/' do
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' }
end

13
lib/travis/api/v3.rb Normal file
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -0,0 +1,9 @@
module Travis::API::V3
class Result
attr_accessor :resource
def initialize(resource = nil)
@resource = resource
end
end
end

View 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

View 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

View File

@ -0,0 +1,5 @@
module Travis::API::V3
module Routes::DSL
def resource(*) end
end
end

View File

@ -0,0 +1,6 @@
module Travis::API::V3
class Service
def self.params(*)
end
end
end

View File

@ -0,0 +1,4 @@
module Travis::API::V3
module Services
end
end

View 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