Reflective Validation

Zu Beginn meines ersten Artikels in unserem TechBlog möchte ich mich kurz vorstellen. Mein Name ist Christian Proinger und ich bin bei Cenarion Teil des Architekturteams.

In diesem Artikel werde ich beschreiben wie man unter Ausnutzung der Java Reflection API und der Open Source Bibliothek Reflections das Java-Meta-Modell untersuchen kann.

Das konkrete Problem war herauszufinden, ob alle relevanten Methoden mit Spring-Security-Annotationen abgesichert wurden. Dies können z.B. Methoden in einem Domain-Model oder einem Service-Layer sein. Im unten angeführten ExampleService (Spring stereotype-Annotation @Service) gibt es vier Methoden: 

  1. doSomethingThatShouldBeSecured: Eine Methode die abgesichert werden sollte, was aber bisher noch nicht gemacht wurde. Auf solche Methoden wollen wir in der Unit-Test-Phase des Projekt-Builds aufmerksam gemacht werden, damit nur ein abgesicherter Code ins SCM eingecheckt wird. 
  2. doSomethingSecured: Eine korrekt abgesicherte Methode.
  3. somethingPrivate: Obwohl man Spring-Security-Annotationen auch auf privaten Methoden benutzen kann, ist dies im Normalfall nicht nötig. 
  4. iAmAwareThatThisMethodWillNotBeSecured: Manche Methoden dürfen auch von anonymen Benutzern aufgerufen werden. Wenn dies der Fall ist, dann soll der Entwickler eine Begründung angeben. Dadurch kann unterschieden werden zwischen Methoden, welche absichtlich nicht abgesichert sind, und solchen, wo dies vergessen wurde. 
@Service
public class ExampleService {

	public void doSomethingThatShouldBeSecured(String p1) {
		//TODO: secure me
		System.out.println("doSomethingThatShouldBeSecured()");
		somethingPrivate();
	}
	
	@PreAuthorize("hasRole('ADMIN')")
	public void doSomethingSecured() {
		System.out.println("doSomethingSecured()");
		somethingPrivate();
	}

	// we don't need to secure this because it will only be accessed from secured public methods. 
	private void somethingPrivate() {
		System.out.println("somethingPrivate()");
	}
	
	@IgnoreInSecurityScan(reason = "because this does not need security!")
	public void iAmAwareThatThisMethodWillNotBeSecured() {
		
	}
}

Mit folgender Testmethode wird diese Überprüfung nun durchgeführt und ich werde die einzelnen Teile im Folgenden erörtern.

/**
	 * Testet, ob alle public-Methoden im package
	 * "com.cenarion.example.reflectivevalidation.service" in @Service-Klassen
	 * entweder mit @PreAuthorize, @PostFilter oder mit
	 * @IgnoreInSecurityScan annotiert sind.
	 * 
	 */
	@Test
	public void testThatAllPublicMethodsAreSecured() {
		//setup
		Set<Class<?>> classesToCheck = retrieveMatchingClasses(
				"com.cenarion.example.reflectivevalidation.service",
				service.class);

		//exercise
		UnsecureMethodChecker unsec = new UnsecureMethodChecker(
				new MethodChecker() {

					@override
					public boolean check(Method m) {
						PreAuthorize preAuthAnno = m
								.getAnnotation(PreAuthorize.class);
						PostFilter postFilterAnno = m
								.getAnnotation(PostFilter.class);

						if (preAuthAnno == null && postFilterAnno == null) {
							return !ignore(m); //ignore Methods like getters, setters, toString, ...
						} else {
							printAnnotationDetails(m, preAuthAnno,
									postFilterAnno);
							return false;
						}
					}
		});
		//verify
		verifyNoUnsecured(unsec.evaluate(classesToCheck).getMap());
	}

In der Setup-Phase des Unit-Tests werden alle Klassen, die mit der Spring-Stereotype-Annotation @Service annotiert sind und im Package com.cenarion.example.reflectivevalidation.service enthalten sind, mit der Reflections-Bibliothek im Classpath gesucht.

import org.reflections.Reflections;

	private Set<Class<?>> retrieveMatchingClasses(String targetPackage,
			Class annotation) {
		Reflections reflections = new Reflections(new ConfigurationBuilder()
				.addUrls(ClasspathHelper.forPackage(targetPackage))
				.setScanners(new ResourcesScanner(),
						new TypeAnnotationsScanner(), new SubTypesScanner()));

		Set<Class<?>> classesToCheck = reflections
				.getTypesAnnotatedWith(annotation);
		
		return classesToCheck;
	}

In der Exercise-Phase werden die Methoden herausgefiltert,  welche weder eine PreAuthorize, PostFilter (beides Spring-Security-Annotations) oder eine IgnoreInSecurityScan (eigene Annotation) aufweisen. Je nach Log-Level werden dabei Ausgaben für gesicherte und ignorierte Methoden gemacht (get/set-Methoden und Object-Methoden wie notifiy werden beispielsweise ignoriert). 

In der Exercise-Phase werden die Methoden herausgefiltert,  welche weder eine PreAuthorize, PostFilter (beides Spring-Security-Annotations) oder eine IgnoreInSecurityScan (eigene Annotation) aufweisen. Je nach Log-Level werden dabei Ausgaben für gesicherte und ignorierte Methoden gemacht (get/set-Methoden und Object-Methoden wie notifiy werden beispielsweise ignoriert). 

Abschließend wird in der Verification-Phase, eine für die Entwickler, geeignete Ausgabe erzeugt, um die ungesicherten Methoden zu finden. Diese sieht wie folgt aus: 

MethodsAreSecuredTest - unsecured: {class com.cenarion.example.reflectivevalidation.service.ExampleService=[public void com.cenarion.example.reflectivevalidation.service.ExampleService.doSomethingThatShouldBeSecured(java.lang.String)]}
MethodsAreSecuredTest - the default @PreAuthorize or @PostFilter annotation should be added to the following methods
======================
	public void com.cenarion.example.reflectivevalidation.service.ExampleService.doSomethingThatShouldBeSecured(java.lang.String)

Der angeführte Testfall ersetzt natürlich nicht das Testen des SpringEL-Test-Ausdrucks, der in diesen Spring-Security-Annotationen angegeben ist. Vor allem nicht den Test der semantischen Korrektheit des Ausdrucks. Die syntaktische Korrektheit kann aber mithilfe des SpelExpressionParsers sehr wohl auch in Unit-Tests geprüft werden. 

Ich hoffe dieser Artikel hilft dem ein oder anderen von euch, im Bezug auf Application-Security weiterhin auf der sicheren Seite zu bleiben. Habt ihr die Java-Reflection schon einmal für die Verifizierung von Java-Meta-Informationen benutzt?

Christian Proinger (Software Architect)