7. A deeper look at dynamic rules

The @Dynamic annotation and @dynamic template tag allow you to specify arbitrary rules to control access. For example:

  • Simple (contrived) examples
    • Allow access if today is Tuesday
    • Allow access if a user has a date of birth defined
  • Some more complex examples
    • Allow access if the user is a beta tester AND the controller/action/template area is available for beta testing
    • Allow a maximum of 300 requests per hour per API key
  • Blacklisting
    • Deny access only if user registered within the last 30 days

7.1 Using sessions and requests in your rules

In order to make these decisions, information is helpful. You may need access to the session, or the current request. Both are available from the Http.Context object passed into your DynamicResourceHandler (DRH) implementations.

Sessions

In Play, sessions implement the java.util.Map interface and so data can get accessed in the usual way, i.e. session.get(key).

 1 public class MyDynamicResourceHandler implements DynamicResourceHandler
 2 {
 3     public F.Promise<Boolean> isAllowed(final String name,
 4                                         final String meta,
 5                                         final DeadboltHandler deadboltHandler,
 6                                         final Http.Context ctx)
 7     {
 8 	    return F.Promise.promise(() -> ctx.session())
 9 	                    .map(session -> // and so on
10 	    ...
11     }
12 							 
13 
14     public boolean checkPermission(String permissionValue,
15                                    DeadboltHandler deadboltHandler,
16                                    Http.Context ctx)
17     {
18 	    return F.Promise.promise(() -> ctx.session())
19 	                    .map(session -> // and so on
20 	    ...
21     }
22 }

You can also store write data into the session - just don’t forget that sessions are stored as client-side cookies! Any and all confidential information should be kept out of sessions.

Request query parameters

Using the Http.Request#queryString() method, you can get a map of all query parameters for the current request. For example, for the URL

1 http://localhost:9000/users?userName=foo

a call to queryString() would result in a map containing the key userName mapped to the value foo.

 1 public class MyDynamicResourceHandler implements DynamicResourceHandler
 2 {
 3     public F.Promise<Boolean> isAllowed(final String name,
 4                                         final String meta,
 5                                         final DeadboltHandler deadboltHandler,
 6                                         final Http.Context ctx)
 7     {
 8 	    return F.Promise.promise(() -> ctx.request())
 9 	                    .map(request -> request.queryString())
10 	                    .map((Map<String, String> queryStrings) -> // and so on
11 		...
12     }
13 							 
14 
15     public F.Promise<Boolean> checkPermission(final String permissionValue,
16                                               final DeadboltHandler deadboltHandler,
17                                               final Http.Context ctx)
18     {
19 	    return F.Promise.promise(() -> ctx.request())
20 	                    .map(request -> request.queryString())
21 	                    .map((Map<String, String> queryStrings) -> // and so on
22 		...
23     }
24 }

A major problem here is that your DRH needs knowledge of the methods - or, more precisely, the parameters of the method - to which it is applied. This can be alleviated somewhat through the use of convention, or by passing metadata into the DRH. Both will be covered later in this chapter.

Request bodies

When using a HTTP POST or PUT operation, the request body will typically contain information. Bodies can be accessed in a variety of formats, including custom formats.

 1 public class MyDynamicResourceHandler implements DynamicResourceHandler
 2 {
 3     public F.Promise<Boolean> isAllowed(final String name,
 4                                         final String meta,
 5                                         final DeadboltHandler deadboltHandler,
 6                                         final Http.Context ctx)
 7     {
 8         return F.Promise.promise(() -> ctx.request().body())
 9                         .map(body -> body.asJson())
10                         .map( // and so on
11 		...
12     }
13 							 
14 
15     public F.Promise<Boolean> checkPermission(final String permissionValue,
16                                               final DeadboltHandler deadboltHandler,
17                                               final Http.Context ctx)
18     {
19         return F.Promise.promise(() -> ctx.request().body())
20                         .map(body -> body.as(MyCustomType.class))
21                         .map(myCustomType -> // and so on
22 		...
23     }
24 }

Once you have obtained the information from the body, you’re back in the decision-making process of your rule.

Request path parameters

Path parameters are, unfortunately, the point at which we hit a problem. Play encourages developers to create clean, RESTful URLs, for example

1 http://localhost:9000/users/foo

in place of the query parameter example used earlier,

1 http://localhost:9000/users?userName=foo

The benefits of clean URLs are many - they can be bookmarked, are easily shared and are more easily cached, to name just a few. The one thing that path parameters are not, in Play, is available at runtime. There is no way in Play to get these parameters without deriving them manually by comparing the route definition with the actual URL.

It has been stated on the Play Framework group that path parameters should not be accessible by calling Request#queryStrings(), because path parameters are not query parameters. This is correct, but doesn’t really help in the real world - things will be much easier when path parameters can be accessed as easily as other request information.

7.2 Strategies for using dynamic resource handlers

To the best of my knowledge, there are two ways in which to use dynamic resource handlers in Deadbolt:

  1. Use a single DynamicResourceHandler that deals with all dynamic security
  2. Use a single DynamicResourceHandler as a façade

1. Use a single, potentially huge DRH

A friend of mine, sitting in a second-year university course discussion on object-oriented design, witnessed a student put up his hand and say, “I don’t get it. Why don’t we just put everything in one big class?”. If you would ask a similar question, this is the approach for you. For the rest of us, I think we can all appreciate that any DRH dealing with more than a couple of separate dynamic restrictions would get very large, very quickly.

2. Use a single DRH façade that dispatches to other DRHs

If you have a single DRH acting as a façade, you can aggregate or compose logic into discrete chunks. Furthermore, by extending AbstractDynamicResourceHandler, you can create specific handlers for individual methods in the façade, i.e. isAllowed-specific handlers and checkPermission-specific ones.

 1 public class FacadeDRH implements DynamicResourceHandler
 2 {
 3     private final Map<String, DynamicResourceHandler> allowed = new HashMap<>();
 4     private final Map<String, DynamicResourceHandler> permitted = new HashMap<>();
 5 
 6     public FacadeDRH()
 7     {
 8         // populate the maps, either through new instance creation, constructor parameters\
 9 , etc
10     }
11 
12     public F.Promise<Boolean> isAllowed(final String name,
13                                         final String meta,
14                                         final DeadboltHandler handler,
15                                         final Http.Context ctx)
16     {
17         return allowed.get(name).isAllowed(name,
18                                            meta,
19                                            handler,
20                                            ctx);
21     }
22 
23 
24     public F.Promise<Boolean> checkPermission(final String permissionValue,
25                                               final DeadboltHandler handler,
26                                               final Http.Context ctx)
27     {
28         return permitted.get(permissionValue).checkPermission(permissionValue,
29                                                               handler,
30                                                               ctx);
31     }
32 }