Merge pull request #97 from sschneid/basic_auth

Add basic HTTP authentication and /token routes
This commit is contained in:
Roger Ignazio 2015-04-22 11:39:24 -07:00
commit 8cd49d215b
4 changed files with 296 additions and 0 deletions

View file

@ -4,6 +4,60 @@ module Vmpooler
module Helpers module Helpers
def protected!
return if authorized?
content_type :json
result = { 'ok' => false }
headers['WWW-Authenticate'] = 'Basic realm="Authentication required"'
halt 401, JSON.pretty_generate(result)
end
def authorized?
@auth ||= Rack::Auth::Basic::Request.new(request.env)
if @auth.provided? and @auth.basic? and @auth.credentials
username, password = @auth.credentials
if authenticate(Vmpooler::API.settings.config[:auth], username, password)
return true
end
end
return false
end
def authenticate(auth, username_str, password_str)
case auth['provider']
when 'ldap'
require 'rubygems'
require 'net/ldap'
ldap = Net::LDAP.new(
:host => auth[:ldap]['host'],
:port => auth[:ldap]['port'] || 389,
:encryption => {
:method => :start_tls,
:tls_options => { :ssl_version => 'TLSv1' }
},
:base => auth[:ldap]['base'],
:auth => {
:method => :simple,
:username => "#{auth[:ldap]['user_object']}=#{username_str},#{auth[:ldap]['base']}",
:password => password_str
}
)
if ldap.bind
return true
end
end
return false
end
def mean(list) def mean(list)
s = list.map(&:to_f).reduce(:+).to_f s = list.map(&:to_f).reduce(:+).to_f
(s > 0 && list.length > 0) ? s / list.length.to_f : 0 (s > 0 && list.length > 0) ? s / list.length.to_f : 0

View file

@ -11,6 +11,18 @@ module Vmpooler
call env.merge("PATH_INFO" => "/api/v#{api_version}/summary") call env.merge("PATH_INFO" => "/api/v#{api_version}/summary")
end end
post '/token/?' do
call env.merge("PATH_INFO" => "/api/v#{api_version}/token")
end
get '/token/:token/?' do
call env.merge("PATH_INFO" => "/api/v#{api_version}/token/#{params[:token]}")
end
delete '/token/:token/?' do
call env.merge("PATH_INFO" => "/api/v#{api_version}/token/#{params[:token]}")
end
get '/vm/?' do get '/vm/?' do
call env.merge("PATH_INFO" => "/api/v#{api_version}/vm") call env.merge("PATH_INFO" => "/api/v#{api_version}/vm")
end end

View file

@ -173,6 +173,74 @@ module Vmpooler
JSON.pretty_generate(result) JSON.pretty_generate(result)
end end
get "#{api_prefix}/token/:token/?" do
content_type :json
status 404
result = { 'ok' => false }
if Vmpooler::API.settings.config[:auth]
status 401
protected!
token = Vmpooler::API.settings.redis.hgetall('vmpooler__token__' + params[:token])
if not token.nil? and not token.empty?
status 200
result = { 'ok' => true, params[:token] => token }
else
status 404
end
end
JSON.pretty_generate(result)
end
delete "#{api_prefix}/token/:token/?" do
content_type :json
status 404
result = { 'ok' => false }
if Vmpooler::API.settings.config[:auth]
status 401
protected!
if Vmpooler::API.settings.redis.del('vmpooler__token__' + params[:token]).to_i > 0
status 200
result['ok'] = true
end
end
JSON.pretty_generate(result)
end
post "#{api_prefix}/token" do
content_type :json
status 404
result = { 'ok' => false }
if Vmpooler::API.settings.config[:auth]
status 401
protected!
o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
result['token'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
Vmpooler::API.settings.redis.hset('vmpooler__token__' + result['token'], 'user', @auth.username)
Vmpooler::API.settings.redis.hset('vmpooler__token__' + result['token'], 'timestamp', Time.now)
status 200
result['ok'] = true
end
JSON.pretty_generate(result)
end
get "#{api_prefix}/vm/?" do get "#{api_prefix}/vm/?" do
content_type :json content_type :json

View file

@ -0,0 +1,162 @@
require 'spec_helper'
require 'rack/test'
module Vmpooler
class API
module Helpers
def authenticate(auth, username_str, password_str)
username_str == 'admin' and password_str == 's3cr3t'
end
end
end
end
describe Vmpooler::API::V1 do
include Rack::Test::Methods
def app()
Vmpooler::API
end
describe '/token' do
let(:redis) { double('redis') }
let(:prefix) { '/api/v1' }
before do
app.settings.set :config, config
app.settings.set :redis, redis
end
describe 'GET /token/:token' do
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'returns a 404' do
get "#{prefix}/token/this"
expect(last_response).not_to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
expect(last_response.status).to eq(404)
end
end
context '(auth configured)' do
before do
allow(redis).to receive(:hgetall).and_return 'atoken'
end
let(:config) { { auth: true } }
it 'returns a 401 if not authed' do
get "#{prefix}/token/this"
expect(last_response).not_to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
expect(last_response.status).to eq(401)
end
it 'returns a token if authed' do
authorize 'admin', 's3cr3t'
get "#{prefix}/token/this"
expect(last_response).to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => true, 'this' => 'atoken'}))
expect(last_response.status).to eq(200)
end
end
end
describe 'POST /token' do
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'returns a 404' do
post "#{prefix}/token"
expect(last_response).not_to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
expect(last_response.status).to eq(404)
end
end
context '(auth configured)' do
before do
allow(redis).to receive(:hset).and_return '1'
end
let(:config) { { auth: true } }
it 'returns a 401 if not authed' do
post "#{prefix}/token"
expect(last_response).not_to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
expect(last_response.status).to eq(401)
end
it 'returns a token if authed' do
authorize 'admin', 's3cr3t'
post "#{prefix}/token"
expect(last_response).to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(JSON.parse(last_response.body)['ok']).to eq(true)
expect(JSON.parse(last_response.body)['token'].length).to be(32)
expect(last_response.status).to eq(200)
end
end
end
describe 'DELETE /token/:token' do
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'returns a 404' do
delete "#{prefix}/token/this"
expect(last_response).not_to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
expect(last_response.status).to eq(404)
end
end
context '(auth configured)' do
before do
allow(redis).to receive(:del).and_return '1'
end
let(:config) { { auth: true } }
it 'returns a 401 if not authed' do
delete "#{prefix}/token/this"
expect(last_response).not_to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
expect(last_response.status).to eq(401)
end
it 'deletes a token if authed' do
authorize 'admin', 's3cr3t'
delete "#{prefix}/token/this"
expect(last_response).to be_ok
expect(last_response.header['Content-Type']).to eq('application/json')
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => true}))
expect(last_response.status).to eq(200)
end
end
end
end
end