I'd like to implement declarative security with Spring/AOP and annotations.
As you see in the next code sample I have the Restricted Annotations with the paramter "allowedRoles" for defining who is allowed to execute an adviced method.
#Restricted(allowedRoles="jira-administrators")
public void setPassword(...) throws UserMgmtException {
// set password code
...
}
Now, the problem is that in my Advice I have no access to the defined Annotations:
public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature();
System.out.println("Allowed:" + rolesAllowedForJoinPoint(pjp));
...
}
private Restricted rolesAllowedForJoinPoint(ProceedingJoinPoint thisJoinPoint)
{
MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
return targetMethod.getAnnotation(Restricted.class);
}
The method above always returns null (there are no annotations found at all).
Is there a simple solution to this?
I read something about using the AspectJ agent but I would prefer not to use this agent.
To whoever is still having problem after changing annotation retention to Runtime, you might be having the same problem I had: getMethod() returns interface method instead of the implementing class. So, if you have your annotations in the class then naturally getAnnotations() on the interface method returns null.
The following solution solved this problem:
final String methodName = pjp.getSignature().getName();
final MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
Method method = methodSignature.getMethod();
if (method.getDeclaringClass().isInterface()) {
method = pjp.getTarget().getClass().getDeclaredMethod(methodName, method.getParameterTypes());
}
and if you like, you have the option of handling interface annotations here too.
Some more comments available here:
getting template method instance from ProceedingJoinPoint
Oleg
I assume #Restricted is your annotation. If that is the case, make sure you have:
#Retention(RetentionPolicy.RUNTIME)
in your annotation definition. This means that the annotation is retained at runtime.
Even after changing the retention policy like Bozho mentioned this call to get annotation returns null:
targetMethod.getAnnotation(Restricted.class);
What I found is you have to bind the annotation. Given the interface is declared like this:
#Retention(RetentionPolicy.RUNTIME)
public #interface Restricted {
String[] allowedRoles();
}
The advice would need to be declared like this:
#Before("#annotation( restrictedAnnotation )")
public Object processRequest(final ProceedingJoinPoint pjp, Restricted restrictedAnnotation) throws Throwable {
String[] roles = restrictedAnnotation.allowedRoles();
System.out.println("Allowed:" + roles);
}
What this does is bind the annotation to the parameter in the method signature, restrictedAnnotation. The part I am not sure about is how it gets the annotation type, it seems to be based on the parameter. And once you have the annotation you can get the values.
Why don't you just use Spring Security ? It's a brief to implement and use, I don't really see the point in wasting time reinventing the wheel.
Whith Spring AOP if you have a situation like MyManagerImpl implements MyManager the pointcut is applied to the interface method so MethodSignature describes the method defined on MyManager that doesn't have any annotation. the only way I've found to fix this is to inspect the class of the jp.getTarget() object and retrieve the corresponding method.
Related
I have created a java 17 repeatable annotation and want to create an aspect around the method containing the annotation is invoked. This seems to work when method is annotated once but fails to invoke when I have repeatable annotation. I am using aspectjrt version 1.9.7. Am I doing something wrong or aspect doesn't support repeatable annotations? Any workaround for the same?
Annotation class ->
#Repeatable(Schedules.class)
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
The repeatable class ->
#Retention(value = RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface Schedules {
Schedule[] value();
}
The aspect class ->
#Aspect
#Component
#Slf4j
public class Aspect {
#Around("#annotation(Schedule)")
public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Schedule filters = method.getAnnotation(Schedule.class);
//Business Logic
return joinPoint.proceed();
}
}
This actually is not an AOP problem, but you need to understand how repeatable annotations work in Java: If the annotation appears multiple times on the annotated element, it is not represented as a single annotation anymore but as an array of annotations in the value() of the annotation type mentioned in #Repeatable, i.e. in your case #Schedules (plural "s"!).
For your aspect, it means that you need two pointcuts, one for the single-annotation case and one for the multi-annotation one. I am suggesting to factor out the common advice code into a helper method of the aspect which always takes an array of the repeatable annotations, then just pass the value of the wrapper annotation on in one case and a one-element array in the other case.
Feel free to ask follow-up questions, if anything is unclear. But it should be straightforward, almost trivial.
Resources:
https://docs.oracle.com/javase/tutorial/java/annotations/repeating.html
https://dzone.com/articles/repeatable-annotations-in-java-8-1
P.S.: You should learn about how to bind annotations to advice method parameters:
#Around("#annotation(schedule)")
public Object traceSingle(ProceedingJoinPoint joinPoint, Schedule schedule) throws Throwable
// ...
#Around("#annotation(schedules)")
public Object traceMultiple(ProceedingJoinPoint joinPoint, Schedules schedules) throws Throwable
I am trying to implement custom annotation and aspect which will insert path variable into request body before validation.
For now it looks like this...
#Aspect
#Component
public class AddParameterToFormAspect {
#Before("#annotation(addParameterToForm)")
public void addParameterToForm(JoinPoint joinPoint, AddParameterToForm addParameterToForm) {
String form = addParameterToForm.form();
String pathVariable = addParameterToForm.pathVariable();
CodeSignature methodSignature = (CodeSignature) joinPoint.getSignature();
List<String> methodParamNames = Arrays.asList(methodSignature.getParameterNames());
int formIndex = 0;
int pathVariableIndex = 0;
for(String s : methodSignature.getParameterNames()) {
if(s.equals(form)) {
formIndex = methodParamNames.indexOf(s);
}
if(s.equals(pathVariable)) {
pathVariableIndex = methodParamNames.indexOf(s);
}
}
Object[] methodArgs = joinPoint.getArgs();
Object formObject = methodArgs[formIndex];
Field pathVariableObject;
try {
pathVariableObject = formObject.getClass().getDeclaredField(pathVariable);
pathVariableObject.setAccessible(true);
pathVariableObject.set(formObject, methodArgs[pathVariableIndex]);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
Controller example of working annotation...
#PostMapping("/test/{username}")
#AddParameterToForm(pathVariable = "username", form = "user")
public String test(#PathVariable String username, #RequestBody User user) {
return user.getUsername();
}
Controller example of validation not working...
#PostMapping("/{domainCode}")
#AddParameterToForm(pathVariable = "domainCode", form = "userAddForm")
public ResponseEntity<UserDto> saveUserForDomain(#PathVariable(name="domainCode") String domainCode, #RequestBody #Valid final UserAddForm userAddForm, BindingResult results) {...}
Adding path variable to form works but it seems #Valid no longer works, problem is probably in join point expression... How can I make it to do advice before validation and then validate?
Changing method parameters in a #Before advice is not meant to work. You should use an #Around advice in order to change parameters before calling thisJoinPoint.proceed(). This is because when calling thisJoinPoint.getArgs() you get copies of primitive type parameters, you cannot manipulate the originals in a before-advice. You are lucky that you want to manipulate object types in this case, so that is the reason it works. Using an around-advice would enable you to pass completely new arguments to a method or just manipulate the original objects, you are free to choose.
Furthermore, you should - whenever possible - use args() in order to bind your method arguments of interest to advice parameters in order to be able to interact with them in a non-cryptic and type-safe manner. Creating a local variable and assigning some value to it will not influence the method parameter of the same type at all. Why should it?
Feel free to ask follow-up questions if this explanation is not comprehensive enough for you. Then I could add some sample code for you, too.
Update after question edit:
After having inspected you code a bit more closely and in addition to my remarks earlier today in my comments under your question, disregarding the content of your aspect code, your actual problem is that the validation check cause by #Valid annotations is performed before the method is executed. I.e. what is validated is not the state after the aspect has done its job (populate member fields in your target objects) but the state before the aspect runs. It is actually the same problem discussed in this question, see also M. Deinum's and my suggestions how to solve it:
Maybe you want to try full AspectJ via LTW (load-time weaving) and see if a call() pointcut instead of the implicit execution() pointcut used by Spring AOP solves the problem. You would weave into the calling code (method calls) instead of the callee (method execution) itself. Chances are, that this happens before validation is performed.
A more Spring-like way to solve it is to use a Spring interceptor (M. Deinum mentions HandlerInterceptor) instead of an aspect. There is also a link to an example by someone else.
Having said that, I still recommend to refactor your code so as not to use reflection and matching strings on method parameter names or class member names. I think you could also get rid of your custom annotation by matching your pointcut on methods parameters with #RequestBody and #PathVariable annotations.
I need to get the annotation (a value inside of it) of the declaring class of a method during intercept():
#RuntimeType
public static Object intercept(#SuperCall Callable callable, #Origin Method method) throws Exception {
method.getDeclaringClass().getDeclaredAnnotation(SomeAnnotationOnClass.class);
The last line returns null.
new AgentBuilder.Default().with(AgentBuilder.Listener.StreamWriting.toSystemOut()).type(ElementMatchers.isAnnotatedWith(SomeAnnotationOnClass.class))
.transform((builder, type, clazzLoader, javaModule) -> {
return builder.method(ElementMatchers.any()).intercept(MethodDelegation.to(MyInterceptor.class));
This works though... So the annotation is present (on class level!). But not when intercept() is called
You can define your own binder that is executed during instrumentation where the class file-level information is still available. Assuming you define an annotation #interface Foo with runtime retention, you can implement some:
class FooBinder extends ParameterBinder.ForFixedValue<Foo>
where you can extract the constant value from the annotation. This value is then made available to any interceptor method annotated with #Foo.
I have several APIs which retain a parameter "feature" from the url (path param). To avoid retrieving it in each method endpoint (eg.)
#GET
public void findAll(#PathParam("feature") String feature);
am trying to implement AOP using AspectJ.
Following is the implementation of the Aspect
#Aspect
public class FeatureAOP {
#Pointcut("execution(* x.y.z.rest.ModifiersFacadeWrapper.*(..)) && args(feature)")
public void pointCut(String feature) {
}
#Before("x.y.z.rest.aop.FeatureAOP.pointCut(feature)")
public void parseParams(JoinPoint jp, String feature) {
Object[] x = jp.getArgs();
System.out.println("Feature: " + feature);
}
}
The above method gives me the value of "feature" in the Aspect class but if I change the method findAll to following signature, it doesn't works.
#GET
public void findAll();
What I understand is the control is transferred to the Aspect after the parameters are resolved and removing it from the method definition is failing it.
Doing so, thus takes me to the same point where I have to define all method endpoints with the parameter in its signature. I would like to know if there is a way I can get the PathParams in the Aspect class without having to define my methods with the designated parameters.
I think you could probably do it by putting the resolved params in a globally accessible data structure (e.g. a Singleton having some sort of Map or Set), but
I wouldn't recommend that kind of approach. I don't know why you don't like having all the params in your method signatures, but that is the intended way of declaring rest services, e.g.
#GET
#Path("{feature}")
#Produces("text/plain")
public String getFeature(#PathParam("feature") String feature) {
return feature;
}
This way you don't have to write any code for retrieving the params, the rest library you are using (be it Jersey or a different one) will just do everything for you.
I'm using spring's PreAuthorize annotation as follows:
#PreAuthorize("hasRole('role')");
However, I already have 'role' defined as a static String on another class. If I try to use this value:
#PreAuthorize("hasRole(OtherClass.ROLE)");
I get an error:
org.springframework.expression.spel.SpelEvaluationException: EL1008E:(pos 14): Field or property 'OtherClass' cannot be found on object of type 'org.springframework.security.access.expression.method.MethodSecurityExpressionRoot'
Is there a way to access static variables like this with a PreAuthorize annotation?
Try the following which uses Spring Expression Language to evaluate the type:
#PreAuthorize("hasRole(T(fully.qualified.OtherClass).ROLE)");
Be sure to specify the fully qualified class name.
Documentation
You can also create a bean container with roles, like:
#Component("R")
public final class RoleContainer {
public static final String ROLE_A = "ROLE_A";
}
then on controller you can use:
#PreAuthorize("hasRole(#R.ROLE_A)")
To make it possible to write expressions without package names:
<sec:global-method-security>
<sec:expression-handler ref="methodSecurityExpressionHandler"/>
</sec:global-method-security>
<bean id="methodSecurityExpressionHandler" class="my.example.DefaultMethodSecurityExpressionHandler"/>
Then extend the DefaultMethodSecurityExpressionHandler:
public class DefaultMethodSecurityExpressionHandler extends org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler {
#Override
public StandardEvaluationContext createEvaluationContextInternal(final Authentication auth, final MethodInvocation mi) {
StandardEvaluationContext standardEvaluationContext = super.createEvaluationContextInternal(auth, mi);
((StandardTypeLocator) standardEvaluationContext.getTypeLocator()).registerImport("my.example");
return standardEvaluationContext;
}
}
Now create my.example.Roles.java :
public class Roles {
public static final String ROLE_UNAUTHENTICATED = "ROLE_UNAUTHENTICATED";
public static final String ROLE_AUTHENTICATED = "ROLE_AUTHENTICATED";
}
And refer to it without package name in annotations:
#PreAuthorize("hasRole(T(Roles).ROLE_AUTHENTICATED)")
instead of:
#PreAuthorize("hasRole(T(my.example.Roles).ROLE_AUTHENTICATED)")
Makes it more readable imho. Also roles are now typed. Write:
#PreAuthorize("hasRole(T(Roles).ROLE_AUTHENTICATEDDDD)")
and you will get startup errors that wouldn't have been there if you wrote:
#PreAuthorize("hasRole('ROLE_AUTHENTICATEDDDD')")
Try something like this:
#PreAuthorize("hasRole(T(com.company.enumpackage.OtherClass).ROLE.name())");
If your OtherClass enum is declared as public static, then you need to use $ sign:
#PreAuthorize("hasRole(T(com.company.ParentTopLevelClass$OtherClass).ROLE.name())");
name() to prevent futer problems if toString() will be overriden later
The accepted answer from Kevin Bowersox works, but I didn't like having the T(fully.qualified.path) stuff so I kept looking. I started by creating a custom security method using the answer from James Watkins here:
How to create custom methods for use in spring security expression language annotations
However, instead of a String, I used my enums.Permissions class as the parameter type:
#Component
public class MySecurityService {
public boolean hasPermission(enums.Permissions permission) {
...do some work here...
return true;
}
}
Now the neat part is that when I call the hasPermission from an annotation, I don't have to have to type the whole path, but I do have to enclose it in single quotes:
#PreAuthorize("#mySecurityService.hasPermission('SOME_ROLE_NAME')")
Because the hasPermission method expects an Enum, it will automatically find the Enum value with that name. If it doesn't find it you'll get an exception:
org.springframework.expression.spel.SpelEvaluationException: Type conversion problem, cannot convert from java.lang.String to enums.Permissions
You can rename hasPermission to hasRole, in which case the only trade off is that you are trading T(fully.qualified.path) for #mySecurityService and extra single quotes.
Not sure if it is any better, but there it is. Since none of this is going to verify the values at compile time anyways, my next step is to make an annotation processor.
I also have to give credit to krosenvold for pointing out that spring can automatically convert to an enum:
https://stackoverflow.com/a/516899/618881