use API keys for user basic auth; refactoring

pull/2978/head
Markus Kahl 10 years ago
parent 2b8848dc8b
commit d4dfdb9dcc
  1. 21
      config/initializers/warden.rb
  2. 46
      lib/open_project/authentication.rb
  3. 45
      lib/open_project/authentication/manager.rb
  4. 12
      lib/warden/strategies/basic_auth_failure.rb
  5. 14
      lib/warden/strategies/global_basic_auth.rb
  6. 23
      lib/warden/strategies/user_basic_auth.rb
  7. 13
      spec/factories/token_factory.rb
  8. 23
      spec/lib/api/v3/authentication_spec.rb

@ -1,7 +1,22 @@
# Strategies provided by OpenProject:
require 'warden/strategies/basic_auth_failure'
require 'warden/strategies/global_basic_auth'
require 'warden/strategies/user_basic_auth'
require 'warden/strategies/session'
Warden::Strategies.add :global_basic_auth, Warden::Strategies::GlobalBasicAuth
Warden::Strategies.add :user_basic_auth, Warden::Strategies::UserBasicAuth
Warden::Strategies.add :session, Warden::Strategies::Session
strategies = [
[:basic_auth_failure, Warden::Strategies::BasicAuthFailure],
[:global_basic_auth, Warden::Strategies::GlobalBasicAuth],
[:user_basic_auth, Warden::Strategies::UserBasicAuth],
[:session, Warden::Strategies::Session]
]
strategies.each do |name, clazz|
Warden::Strategies.add name, clazz
end
include OpenProject::Authentication::Scope
OpenProject::Authentication.update_strategies(API_V3) do |_strategies|
[:global_basic_auth, :user_basic_auth, :basic_auth_failure, :session]
end

@ -0,0 +1,46 @@
require 'open_project/authentication/manager'
module OpenProject
##
# OpenProject uses Warden strategies for request authentication.
module Authentication
class << self
##
# Updates the used warden strategies for a given scope. The strategies will be tried
# in the order they are set here.
# For available scopes please refer to `OpenProject::Authentication::Scope`.
#
# @param [Symbol] scope The scope for which to update the used warden strategies.
# @param [Boolean] store Indicates whether the user should be stored in the session for this scope.
#
# @yield [strategies] A block returning the strategies to be used for this scope.
# @yieldparam [Array] The strategies currently used by this scope. May be empty.
# @yieldreturn [Array] The strategies to be used by this scope.
def update_strategies(scope, store: nil, &block)
raise ArgumentError, "invalid scope: #{scope}" unless Scope.values.include? scope
current_strategies = Array(Manager.scope_strategies[scope])
Manager.store_defaults[scope] = store unless store.nil?
Manager.scope_strategies[scope] = block.call current_strategies if block_given?
end
end
##
# This module is only there to declare all used scopes. Technically a scope can be an
# arbitrary symbol. But we declare them here not to lose sight of them.
#
# Plugins can declare new scopes by declaring new constants in this module.
module Scope
API_V3 = :api_v3
class << self
def values
constants.map do |name|
const_get name
end
end
end
end
end
end

@ -1,31 +1,13 @@
module OpenProject
module Authentication
class Manager < Warden::Manager
def self.strategies
@strategies ||= begin
hash = {
api_v3: [:global_basic_auth, :user_basic_auth, :session]
}
hash.tap do |h|
h.default = []
end
end
end
def self.register_strategy(name, clazz, scopes)
Warden::Strategies.add name, clazz
scopes.each do |scope|
strategies[scope] << name
end
serialize_into_session do |user|
user.id
end
def self.configure(config)
config.default_strategies :session
config.failure_app = OpenProject::Authentication::FailureApp
config.scope_defaults :api_v3, strategies: strategies[:api_v3], store: false
serialize_from_session do |id|
User.find id
end
def initialize(app, options={}, &configure)
@ -37,6 +19,25 @@ module OpenProject
super app, options, &block
end
class << self
def scope_strategies
@scope_strategies ||= {}
end
def store_defaults
@store_defaults ||= Hash.new false
end
def configure(config)
config.default_strategies :session
config.failure_app = OpenProject::Authentication::FailureApp
scope_strategies.each do |scope, strategies|
config.scope_defaults scope, strategies: strategies, store: store_defaults[scope]
end
end
end
end
end
end

@ -0,0 +1,12 @@
module Warden
module Strategies
##
# This strategy is inserted after optional basic auth strategies to
# indicate that invalid basic auth credentials were provided.
class BasicAuthFailure < BasicAuth
def authenticate_user(_username, _password)
nil # always fails
end
end
end
end

@ -2,6 +2,20 @@ require 'warden/basic_auth'
module Warden
module Strategies
##
# Allows authentication via a singular set of basic auth credentials for admin access.
#
# The credentials must be configured in `config/configuration.yml` like this:
#
# production:
# authentication:
# global_basic_auth:
# user: admin
# password: 123456
#
# The strategy will only be triggered when the configured user name is sent.
# Meaning that this strategy is skipped if a basic auth attempt involving any
# other user name is made.
class GlobalBasicAuth < BasicAuth
def self.configuration
config = Hash(OpenProject::Configuration['authentication'])

@ -2,11 +2,28 @@ require 'warden/basic_auth'
module Warden
module Strategies
##
# Allows users to authenticate using their API key via basic auth.
# Note that in order for a user to be able to generate one
# `Setting.rest_api_enabled` has to be `1`.
#
# The basic auth credentials are expected to contain the literal 'apikey'
# as the user name and the API key as the password.
class UserBasicAuth < BasicAuth
def authenticate_user(username, password)
user = User.find_by_login username
def self.user
'apikey'
end
def valid?
super && username == self.class.user
end
def authenticate_user(_, api_key)
token(api_key).try(:user)
end
user if user && user.check_password?(password)
def token(value)
Token.where(action: 'api', value: value).first
end
end
end

@ -26,8 +26,19 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'securerandom'
FactoryGirl.define do
factory :token do
# doesn't need anything
user
value { SecureRandom.hex(16) }
factory :api_key do
action 'api'
end
factory :rss_key do
action 'rss'
end
end
end

@ -92,15 +92,10 @@ describe API::V3, type: :request do
it_behaves_like 'it is basic auth protected'
describe 'user basic auth' do
let(:username) { 'hans' }
let(:password) { 'bambidibam' }
let!(:api_user) do
FactoryGirl.create :user,
login: username,
password: password,
password_confirmation: password
end
let(:api_key) { FactoryGirl.create :api_key }
let(:username) { 'apikey' }
let(:password) { api_key.value }
# check that user basic auth is tried when global basic auth fails
it_behaves_like 'it is basic auth protected'
@ -117,12 +112,8 @@ describe API::V3, type: :request do
let(:username) { 'hancholo' }
let(:password) { 'olooleol' }
let!(:api_user) do
FactoryGirl.create :user,
login: 'user_account',
password: 'user_password',
password_confirmation: 'user_password'
end
let(:api_user) { FactoryGirl.create :user, login: 'user_account' }
let(:api_key) { FactoryGirl.create :api_key, user: api_user }
before do
authentication = {
@ -175,7 +166,7 @@ describe API::V3, type: :request do
context 'with valid user credentials' do
before do
get resource, {}, basic_auth('user_account', 'user_password')
get resource, {}, basic_auth('apikey', api_key.value)
end
it 'should return 200 OK' do

Loading…
Cancel
Save