Eigene PermissionEvaluator mit Spring Security

Spring Security bietet eine Fülle von Möglichkeiten, die Rechtekontrolle für REST Endpoints zu realisieren. Besonders interessant ist die Verwendung der Annotationen @PreAuthorize und @PostAuthorize.

Beide Annotationen verwenden Spring Expression Language (SpEL) Ausdrücke um die Authentisierung für einen Methodenaufruf zu beschreiben. Im folgenden Beispiel etwa, wird vor dem Aufruf der create() Methode geprüft, ob der aufrufende Benutzer die Rolle ROLE_USER besitzt.

@PreAuthorize("hasRole('ROLE_USER')")
public void create(Person person);

Ist die Prüfung erfolgreich wird die Methode ausgeführt, im anderen Fall liefert der Aufruf des REST Endpoints den HTTP Status 403.

@PreAuthorize("hasPermission(#person, 'WRITE')")
Person update(@@RequestBody Person person);

Dieses Beispiel benutzt den hasPermission Ausdruck, der über Spring Security ACL prüft, ob der aktuelle Benutzer Schreibrechte für Personen Entitäten besitzt.

In diesem Beitrag wollen wir für unseren Ancestors Controller aus REST in Peace einige Restriktionen auf die Endpoints legen. Erst einmal wollen wir verhindern, dass ein Benutzer Personendaten lesen kann, wenn er nicht mit ihnen verwandt ist.

@PreAuthenticate("hasPermission(#id, 'relative')")
@GetMapping(path = "/anchestors/{id}")
public Person getPerson(@PathVariable("id") Long id) {
  ...
}

Der Ausdruck #id verweist auf den Aufruf-Parameter der Methode und relative ist der Name der Berechtigung, die wir prüfen möchten. Damit wir solche eine Restriktion prüfen können, müssen wir einen eigenen PermissionEvaluator schreiben und ihn in unserer Spring Boot Anwendung bekannt machen.

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
  @Autowired
  private FamilyPermissionEvaluator permissionEvaluator;

  @Override
  protected MethodSecurityExpressionHandler createExpressionHandler() {
    DefaultMethodSecurityExpressionHandler expressionHandler =
      new DefaultMethodSecurityExpressionHandler();
    expressionHandler.setPermissionEvaluator(permissionEvaluator);
    return expressionHandler;
  }
}

Wir ergänzen eine weitere Konfigurationsklasse, in der wir unseren FamilyPermissionEvaluator bei dem MethodSecurityExpressionHandler anmelden. Besonders zu beachten ist hier, dass wir unseren
PermissionEvaluator als @Autowired Member in die Konfiguration einhängen, dadurch kann dieser auch per Dependency Injection befüllt werden. Fast alle Beispiele im Netz erzeugen den neuen PermissionEvaluator mit einem Konstruktoraufruf in der createExpressionHandler, dann ist aber DI nicht mehr möglich.

Damit die Anmeldung funktioniert, benötigen wir natürlich noch die Implementierung unseres
PermissionEvaluator, dafür müssen wir die Methode PermissionEvaluator#hasPermission implementieren.

@Component
public class FamilyPermissionEvaluator implements PermissionEvaluator {
  @Autowired
  private FamilyService familyService;
  @Autowired
  private PersonService personService;

  @Override
  public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
    if (!"relative".equals(permission) || !(targetDomainObject instanceof Long)) {
      return false;
    }
    Optional<Person> me = personService.findByUsername(auth.getName)();
    if(!me.isPresent()) {
      return false;
    }
    return familyService.areRelatives(me.get().getId(), (Long)targetDomainObject);
  }

  @Override
  public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
    return false;
  }
}

Zu Beginn unserer hasPermission Methode prüfen wir, ob die Eingabe-Parameter der Methode korrekt gefüllt sind und suchen dann den aktuellen Benutzer in unserer Personendatenbank. Ist der angemeldete Benutzer dort nicht vorhanden, dann ist er auch mit keiner Person in der Datenbank verwandt und es gibt keine Berechtigung für den Endpoint. Danach prüfen wir, ob die gesuchte Person mit dem aktuellen Benutzer verwandt ist und gewähren die Berechtigung, wenn dem so ist.

Damit ist der REST Endpoint auch schon gegen unbefugte Verwendung gesichert. Ich kann weiterhin die Daten meiner Kinder, Geschwister und Ahnen abrufen, bekomme aber bei Schwager und Schwägerin den Status Code FORBIDDEN zurückgeliefert, da die Verwandschaftsprüfung keine Ehen berücksichtigt.

In Zeiten des Datenschutzes finden wir auch ein Beispiel für die @PostAuthorize Annotation. Im Gegensatz zur
@PreAuthorize, die vor dem Aufruf der Methode behandelt wird, findet die Behandlung der @PostAuthorize Annotation nach dem Aufruf statt.

Das klingt erst einmal ein wenig unsinnig, bietet aber die Möglichkeit, zu prüfen, ob der Benutzer das Ergebnis der Methode sehen darf. Da nicht jeder Verwandte die Daten einer Person einsehen dürfen sollte, ergänzen wir unseren PermissionEvaluator um Sperrfristen für Verstorbene und das Recht der Eltern die Informationen ihrer minderjährigen Kinder einzusehen.

@PreAuthenticate("hasPermission(#id, 'relative')")
@PostAuthenticate("hasPermission(returnObject, 'privacy')")
@GetMapping(path = "/anchestors/{id}")
public Person getPerson(@PathVariable("id") Long id) {
  ...
}

Der hasPermission Aufruf in der @PostAuthorize Annotation beinhaltet den Ausdruck returnObject. Damit ist der aktuelle Rückgabewert der Methode gemeint. In diesem Fall eine Instanz der Klasse Person. Damit sind wir nun in der Lage zu prüfen, wie alt die Person ist, oder vor wie vielen Jahren sie schon verstorben ist. Um die Auswertungen der Berechtigungen zu trennen, übergeben wir hier privacy als permission Parameter.

@Component
public class FamilyPermissionEvaluator implements PermissionEvaluator {
  @Autowired
  private FamilyService familyService;
  @Autowired
  private PersonService personService;

  @Override
  public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
    if ("relative".equals(permission) && (targetDomainObject instanceof Long)) {
      Optional<Person> me = personService.findByUsername(auth.getName)();
      if(!me.isPresent()) {
        return false;
      }
      return familyService.areRelatives(me.get().getId(), (Long)targetDomainObject);
    }
    if ("privacy".equals(permission) && (targetDomainObject instanceof Person)) {
      return !personService.isProtectedPrivacy((Person)targetDomainObject);
    }
    return false;
  }

  @Override
  public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
    return false;
  }
}

Die Absicherung der REST Controller durch eigene PermissionEvaluator sorgt für eine saubere Trennung zwischen den Verantwortlichkeiten. So kann sich die Controller Implementierung gänzlich um die Bereitstellung der Personendaten konzentrieren, während sich die PermissionEvaluator um Zugangsberechtigungen und Datenschutz als Querschnittsfunktionen kümmern.