Thymeleaf Template Engine

@cmrudolph du wirst wohl recht damit haben, dass der Spring-Security-Dialekt, der richtige Ansatz ist. Dennoch bleibt zumindest für mich die Frage offen, woher dieses für mich unerwartete Verhalten kommt, dass Role-Objekte nach Long gewandelt werden, bevor sie weitergereicht werden.

Über eine Erklärung würde ich mich sehr freuen. Bei dieser Rollengeschichte mag dies mit dem Spring-Security-Dialekt ja noch funktionieren. An einer anderen Stelle hingegen fliegt einem die Sache dann wieder um die Ohren. Und das kann ja wohl nicht Sinn und Zweck sein.

[QUOTE=cmrudolph]@Landei : sry, dass ich nicht vorher geschrieben habe. Ich hatte diesen Thread aus Zeitgründen nicht verfolgt. Ich glaube aber, dass du mit der Lösung in #18 in die falsche Richtung läufst.
Der “richtige” Ansatz ist mit dem Spring-Security-Dialekt. Siehe dazu hier: Thymeleaf + Spring Security integration basics - Thymeleaf: java XML/XHTML/HTML5 template engine
Das sieht dann im Template in etwa so aus:

<div sec:authorize="hasRole('ROLE_ADMIN')">
  This content is only shown to administrators.
</div>
<div sec:authorize="hasRole('ROLE_USER')">
  This content is only shown to users.
</div>

*** Edit ***

Siehe auch hier: https://github.com/thymeleaf/thymeleaf-extras-springsecurity#features[/QUOTE]

Das ist ganz klar der korrekte Ansatz, wenn ich etwas in der Seite autorisieren will (das habe ich noch nicht so, hole ich nach).

Aber in diesem speziellen Fall will ich ja nur meiner User-Entity Rollen zuweisen - das hätten genauso gut auch Telefonnummern sein können, das hat mit Security nicht viel zu tun. Und da verhält sich Thymeleaf sehr seltsam.

Ich habe mir gerade die Templates mal angesehen und etwas korrigiert. th:when gibt es nicht. Im Debugger siehst du auch, dass das nicht ausgewertet wird. Ich habe das durch die entsprechenden Aufrufe im th:checked ersetzt.
Außerdem habe ich die Templates von den URLs entkoppelt, indem ich die Links an die Controllermethoden gebunden habe. Z. B. so:

<form role="form" th:action="${#mvc.url('AC#handleUserCreateForm').build()}" th:object="${userCreateForm}" method="post">
    <!-- ... -->
</form>

*** Edit ***

Jetzt hab ich vergessen das zu schreiben, weshalb ich überhaupt posten wollte… Ich kann das Problem derzeit nicht nachvollziehen. Gibt es im Code schon irgendwo eine Stelle, an der das relevant ist?

Ja, siehe #17

So, ich habe das Problem nun diagnostiziert. Man darf nicht vergessen, dass es sich bei den Ausdrücken nicht um Java, sondern um SpEL handelt. Dort wird user.roles.contains(r) wie folgt ausgewertet:
Es wird die Collection geholt und dann darauf die Methode contains ausgeführt (soweit intuitiv). Dann wird bei dessen Evaluation allerdings der Parameter konvertiert. Und zwar von Role in Object. Dazu sucht Spring einen ConversionService und findet auch einen passenden: den DomainClassConverter$ToIdConverter. Dieser macht aus dem Domänenobjekt dessen ID. Das selbe Problem gibt es auch bei der Verwendung vom #sets-Utility-Objekt.
Der ConversionService wird von SpringData registriert (DomainClassConverter#setApplicationContext).

*** Edit ***

Da passiert bei mir gar nichts. Ich bin im thymeleaf-Branch. (Edit: ok, das ist im master-Branch, deshalb hatte ich das nicht gesehen)

*** Edit ***
@Landei : die von mir gepushten Änderungen sind ok? (Edit: die Änderungen sind im thymeleaf-Branch. Soll der Branch sterben? Du hast die Freemarker-Settings ja schon aus master entfernt.)

Probiere es bitte mal im Master. Habe deinen Änderungen schon gemerged.

gibt es dazu auch seitenlange Erklärungen, Anwendungsszenarien, langjährige notwendige Problemstellungen?
oder ist das als beliebige versteckte Zerstörung von Programmen zu interpretieren :wink:

@Landei : Ich habe das Problem bereits nachvollzogen (siehe auch Edit in #25). Das Matching des Konverters ist zu grobgranular. Das wurde in DATACMNS-683 bereits gemeldet und ist in der nächsten SpringData-Version behoben. Aus dem relevanten Commit ist folgende Änderung entscheidend:


		if (sourceType.isAssignableTo(targetType)) {
			return false;
		}```

*** Edit ***

[quote=SlaterB]oder ist das als beliebige versteckte Zerstörung von Programmen zu interpretieren[/quote]
Das ist ein Bug ;-)

*** Edit ***

Wenn man folgende Dependency in die pom.xml einfügt, dann funktioniert der Code aus master:
[xml]<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-commons</artifactId>
    <version>1.10.1.BUILD-SNAPSHOT</version>
</dependency>[/xml]

Super!

Dann würde ich jetzt daran arbeiten, dass auch das Abspeichern funktioniert. Danach eine ähnliche Lösungen zum Anlegen von Roles und für die Zuweisung von Permissions zu Roles. Das würde theoretisch erst einmal im Adminbereich ausreichen, und dann wäre das Forum dran.

Trotzdem müsste man sich noch eine Lösung überlegen, um die Dialoge einfacher erstellen zu können. Ich weiß nicht, ob das ohne weiteres über Fragmente gelöst werden kann. Auf jeden Fall würde ich die “Untertabs” (Nutzer, Rollen, Settings, Logs, …) in Admin in eigene Fragmente auslagern, sonst wird es zu unübersichtlich.

Klingt das einigermaßen vernünftig?

Ja, das klingt vernünftig.

Dass der Adminbereich derzeit noch nicht geschützt ist, ist dir klar, oder?

Ist er bei dir nicht? Also bei mir wird er nicht nur aus der Navbar ausgeblendet, auch über die URL komme ich nicht rein. Außer man ist halt als admin angemeldet.

Nein, zumindest nicht vollständig. Ich kann den Adminbereich sehen, wenn ich irgendwann einmal als Admin angemeldet war. Zwischendurch war ich auch als Nichtadmin eingeloggt und nach einem Logout kann ich noch immer auf den Adminbereich zugreifen.
Wenn ich direkt nach dem Start der Anwendung versuche die URL aufzurufen, klappt es nicht.

Ich habe die Ursache für das Verhalten gerade zufällig herausgefunden. Der Pfad, der bei den geschützten URLs angegeben ist, ist nur ein einziger. Das heißt, dass nur /admin geschützt war. Ich habe daraus /admin/** gemacht, jetzt ist der Adminbereich tatsächlich geschützt.

Danke, wieder was gelernt.

Ich war die letzten zwei Wochen krank. Hatte eigentlich gehofft, dass ich mich wenigstens ein bisschen aufraffen kann, aber es ist beim guten Vorsatz geblieben. Inzwischen bin ich wieder einigermaßen auf dem Damm.

So, endlich wieder ein Fortschritt. Jetzt können im Admin-Bereich die Rechte für einen Nutzer mit dem modalen Dialog gesetzt werden.

Alles noch nicht sonderlich hypsch, und es gibt auch keine Sicherheitsabfragen oder sonstwas, aber immerhin.

Aber wenn die GUI nicht allzusehr zugemüllt werden soll, kommen wir um Dialoge nicht herum (auch wenn sie in Bootstrap ein PITA sind). Muss mal sehen, ob man das über Fragmente besser hinbekommt.

@cmrudolf Kannst du bitte mal einen Blick auf das frisch committete admin_userTab.html, Zeile 85 werfen? Ich wollte die URL so wie für handleUserCreate erzeugen, mit dem auskommentierten ${#mvc.url('AC#handleUserDeleteForm').build()}. Leider hängt das an die URL noch ein ?userId an, was dazu führt, dass in der Methode dann der RequestParam userId null ist, obwohl selbiger im Form ordentlich übergeben wird.

Ich wollte mir das Problem gerade ansehen, habe dann aber festgestellt, dass der derzeitige Snapshot von Spring-Boot nicht lauffähig ist.

Stacktrace
[spoiler]

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:426)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.IllegalStateException: Failed to introspect annotations on class org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfigureRegistrar$EnableJpaRepositoriesConfiguration
	at org.springframework.core.annotation.AnnotatedElementUtils.searchWithGetSemantics(AnnotatedElementUtils.java:465)
	at org.springframework.core.annotation.AnnotatedElementUtils.getAnnotationAttributes(AnnotatedElementUtils.java:280)
	at org.springframework.core.type.StandardAnnotationMetadata.getAnnotationAttributes(StandardAnnotationMetadata.java:112)
	at org.springframework.core.type.StandardAnnotationMetadata.getAnnotationAttributes(StandardAnnotationMetadata.java:107)
	at org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource.<init>(AnnotationRepositoryConfigurationSource.java:83)
	at org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport$1.<init>(AbstractRepositoryConfigurationSourceSupport.java:67)
	at org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport.getConfigurationSource(AbstractRepositoryConfigurationSourceSupport.java:66)
	at org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport.registerBeanDefinitions(AbstractRepositoryConfigurationSourceSupport.java:58)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:360)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:151)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:124)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:333)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:243)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:269)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:98)
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:651)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:503)
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:678)
	at org.springframework.boot.SpringApplication.doRun(SpringApplication.java:339)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:274)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:932)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:921)
	at org.fjorum.FjorumApplication.main(FjorumApplication.java:13)
	... 6 more
Caused by: java.lang.annotation.AnnotationFormatError: Invalid default: public abstract java.lang.Class org.springframework.data.jpa.repository.config.EnableJpaRepositories.repositoryBaseClass()
	at java.lang.reflect.Method.getDefaultValue(Method.java:611)
	at sun.reflect.annotation.AnnotationType.<init>(AnnotationType.java:128)
	at sun.reflect.annotation.AnnotationType.getInstance(AnnotationType.java:85)
	at sun.reflect.annotation.AnnotationParser.parseAnnotation2(AnnotationParser.java:266)
	at sun.reflect.annotation.AnnotationParser.parseAnnotations2(AnnotationParser.java:120)
	at sun.reflect.annotation.AnnotationParser.parseAnnotations(AnnotationParser.java:72)
	at java.lang.Class.createAnnotationData(Class.java:3521)
	at java.lang.Class.annotationData(Class.java:3510)
	at java.lang.Class.getDeclaredAnnotations(Class.java:3477)
	at org.springframework.core.annotation.AnnotatedElementUtils.searchWithGetSemantics(AnnotatedElementUtils.java:496)
	at org.springframework.core.annotation.AnnotatedElementUtils.searchWithGetSemantics(AnnotatedElementUtils.java:461)
	... 29 more

[/spoiler]
Gibt es einen Grund, weshalb du den Snapshot von Spring-Boot statt die stabile Version 1.2.4 gewählt hast?

Das Problem mit zusätzlichen URL-Parametern hatte ich vor kurzem selbst auch. Ich habe eine ziemlich starke Vermutung, woran das liegt und wollte eigentlich debuggen, um den Programmfluss nachzuvollziehen. Ob sich das beheben lässt, weiß ich allerdings nicht. Wenn nicht, dann mache ich ein Issue im Spring Bugtracker auf.

Bin jetzt mal zu 1.2.4. gewechselt, ist gepusht. Ich kann mich nicht mehr an den Grund erinnern, warum ich damals den Snapshot genommen habe - irgendwas war da. Konnte aber bei mir keinen Unterschied feststellen, beide Versionen laufen bei mir problemlos, und in beiden tritt das Problem auf.

Mit der Version läuft es bei mir auch. Wahrscheinlich hatte ich schon einen aktuelleren Snapshot als du, deshalb lief es bei mir nicht. Reproduzierbarere Ergebnisse gibt es aber mit den stabilen Versionen :wink:

Ich werde mal sehen, ob ich morgen zum Debuggen komme. Ich melde mich dann nochmal.

*** Edit ***

Ich habe jetzt doch etwas durchdebugged. Ein Fehler ist das Verhalten nicht, denn der UriComponentsBuilder findet den RequestParameter und fügt ihn dann zur URL hinzu.
Allerdings finde ich, dass man die Webadressen prinzipiell etwas REST-konformer aufbauen könnte. Und zwar so:

public String handleUserDeleteForm(
        @PathVariable("uid") User user,
        RedirectAttributes redirectAttributes) {
    try {
        if (user == null)
            throw new NoSuchElementException();
        userService.delete(user);
        FlashMessage.SUCCESS.put(redirectAttributes, "user.delete.success");
    } catch (RuntimeException e) {
        logger.error("User not found", e);
        FlashMessage.ERROR.put(redirectAttributes, "user.delete.failure");
    }
    return "redirect:/admin";
}```
Dabei holt SpringData übrigens den User transparent mittels Primärschlüssel aus der Datenbank und weist ihn dem Parameter user zu.

Da HTML bekannterweise keine Formulare mit der Methode DELETE unterstützt, gibt es den HiddenHttpMethodFilter, den man einfach in die Servlet-Filterliste einklinkt.
In Thymeleaf nutzt man im Formular einfach:

```html
<form th:method="delete" ...>

Dadurch wird das versteckte Feld generiert, in dem die Übertragungsmethode übermittelt wird.

Das selbe gilt für das Erstellen eines Nutzers, das sollte per POST auf /admin/users geschehen.

Prinzipiell fände ich es gut, wenn die Seiten etwas REST-konformer gegliedert wären. Dazu müsste man sich einmal genauer Gedanken machen, welche Ressourcen es überhaupt gibt.

Berechtigungen wären dann auch unter /admin/users/{uid}/rights zu finden. Mit der Methode PUT könnte man die Berechtigungen als Set setzen. Einzelberechtigungen könnte man ggf. über DELETE auf /admin/users/{uid}/rights/{right} löschen, das wäre aber eigentlich redundant.

*** Edit ***

Ach so, in Thymeleaf sollten konstante URLs immer mit dem @ angegeben werden, damit auch ein alternatives Servlet-Präfix funktioniert. Also th:action="@{/admin/userDelete}"

Mit dem REST-konformen URL-Aufbau sähe das Formular ungefähr so aus:

<form role="form" th:action="@{/admin/users/{uid}(uid=${user.id})}" th:method="delete">
    <button type="button" class="btn btn-default" data-dismiss="modal">Close
    </button>
    <button type="submit" class="btn btn-primary">Delete User</button>
</form>

Den oben genannten HiddenHttpMethodFilter registriert SpringBoot scheinbar schon bei der Autokonfiguration.

Mit dem MvcUriComponentsBuilder gibt es offensichtlich noch einige Querälen in Bezug auf SpringDatas transparentes Laden der Entities. Denn wenn man den URL bauen möchte, schlägt das wegen des Type-Mismatch (int vs. User) fehl.
Ob man das beheben kann oder wo man da am besten ansetzt, muss ich noch einmal in einer ruhigen Minuten herausfinden.

Die Idee, ein wenig REST-konformer zu werden, gefällt mir. Wahrscheinlich sollte man dann die Änderung der Nutzer-Rechte mit in die ganz normale Nutzer-Änderung (die es noch nicht gibt) packen. Die Rechte würde ich dann REST-technisch unterhalb von User gar nicht weiter ausführen, sondern nur parallel in “/admin/roles” oder so.

Ein Problem sehe ich darin, dass manchmal die gleiche Aktion von unterschiedlichen Stellen der Applikation aus ausgeführt werden soll (etwa wenn ein Nutzer in seinem Profil die eMail-Adresse ändert, oder das ein Admin im Adminbereich tut). Das passt nicht so ganz mit dem Konzept von Resourcen zusammen, oder? Wie fasst man diesen “Kontext”?

Wenn wir uns in diese Richtung bewegen wollen, sollten wir auch mal schauen, was das Artefakt spring-boot-starter-data-rest so bietet.