Adding annotation-based right checks to your controller

Sooner or later any business application will have some login/logout mechanism and after that there are different types of users. Some users are allowed to do certain things, others are not. This can be solved via a check in every controller, but is way too much overhead. This recipe shows a clean and fast (though somewhat limited) solution to creating security checks, without touching anything of the business logic inside your controller.

You can find the source code of this example in the chapter2/annotation-rights directory.

Getting ready

Again we will start with a test, which performs several checks for security:

public class UserRightTest extends FunctionalTest {

    @Test
    public void testSecretsWork() {
        login("user", "user");
        Response response = GET("/secret");
        assertIsOk(response);
        assertContentEquals("This is secret", response);
    }

    @Test
    public void testSecretsAreNotFoundForUnknownUser() {
        Response response = GET("/secret");
        assertStatus(404, response);
    }

    @Test
    public void testSuperSecretsAreAllowedForAdmin() {
        login("admin", "admin");
        Response response = GET("/top-secret");
        assertIsOk(response);
        assertContentEquals("This is top secret", response);
    }

    @Test
    public void testSecretsAreDeniedForUser() {
        login("user", "user");
        Response response = GET("/top-secret");
        assertStatus(403, response);
    }

    private void login(String user, String pass) {
        String data = "username=" + user + "&password=" + pass;
        Response response = POST("/login", 
                APPLICATION_X_WWW_FORM_URLENCODED, data);
        assertIsOk(response);
    }
}

As you can see here, every test logs in with a certain user first, and then tries to access a resource. Some are intended to fail, while some should return a successful access. Before every access, a login is needed using the login() method. In case you wonder why a simple HTTP request stores the returned login credentials for all of the next requests, this is actually done by the logic of the FunctionalTest, which stores all returned cookies from the login request during the rest of the test.

How to do it...

Add the needed routes:

POST    /login         Application.login
GET      /secret       Application.secret
GET      /top-secret   Application.topsecret

Create User and Right entities:

@Entity
public class User extends Model {

    public String username;
    public String password;
    @ManyToMany
    public Set<Right> rights;

    public booleanhasRight(String name) {
        Right r = Right.find("byName", name).first();
        return rights.contains(r);
    }
}

A simple entity representing a right and consisting of a name is shown in the following code:

@Entity
public class Right extends Model {

    @Column(unique=true)
    public String name;
}

Create a Right annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Right {
    String value();
}

Lastly, create all the controller methods:

public class Application extends Controller {

    public static void index() {
        render();
    }

    @Before(unless = "login")
    public static void checkForRight() {
        String sessionUser = session.get("user");
        User user = User.find("byUsername", sessionUser).first();
        notFoundIfNull(user);

        Right right = getActionAnnotation(Right.class);
        if (!user.hasRight(right.value())) {
            forbidden("User has no right to do this");
        }
    }

    public static void login(String username, String password) {
        User user = User.find("byUsernameAndPassword", username, password).first();

        if (user == null) {
            forbidden();
        }

        session.put("user", user.username);
    }

    @Right("Secret")
    public static void secret() {
        renderText("This is secret");
    }

    @Right("TopSecret")
    public static void topsecret() {
        renderText("This is top secret");
    }
}

How it works...

Going through this step by step reveals surprisingly few new items, but rather a simple and concise change at the core of each controller call. Neither the routes are new, nor the entity definitions, or its possibility to create the hasRight() method. The only real new logic is inside the controller. The logic here is not meant as business logic of your application but rather permission checking. On the one hand every security aware controller has a @Right annotation at its definition, which defines the required right as a text string.

On the other hand all the logic regard permissions is executed at the checkForRight() method before every controller call. It inspects the annotation value and checks whether the currently logged-in user has this specific annotation value as a right set using the hasRight() method defined in the user entity.

There's more...

This is a pretty raw method to check for rights. It imposes several design weaknesses and severe performance issues. But it is a start to go further.

Be flexible with roles instead of rights

The security model here is pretty weak. You should think of using roles on user level instead of rights, and check these roles for the rights called. This allows you to create less fine-grained permission checks such as a "Content editor" and a "publisher" role for example.

More speed with caching

The whole code presented here can be pretty slow. First you could cache the roles or rights of a certain user. Furthermore you could cache the security right of the controller action and the login credentials, which are looked up on every request.

Increased complexity with context-sensitive rights

The security checks compared here are very simple. If you want to have a right, then only the owner of an object can change it, you are not completely off with the solution presented here. You need to define more logic inside your controller call.

Check out the deadbolt module

There is a module which already does more checks and has more options than this example. You should take a look at the deadbolt module at http://www.playframework.org/modules/deadbolt. This module offers restrictions not only on the controller level, but also inside Views. Deadbolt also provides arbitrary dynamic security, which allows you to provide application-specific security strategies.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.196.172