“A good programmer is someone who always looks both ways before crossing a one-way street.”
Doug Linder
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 Konstruktor-Aufruf in der createExpressionHandler
, dann ist aber Dependency Injection 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ück geliefert, 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
Implementierung gänzlich um die Bereitstellung der Personendaten konzentrieren, während sich die Controller
PermissionEvaluator
um Zugangsberechtigungen und Datenschutz als Querschnittsfunktionen kümmern.