Po ostatnim prostym zabezpieczeniu aplikacji Grailsowej z użyciem wtyczki JSecurity pozostał pewien niedosyt. Nasz przykład – i owszem – skutecznie zabezpieczył dostęp osób niezalogowanych, ale nie pokazał prawdziwej potęgi JSecurity – możliwości autoryzacji, czyli sprawdzania praw dostępu do określonych zasobów. Wyglądało to trochę tak, jakbyśmy wytaczali armatę po to by zabić muchę, dlatego teraz spróbujemy tą armatą zapolować na grubszego zwierza ;) wciąż jednak pozostając przy założeniu, że ma być prosto i estetycznie na ile to możliwe.
Przyjmijmy zatem, że nasza aplikacja ma:

  • dopuszczać tylko zalogowanych użytkowników (tak, jak poprzednio),
  • pozwalać na oglądanie zasobów wszystkim zalogowanym użytkownikom (posiadającym rolę „users”),
  • pozwalać na edycję/usuwanie/tworzenie nowych zasobów tylko użytkownikom posiadającym rolę „admin” (pozostali użytkownicy dostają informację o braku dostępu)

Jak widzisz, wykorzystamy szkielet poprzedniej aplikacji. Zauważ też, że wprowadzamy pojęcie roli, czym jest rola?
Zgodnie z infomacją na stronie wtyczki, to

a job or a set of responsibilities that a person can have

, czyli zbiór odpowiedzialności, które posiada dany użytkownik w systemie. System, oczywiście, musi takiemu użytkownikowi udostępnić możliwość wykonania czynności wchodzących w skład takich odpowiedzialności. Upraszczając – posiadanie danej roli pozwala na dostęp do zasobów systemu, do których dostęp bez niej jest zabroniony.
Założeniem naszej aplikacji jest, aby każdy zalogowany użytkownik mógł oglądać dane, jednak modyfikacja jakichkolwiek informacji wiąże się z obowiązkiem posiadania roli admina.

Zaczynajmy.

1. Dodajemy nowe klasy dziedzinowe

Musimy zdefiniować nowe klasy dziedzinowe, które pozwolą nam utrwalać informacje o rolach i powiązaniach ich z użytkownikami.

class Role {
    String name
}
class UserRoleRef {
    User user
    Role role
}

Dzięki takim klasom dziedzinowym (i klasie User z poprzedniego wpisu) możliwa będzie realizacja referencji wiele-do-wielu – jeden użytkownik może mieć wiele ról w systemie, a jedna rola może być w posiadaniu wielu użytkowników.

Możemy teraz zapisać wstępnie uprawnienia dostępu do naszego systemu, np. tak:

// użytkownicy
def jasiu = new User(username: "jasiu", password: new Sha1Hash("sekret").toHex()).save()
def admin = new User(username: "admin", password: new Sha1Hash("admin").toHex()).save()
 
// role
def usersRole = new Role("users").save()
def adminRole = new Role("admin").save()
 
// powiązania user-role
new UserRoleRef(user: jasiu, role: usersRole).save()
new UserRoleRef(user: admin, role: adminRole).save()

2. Uaktualniamy filtr

Role i powiązania z użytkownikami mamy już zdefiniowane i zapisane w bazie danych, jednak wciąż nie wiemy po co nam one. Samo posiadanie praw „admina” nic nie daje, jeśli nie wiemy, co one dają. Uaktualniamy nasz plik SecurityFilters.groovy tak:

class SecurityFilters {
    def filters = {
        auth(controller: "*", action: "*") {
            before = {
                accessControl { true }
            }
        }
 
        manageRecord(controller: "*", action: "(create|edit|save|update|delete)") {
            before = {
                accessControl {
                    role("admins")
                }
            }
        }
 
        showRecord(controller: "*", action: "show") {
            before = {
                accessControl {
                    role("admins") || role("users")
                }
            }
        }
    }
}

Jak widać, mamy zdefiniowane dwie reguły dostępu do dowolnego kontrolera (gwiazdka), ale z wyszczególnionymi akcjami. Akcja show wymaga posiadania roli „users”, albo „admins” (czyli dowolnej, na chwilę obecną), a akcje pozwalające na modyfikacje informacji – roli „admins”.

3. Rozszerzamy funkcjonalność MyRealm

Czas na fragment kodu, który będzie odpowiadał za sprawdzanie, czy dany użytkownik posiada daną rolę. Wzbogacamy naszą klasę MyRealm w dwie dodatkowe (poza authenticate()) metody:

    def hasRole(principal, roleName) {
        def criteria = UserRoleRef.createCriteria()
        def roles = criteria.list {
            role {
                eq('name', roleName)
            }
            user {
                eq('username', principal)
            }
        }
 
        return roles.size() > 0
    }
 
    def hasAllRoles(principal, roles) {
        def criteria = UserRoleRef.createCriteria()
        def r = criteria.list {
            role {
                'in'('name', roles)
            }
            user {
                eq('username', principal)
            }
        }
 
        return r.size() == roles.size()
    }

Obie są dość proste – pierwsza sprawdza istnienie referencji użytkownik-rola dla zadanych parametrów, druga metoda robi to samo, ale ze zbiorem ról zadanych jako parametr.

4. Dodajemy widok informujący o braku dostępu

Założenia mamy takie, żeby użytkownik bez dostępu do konkretnego zasobu został o tym poinformowany komunikatem. W tym celu wprowadzamy drobne usprawnienie do naszego kontrolera AuthController. Wprowadzamy domknięcie unauthorized, które będzie wykonane za każdym razem, gdy użytkownik spróbuje wykonać akcję, do której nie ma dostępu.

def unauthorized = {
}

… oraz plik widoku dla takiej akcji (views/auth/unauthorized.gsp):

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Brak dostępu</title>
  </head>
  <body>
    <h1>Dostęp zabroniony</h1>
  </body>
</html>

5. Uaktualnianie menu

Nasze zabezpieczanie właściwie się zakończyło, jednak warto jeszcze zadbać o dodatkową rzecz. Jeśli pracujemy na widokach wygenerowanych przez Grails (np. za pomocą komendy grails generate-all <klasa_dziedzinowa>), to każdy z widoków wyposażony jest w przyciski/odnośniki kierujące do stron pozwalających na modyfikację/dodawanie/usuwanie rekordów, np. takich:

<div class="nav">
    <span class="menuButton"><a class="home" href="${createLinkTo(dir:'')}">Home</a></span>
    <span class="menuButton">New Book</span>
</div>

Teoretycznie nic się nie stanie, jeśli tak to zostawimy, bo nawet kliknięcie na link New Book nie spowoduje wygenerowania formularza tworzenia nowej książki, tylko skieruje nas do strony z informacją o braku dostępu. Jednak… czego oczy nie widzą, tego sercu nie żal :) więc warto ukryć przed użytkownikiem te pozycje menu, które nie są mu potrzebne. Wykorzystamy więc znacznik jsec:hasRole:

<div class="nav">
    <span class="menuButton"><a class="home" href="${createLinkTo(dir:'')}">Home</a></span>
    <jsec:hasRole name="admins">
        <span class="menuButton"><g:link class="create" action="create">New Book</g:link></span>
    </jsec:hasRole>
</div>

Od tej pory odnośnik New Book pokaże się tylko użytkownikom posiadającym rolę „admins”. Takie modyfikacje możemy wprowadzić we wszystkich widokach naszej aplikacji.

I to właściwie wszystko jeśli chodzi o kontrolę ról za pomocą JSecurity w Grails.

Dość ciekawą rzeczą, która warta jest poznania, ale nie opisałem jej tutaj (miało być prosto, więc nie chciałem dodatkowo komplikować) jest kontrola uprawnień za pomocą JSecurity. Wtyczka dla Grails pozwala na kontrolę uprawnień nie tylko na poziomie kontroler/akcja, ale też np. dostępu do plików, albo innych zasobów systemowych.