Module: Strelka::App::Auth

Extended by:
Configurability, Loggability, MethodUtilities, Plugin
Includes:
Constants
Defined in:
lib/strelka/app/auth.rb

Overview

Pluggable authentication and authorization for Strelka applications.

Enabling the :auth plugin by default causes all requests to your handler to go through an authentication and authorization provider first. This provider checks the request for the necessary credentials, then either forwards it on if sufficient conditions are met, or responds with the appropriate 4xx status response.

The conditions are broken down into two stages:

  • Authentication -- the client is who they say they are
  • Authorization -- the client is allowed to access the resources in question

Auth providers are plugins that are named Strelka::AuthProvider::, and inherit from Strelka::AuthProvider. In order for them to be discoverable, each one should be in a file named lib/strelka/authprovider/.rb. They can implement one or both of the stages; see the API docs for Strelka::AuthProvider for details on how to write your own plugin.

The provider for an application can be specified in the Configurability config file under the 'auth' section:

---
auth:
  provider: basic

Applying Authentication

The default authentication policy is to require authentication from every request, but sometimes you may wish to narrow the restrictions a bit.

Relaxing Auth for A Few Methods

Sometimes you want to expose just one or two resources to the world, say in the case of a REST API that includes the authentication endpoint. Obviously, clients can't be authenticated until after they send their request that authenticates them, so you can expose just the /login URI by using the 'no_auth_for' directive:

class MyService < Strelka::App
    plugins :auth
    no_auth_for '/login'

    # ...
end

A String or a Regexp argument will be used to match against the request's #app_path (the path of the request URI with the Mongrel2 route omitted), and any requests which match are sent along as-is. A String will match the path exactly, with any leading or trailing '/' characters removed, and a Regexp will be tested against the #app_path as-is.

If you require some more-complex criteria for determining if the request should skip the auth plugin, you can provide a block to no_auth_for instead.

# Allow requests from 'localhost' without auth, but require it from
# everywhere else
no_auth_for do |request|
    return 'internal-user' if request.header.x_forwarded_for == '127.0.0.1'
end

If the block returns a true-ish value, it will be used in the place of the authenticated username and the request will be handed to your app.

Returning a false-ish value will go ahead with the rest of the auth processing.

You can also combine String and Regexp arguments with a block to further refine the conditions:

# Allow people to visit the seminar registration view without an account
# if there are still slots open
no_auth_for( '/register' ) do |request|
    if Seminars.any? {|seminar| !seminar.full? }
        'register'
    else

    end
end

Relaxing Auth for All But a Few Methods

Sometimes, though, you want just the opposite -- a few methods are available only to a select few, but the majority are unrestricted.

To do this, use the 'require_auth_for' directive:

class MyBlog < Strelka::App
    plugins :auth
    require_auth_for '/admin'

    # ...
end

Note that this inverts the usual behavior of the :auth plugin: resources will, by default, be unguarded, so be sure you keep this in mind when using require_auth_for.

Like no_auth_for, require_auth_for can also take a block, and a true-ish return value will cause the request to pass through the AuthProvider.

You can't use no_auth_for and require_auth_for in the same application; doing so will result in a ScriptError being raised when the application is loaded.

Adding Authorization

Sometimes simple authentication isn't sufficient for accessing some resources, especially if you have some kind of permissions system that dictates who can see/use what. That's where the second stage of the auth process comes into play: Authorization.

The AuthProvider you're using may provide some form of general authorization itself (especially a custom one), but typically authorization is particular to an application and even particular actions within the application.

To facilitate mapping out what actions are available to whom, there is a declaration similar to require_auth_for that can define a set of permissions that are necessary for a request to be allowed:

# The app ID, which is the default permission
ID = 'gemserver'

# GET /app/admin/upload/install would require:
#   :gemserver, :admin, :upload, and :install
# permissions. What those mean is up to the AuthProvider.
require_perms_for ''
require_perms_for %r{^/admin.*}, :admin
require_perms_for %r{/upload}, :upload
require_perms_for %r{/install}, :install

and its negative corollary:

no_perms_for '/login'

Incoming requests are matched against require_perms_for patterns, and the union of all matching permissions is gathered, then any no_auth_for patterns are used to remove permissions from that set.

If no require_perms_for patterns are declared, authorization is not checked, unless there is at least one no_perms_for pattern, in which case all requests that don't match the negative patterns are checked (with the permission set to the ID of the app).

Authorization will be checked once authentication has succeeded. It will be called with at least the credentials object returned from the authentication stage and the request object. Some AuthProviders may opt to return authentication credentials as a User object of some kind (e.g., a database row, LDAP entry, model object, etc.), but the simpler ones just return the login of the authenticated user. The AuthProvider may also furnish additional useful arguments such as a database handle, permission objects, etc. to your authorization block. See the documentation for your chosen AuthProvider for details.

Customizing Failure

As mentioned before, an authentication or authorization failure results in a 4xx status response. By default Strelka will present this back to the browser as a simple error response, but oftentimes you will want to customize it to look a little nicer, or to behave in a more-intuitive way. The easiest way to do this is to use the :errors plugin.

Redirecting to a Form

If you're using form-based session authentication (as opposed to basic auth, which has its own UI), you can rewrite the response to instruct the browser to go to a static HTML form instead using the :errors plugin:

class FormAuthApp < Strelka::App
   plugins :errors, :auth, :sessions

   on_status HTTP::AUTH_REQUIRED do |res, status|
       formuri = res.request.uri
       formuri.path = '/loginform.html'

       res.reset
       res.status = HTTP::SEE_OTHER
       res.content_type = 'text/plain'
       res.puts "This resource requires authentication."
       res.header.location = formuri

       return res
   end
end

Responding With a Form

With the addition of the :templating plugin, you can respond with the form directly instead:

class TemplateFormAuthApp < Strelka::App
   plugins :auth, :errors, :templating

   layout 'examples/layout.tmpl'
   templates \
       form: 'examples/auth-form.tmpl',
       success: 'examples/auth-success.tmpl'

   on_status HTTP::AUTH_REQUIRED, :form

   ### Handle any (authenticated) HTTP request
   def handle_request( req )
       return :success
   end

end

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

DEFAULT_AUTH_PROVIDER =

The name of the default plugin to use for authentication

:hostaccess
CONFIG_DEFAULTS =

Configuration defaults

{
  provider: DEFAULT_AUTH_PROVIDER,
}

Instance Attribute Summary collapse

Attributes included from Plugin

#pluggable, #successors

Class Method Summary collapse

Instance Method Summary collapse

Methods included from MethodUtilities

attr_predicate, attr_predicate_accessor, singleton_attr_accessor, singleton_attr_reader, singleton_attr_writer, singleton_method_alias, singleton_predicate_accessor, singleton_predicate_reader

Methods included from Plugin

extended, plugin_name, run_inside, run_outside

Instance Attribute Details

#auth_providerObject (readonly)

The instance of (a subclass of) Strelka::AuthProvider that provides authentication logic for the app.



468
469
470
# File 'lib/strelka/app/auth.rb', line 468

def auth_provider
  @auth_provider
end

Class Method Details

.configure(config = nil) ⇒ Object

Configurability API -- configure the Auth plugin via the 'auth' section of the unified config.



260
261
262
263
264
265
266
267
268
269
# File 'lib/strelka/app/auth.rb', line 260

def self::configure( config=nil )
  if config && config[:provider]
    self.log.debug "Setting up the %p AuthProvider for apps: %p" %
      [ config[:provider], self.extended_apps ]
    self.extended_apps.each {|app| app.auth_provider = config[:provider] }
  else
    self.log.warn "Setting up the default AuthProvider for apps %p" % [ self.extended_apps ]
    self.extended_apps.each {|app| app.auth_provider = DEFAULT_AUTH_PROVIDER }
  end
end

.included(object) ⇒ Object

Extension callback -- extend the HTTPRequest class with Auth support when this plugin is loaded.



448
449
450
451
452
# File 'lib/strelka/app/auth.rb', line 448

def self::included( object )
  self.log.debug "Extending Request with Auth mixin"
  Strelka::HTTPRequest.class_eval { include Strelka::HTTPRequest::Auth }
  super
end

Instance Method Details

#authenticate_and_authorize(request) ⇒ Object

Process authentication and authorization for the specified request.



484
485
486
487
488
489
490
# File 'lib/strelka/app/auth.rb', line 484

def authenticate_and_authorize( request )
  credentials = nil
  credentials = self.provide_authentication( request ) if self.request_should_auth?( request )
  request.authenticated_user = credentials

  self.provide_authorization( credentials, request )
end

#default_permissionObject

Return a permission Symbol derived from the app's ID.



550
551
552
# File 'lib/strelka/app/auth.rb', line 550

def default_permission
  return self.app_id.downcase.gsub(/\W+/, '_' ).to_sym
end

#extended_appsObject

The Array of apps that have had the auth plugin installed; this is used to set up the AuthProvider when the configuration loads later.



254
# File 'lib/strelka/app/auth.rb', line 254

singleton_attr_accessor :extended_apps

#handle_request(request, &block) ⇒ Object

Check authentication and authorization for requests that need it before sending them on.



473
474
475
476
477
478
479
480
# File 'lib/strelka/app/auth.rb', line 473

def handle_request( request, &block )
  self.log.debug "[:auth] Wrapping request in auth with a %p" % [ self.auth_provider ]

  request.auth_provider = self.auth_provider
  self.authenticate_and_authorize( request )

  super
end

#initializeObject

Add an AuthProvider instance to the app.



456
457
458
459
# File 'lib/strelka/app/auth.rb', line 456

def initialize( * )
  super
  @auth_provider = self.class.auth_provider.new( self )
end

#perms_required_for(request) ⇒ Object Also known as: required_perms_for

Gather the set of permissions that apply to the specified request and return them.



557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# File 'lib/strelka/app/auth.rb', line 557

def perms_required_for( request )
  self.log.debug "Gathering required perms for: %s %s" % [ request.verb, request.app_path ]

  # Return the empty set if any negative auth criteria match
  return [] if self.negative_perms_criteria_match?( request )

  # If there aren't any positive criteria, default to requiring authorization with
  # the app's ID as the permission
  if self.class.positive_perms_criteria.empty?
    return [ self.default_permission ]
  end

  # Apply positive auth criteria
  return self.union_positive_perms_criteria( request )
end

#provide_authentication(request) ⇒ Object

If the AuthProvider does authentication, try to extract authenticated credentials from the request and return them, throwing a :finish with a properly-constructed 401 (Auth required) response if that fails.



496
497
498
499
500
501
# File 'lib/strelka/app/auth.rb', line 496

def provide_authentication( request )
  provider = self.auth_provider
  self.log.info "Authenticating request using provider: %p" % [ provider ]
  credentials = provider.authenticate( request ) or finish_with( HTTP::AUTH_REQUIRED, "Authentication required." )
  return credentials
end

#provide_authorization(credentials, request) ⇒ Object

Process authorization for the given credentials and request. The credentials argument is the opaque return value from a valid authentication, or nil if the request didn't require authentication.



507
508
509
510
511
512
# File 'lib/strelka/app/auth.rb', line 507

def provide_authorization( credentials, request )
  provider = self.auth_provider
  perms = self.perms_required_for( request )
  self.log.debug "Perms required: %p" % [ perms ]
  provider.authorize( credentials, request, perms ) unless perms.empty?
end

#request_should_auth?(request) ⇒ Boolean

Returns true if the given request requires authentication.

Returns:

  • (Boolean)


516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# File 'lib/strelka/app/auth.rb', line 516

def request_should_auth?( request )
  self.log.debug "Checking to see if Auth(entication/orization) should be applied for app_path: %p" %
    [ request.app_path ]

  # If there are positive criteria, return true if the request matches any of them,
  # or false if they don't
  if self.class.has_positive_auth_criteria?
    criteria = self.class.positive_auth_criteria
    self.log.debug "  checking %d positive auth criteria" % [ criteria.length ]
    return criteria.any? do |pattern, block|
      self.request_matches_criteria( request, pattern, &block )
    end
    return false

  # If there are negative criteria, return false if the request matches any of them,
  # or true if they don't
  elsif self.class.has_negative_auth_criteria?
    criteria = self.class.negative_auth_criteria
    self.log.debug "  checking %d negative auth criteria" % [ criteria.length ]
    return false if criteria.any? do |pattern, block|
      rval = self.request_matches_criteria( request, pattern, &block )
      self.log.debug "    matched: %p -> %p" % [ pattern, block ] if rval
      rval
    end
    return true

  else
    self.log.debug "  no auth criteria; default to requiring auth"
    return true
  end
end