Strelka::App::

Auth

module
Included Modules
Strelka::Constants
Strelka::HTTPRequest::Auth
Extended With
Strelka::Plugin
Strelka::MethodUtilities
Loggability
Configurability

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:

Auth providers are plugins that are named Strelka::AuthProvider::<MyType>, and inherit from Strelka::AuthProvider. In order for them to be discoverable, each one should be in a file named lib/strelka/authprovider/<mytype>.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

Constants

CONFIG_DEFAULTS

Configuration defaults

DEFAULT_AUTH_PROVIDER

The name of the default plugin to use for authentication

Attributes

auth_provider[R]

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

Public Class Methods

anchor
configure( config=nil )

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

# File lib/strelka/app/auth.rb, line 259
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
anchor
included( object )

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

# File lib/strelka/app/auth.rb, line 447
def self::included( object )
        self.log.debug "Extending Request with Auth mixin"
        Strelka::HTTPRequest.class_eval { include Strelka::HTTPRequest::Auth }
        super
end
anchor
new( * )

Add an AuthProvider instance to the app.

# File lib/strelka/app/auth.rb, line 455
def initialize( * )
        super
        @auth_provider = self.class.auth_provider.new( self )
end

Public Instance Methods

anchor
authenticate_and_authorize( request )

Process authentication and authorization for the specified request.

# File lib/strelka/app/auth.rb, line 483
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
anchor
default_permission()

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

# File lib/strelka/app/auth.rb, line 549
def default_permission
        return self.app_id.downcase.gsub(/\W+/, '_' ).to_sym
end
anchor
extended_apps()

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

# File lib/strelka/app/auth.rb, line 253
singleton_attr_accessor :extended_apps
anchor
handle_request( request, &block )

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

# File lib/strelka/app/auth.rb, line 472
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
anchor
perms_required_for( request )

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

# File lib/strelka/app/auth.rb, line 556
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
Also aliased as: required_perms_for
anchor
provide_authentication( request )

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.

# File lib/strelka/app/auth.rb, line 495
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
anchor
provide_authorization( credentials, request )

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.

# File lib/strelka/app/auth.rb, line 506
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
anchor
request_should_auth?( request )

Returns true if the given request requires authentication.

# File lib/strelka/app/auth.rb, line 515
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
anchor
required_perms_for( request )
Alias for: perms_required_for

Protected Instance Methods

anchor
negative_perms_criteria_match?( request )

Returns true if the request matches at least one negative perms criteria whose block also returns true when called.

# File lib/strelka/app/auth.rb, line 609
def negative_perms_criteria_match?( request )
        self.log.debug "  negative perm criteria: %p" % [ self.class.negative_perms_criteria ]
        return self.class.negative_perms_criteria.any? do |pattern, block|
                self.request_matches_criteria( request, pattern, &block )
        end
end
anchor
request_matches_criteria( request, pattern ) { |request| ... }

Returns true if there are positive auth criteria and the request matches at least one of them.

# File lib/strelka/app/auth.rb, line 580
def request_matches_criteria( request, pattern )
        self.log.debug "Testing request '%s %s' against pattern: %p" %
                [ request.verb, request.app_path, pattern ]

        case pattern
        when nil
                self.log.debug "  no pattern; calling the block"
                return yield( request )

        when Regexp
                self.log.debug "  checking app_path with regexp: %p" % [ pattern ]
                matchdata = pattern.match( request.app_path ) or return false
                self.log.debug "  matched: calling the block"
                return yield( request, matchdata )

        when String
                self.log.debug "  checking app_path: %p" % [ pattern ]
                request.app_path.gsub( %r{^/+|/+$}, '' ) == pattern or return false
                self.log.debug "  matched: calling the block"
                return yield( request )

        else
                raise ScriptError, "don't know how to match a request with a %p" % [ pattern.class ]
        end
end
anchor
union_positive_perms_criteria( request )

Find all positive perm criteria, calling each one's block with request if its pattern matches path, and assembling a union of all the permission sets that result.

# File lib/strelka/app/auth.rb, line 620
def union_positive_perms_criteria( request )
        perms = []

        self.log.debug "  positive perm criteria: %p" % [ self.class.positive_perms_criteria ]
        self.class.positive_perms_criteria.each do |pattern, block, newperms|
                criteria = self.request_matches_criteria( request, pattern, &block )
                next unless criteria

                newperms = Array( newperms ).dup

                if criteria.is_a?( Symbol )
                        newperms << criteria

                elsif criteria.respond_to?( :first ) && criteria.first.is_a?( Symbol )
                        newperms += criteria
                end

                newperms << self.default_permission if newperms.empty?

                raise TypeError, "Permissions must be Symbols; got: %p" % [newperms] unless
                        newperms.all? {|perm| perm.is_a?(Symbol) }

                self.log.debug "  found new perms: %p" % [ newperms ]
                perms += newperms
        end

        return perms.compact.uniq
end