Coder Social home page Coder Social logo

cbguard's Introduction

cbguard

Master Branch Build Status

This is in maintenance mode. Current features will be merged with cbsecurity.

Annotation driven guards for authentication and authorization in ColdBox

Usage

cbguard lets us lock down methods to logged in users and users with specific permissions using one annotation — secured. Just sticking the secured annotation on a handler or action is enough to require a user to log in before executing those events.

Here's an example of how to lock down an entire handler:

component secured {

    function index( event, rc, prc ) {
        // ...
    }

    function show( event, rc, prc ) {
        // ...
    }

}

You can be more specific and lock down only specific actions using the same annotation:

component {

    function create( event, rc, prc ) secured {
        // ...
    }

}

You can further lock down handlers and actions to a list of specific permissions. If specified, the logged in user must have one of the permissions in the list specified.

component secured="admin" {

    function index( event, rc, prc ) {
        // ...
    }

    function show( event, rc, prc ) {
        // ...
    }

}

In the above component, the user must have the admin permission to access the actions in this handler.

component {

    function show( event, rc, prc ) secured="admin,reviews_posts" {
        // ...
    }

}

Individual actions can be secured in the same way. Above, the show action requires the logged in user to have either the admin or the reviews_posts permission.

These two approaches can be combined and both handler and actions can be secured together:

component secured {

    function index( event, rc, prc ) {
        // ...
    }

    function new( event, rc, prc ) secured="create_posts" {
        // ...
    }

}

While the user needs to be logged in to interact at all with this handler, they also need the create_posts permission to interact with the new action.

Service approach

cbguard also allows you to check for authorization at any point in the request lifecycle using the Guard@cbguard component.

component secured {

    property name="guard" inject="@cbguard";

    function update( event, rc, prc ) {
        var post = getInstance( "Post" ).findOrFail( rc.post );

        // this will throw a `NotAuthorized` exception if the user cannot update the post
        guard.authorize( "update-post", { "post": post } );

        // update the post as normal...
    }

}

The methods available to you on the Guard component are as follows:

public boolean function allows( required any permissions, struct additionalArgs = {} );
public boolean function denies( required any permissions, struct additionalArgs = {} );
public boolean function all( required any permissions, struct additionalArgs = {} );
public boolean function none( required any permissions, struct additionalArgs = {} );
public void function authorize( required any permissions, struct additionalArgs = {}, string errorMessage );

In all cases permissions can be either a string, a list of strings, or an array of strings.

In the case of authorize the errorMessage replaces the thrown error message in the NotAuthorized. exception. It can also be a closure that takes the following shape:

string function errorMessage( array permissions, any user, struct additionalArgs );

Defining Custom Guards

While handling all of your guard clauses inside the hasPermission method on your user works fine, you may want to define a different way to handle permissions. You can do this by declaring custom guards using the guard.define method. Here's the signature:

public Guard function define( required string name, required any callback );

The name will match against a permission name. If it matches, the guard is called instead of calling hasPermission on the User model. (You can always call hasPermission on the User inside your guard callback if you need.)

The callback can be: a closure or UDF, a component with an authorize function, or a WireBox mapping to a component with an authorize function. Please note that the authorize function must be explicitly defined and public (No onMissingMethod). This authorize function is called with two parameters: the user being authorized and a struct of additionalArgs and must return a boolean, like so:

public boolean function authorize( required any user, struct additionalArgs = {} );

Using this approach, you can define custom guards anywhere in your application: config/ColdBox.cfc, ModuleConfig.cfc of your custom modules, etc. The Guard component is registered as a singleton, so it will keep track of all the guards registered, even from different sources.

If you have a need to remove a guard definition you can do so with the removeDefinition method:

public Guard function removeDefinition( required string name );

Redirects

When a user is denied access to a action, an event of your choosing is executed instead. There are four keys that can be set in the moduleSettings struct that all come with good defaults.

  1. authenticationOverrideEvent (Default: Main.onAuthenticationFailure)

This is the event that is executed when the user is not logged in and is attempting to execute a secured action, whether or not that handler or action has permissions.

  1. authorizationOverrideEvent (Default: same as authenticationOverrideEvent)

This is the event that is executed when the user is logged in and is attempting to execute a secured action but does not have the requisite permissions.

  1. authenticationAjaxOverrideEvent (Default: Main.onAuthenticationFailure)

This is the event that is executed when the user is not logged in and is attempting to execute a secured action via ajax (event.isAjax()), whether or not that handler or action has permissions. By default, this will execute the same action that is configured for authenticationOverrideEvent.

  1. authorizationAjaxOverrideEvent (Default: same as authorizationOverrideEvent)

This is the event that is executed when the user is logged in and is attempting to execute a secured action via ajax (event.isAjax()) but does not have the requisite permissions. By default, this will execute the same action that is configured for authorizationOverrideEvent.

_securedUrl

When an override event is used, the url the user was trying to access is stored in the flash scope as _securedUrl. You can make use of this in your login actions to send the user back where they intended after logging in.

Setup

cbguard requires a bit of setup to function properly.

First, there are two interfaces that must be followed:

  1. AuthenticationServiceInterface
interface {

    /**
    * Must return an object that conforms to `HasPermissionInterface`.
    * (This may be an implicit implements.)
    */
    public HasPermissionInterface function getUser();

    /**
    * Returns true if the user is logged in.
    */
    public boolean function isLoggedIn();

}
  1. HasPermissionInterface
interface {

    /**
    * Returns true if the user has the specified permission.
    * Any additional arguments may be passed in as the second argument.
    * This allows you to check if a user can access a specific resource,
    * rather than just a generic check.
    */
    public boolean function hasPermission( required string permission, struct additionalArgs );

}

Note: These interfaces are not enforced at compile time to give you maximum flexibility.

To configure the AuthenticationService, set the value of authenticationService in your moduleSettings to a WireBox mapping:

moduleSettings = {
    cbguard = {
        authenticationService = "SecurityService@myapp"
    }
};

The default authenticationService for cbguard is AuthenticationService@cbauth. cbauth follows the AuthenticationServiceInterface out of the box.

config/ColdBox.cfc Settings

You can change the method names called on the AuthenticationService and the returned User if you need to. We highly discourage this use case, as it makes it harder to utilize the cbguard conventions across projects. However, should the need arise, you can modify the method names as follows:

moduleSettings = {
    cbguard = {
        methodNames = {
            isLoggedIn    = "getIsLoggedIn",
            getUser       = "retrieveUser",
            hasPermission = "checkPermission"
        }
    }
};

Additionally, you can modify the override action for each of the event types:

moduleSettings = {
    cbguard = {
        overrideActions = {
            authenticationOverrideEvent = "relocate",
            authenticationAjaxOverrideEvent = "override",
            authorizationOverrideEvent = "relocate",
            authorizationAjaxOverrideEvent = "override"
        }
    }
};

relocate refers to calling relocate on the controller. The user will be redirected to the new page. override refers to event.overrideEvent. This will not redirect but simply change the running event.

Module Overrides

All of the cbguard settings can be overriden inside a module. This allows modules, such as an API module, to provide their own authentication services as well as redirect events.

To specify some overrides, create a cbguard struct in your desired module's settings in that module's ModuleConfig.cfc.

component {

    this.name = "myModule";

    function configure() {
        settings = {
            "cbguard" = {
                "authenticationOverrideEvent" = "myModule:Main.onAuthenticationFailure",
                "authorizationOverrideEvent" = "myModule:Main.onAuthorizationFailure"
            }
        };
    }

}

Local Handler Overrides

If an onAuthenticationFailure or onAuthorizationFailure method exists on the handler being secured, it will be used in the case of an authentication or authorization failure event, respectively.

// handlers/Admin.cfc
component secured {

    function index( event, rc, prc ) {
        event.setView( "admin/index" );
    }

    function secret( event, rc, prc ) secured="superadmin" {
        event.setView( "admin/secret" );
    }

    function onAuthenticationFailure( event, rc, prc ) {
        relocate( "/login" );
    }

    function onAuthenticationFailure( event, rc, prc ) {
        flash.put( "authorizationError", "You don't have the correct permissions to access that resource." );
        redirectBack(); // from the redirectBack module
    }

}

Override Order

cbguard will process your authorization and authentication failures in the following order:

  1. Inline handler methods (onAuthenticationFailure & onAuthorizationFailure within your handlers).
  2. cbguard settings in the ModuleConfig of the handler's module. (Overrides in modules_app/api/ModuleConfig.cfc when the handler is in the module, i.e. modules_app/api/handlers/Main.cfc.)
  3. Overrides in config/ColdBox.cfc using moduleSettings.
  4. Default settings for the module.

autoRegisterInterceptor

If you need more control over the order of your interceptors you can disable the automatic loading of the SecuredEventInterceptor interceptor. If you do this you will need to register it yourself (most likely in config/ColdBox.cfc) as cbguard.interceptors.SecuredEventInterceptor.

cbguard's People

Contributors

adrian-sanchez avatar bdw429s avatar elpete avatar gpickin avatar lmajano avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

cbguard's Issues

Introducing Cbguard Breaks Coldbox invalidEventHandler Setting

The Coldbox invalidEventHandler setting allows us to create friendly 404 pages in our apps. However, if you install cbguard in an app that uses invalidEventHandler, it will break the functionality and users will see the following exception error: "The invalidEventHandler setting is also invalid: error.onInvalidEvent. Please check your settings"

For reference (and screenshots) I brought this issue up on Google Groups awhile back.

I've done my best to try and solve this problem on my own by closely following a request with an invalid event both with and without cbguard installed. Even though I was not able to figure out how to fix the issue, I did learn a few things that might help.

First of all, I took a look at Coldbox's own handler service in coldbox\system\web\services\HandlerService.cfc and noticed that this error gets triggered when a global request variable, request._lastInvalidEvent matches the currently called event. This was set up to initially prevent infinite loops.

What I figured out is that when cbguard is installed, the invalidEvent() method gets executed twice causing request._lastInvalidEvent to be set two times which makes the function think the invalidEventHandler is invalid. I cannot figure out why cbguard triggers the HandlerService method twice.

Steps to Reproduce:
If you want to create a simple test setup to demonstrate the problem, create a blank coldbox app, and utilize the invalidEventHandler in your config. Then, confirm that you can show a friendly 404 error when an invalid event gets called.
Next, install cbsecurity and reinit your app. You should see the problem occur.

CBGuard errors if you have no handler / implicit handler

Instead of just skipping the handler that doesn't exist, this errors.
This could be an issue with implicit handlers.

This function will fail is the handlerBean.getHandler() is empty.

var handler = handlerService.getHandler(
            handlerBean,
            event
        );
Template: | C:\www\revagency\revagency-ap6-lucee\coldbox\system\core\util\Util.cfc
-- | --
LINE: | 288: <cfset md = getMetaData( component )>289: <cfelse>290: <cfset md = getComponentMetaData( component )>291: </cfif>292: </cfif>
Template: | C:\www\revagency\revagency-ap6-lucee\coldbox\system\ioc\config\Mapping.cfc
LINE: | 602: }603: else{604: var produceMetadataUDF = function() { return injector.getUtil().getInheritedMetaData(instance.path, binder.getStopRecursions()); };605: 606: // Are we caching metadata?
Template: | C:\www\revagency\revagency-ap6-lucee\coldbox\system\ioc\config\Mapping.cfc
LINE: | 612: );613: } else {614: md = produceMetadataUDF();615: }616: }
Template: | C:\www\revagency\revagency-ap6-lucee\coldbox\system\ioc\Injector.cfc
LINE: | 346: try {347: // process inspection of instance348: mapping.process( binder=variables.binder, injector=this );349: } catch( any e ) {350: // Remove bad mapping
Template: | C:\www\revagency\revagency-ap6-lucee\coldbox\system\web\services\HandlerService.cfc
LINE: | 110: 111: // retrieve, build and wire from wirebox112: return wirebox.getInstance( arguments.invocationPath );113: }114:
Template: | C:\www\revagency\revagency-ap6-lucee\coldbox\system\web\services\HandlerService.cfc
LINE: | 126: 127: // Create Runnable Object via WireBox128: var oEventHandler = newHandler( arguments.ehBean.getRunnable() );129: 130: /* ::::::::::::::::::::::::::::::::::::::::: EVENT METHOD TESTING :::::::::::::::::::::::::::::::::::::::::::: */
Template: | C:\www\revagency\revagency-ap6-lucee\modules\cbguard\interceptors\SecuredEventInterceptor.cfc
LINE: | 30: handlerBean,31: event32: );33: 34: var handlerMetadata = getMetadata( handler );

```

Add ability to customize guard responses by moudule

If a module implements cbguard, the module should have the ability to customize the responses for authorization failures, when within the modules routing namespace.

Allow for nested cbguard settings within each module's settings.

Example:

settings = {
            "cbguard" : {
                "authenticationService"           = "SecurityService@myModule",
                "authenticationOverrideEvent"     = "myModule:Main.onAuthenticationFailure",
                "authenticationAjaxOverrideEvent" = "myModule:api.v1.BaseAPIHandler.onAuthenticationFailure",
                "authorizationOverrideEvent"      = "myModule:Main.onAuthorizationFailure",
                "authorizationAjaxOverrideEvent"  = "myModule:api.v1.BaseAPIHandler.onAuthorizationFailure"
            }
}

If a customized module setting is not detected, then the top-level module settings would apply.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.