Hi Eric, sorry that this post comes that late.
Our Sitiation was that we have a User and a Resource. In our implementation we have our own roles and don't have them integrated to the interfaces that are provided by java ee and picketlink. But shouldn't make a big difference. So we have a page with many buttons that allow the user to modify the Resource. To modify a resource the user needs a Permission for. So we have a List of ResourcePermissions and every Object in it has the Id of the Resource and the Permission like Modify / Delete ....
Now depending on the Resource the user is viewing we want to have the particular buttons activated / deactivated (or hidden if you want).
In our case we use the StateChange event to know when the resource id is set. This event is only fired if the page is directly created by the navigation of errai, if this is not the case you'll need to fire it by your own.
In the next class you can see our use case we had.
@Page(path = EditResource)
@Templated("#editResourceContainer")
@SecuredPageComponent
public class EditResourcePage extends Composite implements HasResource {
@PageState
long ResourceId;
@Inject
@DataField
@RequirResourcePermission(ResourcePermissionValue.EDIT)
private Button updateButton;
...
}
The @SecuredPageComponent indicates that this page can have ui elements that are annotated with @RequireResourcePermission.
The PageState indicates the field the StateChange event is bound to. The HasResource interface is only an interface with a getter for the id because the StateChange event doesn't provide the value.
The updateButton is the ui element we want to secure and should only be available if the user has a EDIT permission.
Our @RequireResourcePermission annotation interface looks like this
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.FIELD
})
public @interface RequireResourcePermission {
public ResourcePermissionValue value();
}
This annotation has nothing special it only allows us to mark a ui element with a permission.
Here the @SecuredPageComponent
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SecuredPageComponent {}
Now the more important part how it's done.
We have a LifeCycleListener for the StateChange event that checks the permissions and disables (hides) the elements.
public class SecuredPageComponentLifecycleListener<W extends IsWidget> implements LifecycleListener<W> {
private final List<Tuple<Element, RequireResourcePermission>> elementsWithPermissions = new ArrayList<Tuple<Element, RequireResourcePermission>>();
private final HasResource resource;
private final AuthorizationUtil authorizationUtil;
public SecuredPageComponentLifecycleListener(HasResource resource) {
this.resource = resource;
authorizationUtil = IOC.getBeanManager().lookupBean(AuthorizationUtil.class).getInstance();
}
public void addElementPermission(Element element, RequireResourcePermission resourcePermission) {
elementsWithPermissions.add(Tuple.of(element, resourcePermission));
}
@Override
public void observeEvent(LifecycleEvent<W> event) {
for (Tuple<Element, RequireResourcePermission> elementPermissionEntry : elementsWithPermissions) {
final ResourcePermissionValue resourcePermissionValue = elementPermissionEntry.getValue().value();
final Element element = elementPermissionEntry.getKey();
if (!authorizationUtil.validateResourceAccess(resourcePermissionValue, resource.getResource()))) {
element.addClassName(RequireSystemPermissionOnElementHandler.DISABLED_CLASS_NAME);
}
}
}
@Override
public boolean isObserveableEventType(Class<? extends LifecycleEvent<W>> eventType) {
return eventType.equals(StateChange.class);
}
}
The SecuredPageComponentLifecycleListener is initialized with the Page to get the ResourceId by the HasResource interface when the StateChange is observed.
After the Listener is initialized all annotated buttons will be added by the addElementPermission method. How this is done is described later.
Then on StateChange we iterate over all elements that were added and check their permission. We use a AuthorizationUtil to check the users permission but this part is different for the most i think. The important point for the AuthorizationUtil is that is provides a method to check the users permission against. Our AuthorizationUtil accepts the permission value and the resource id to find out if the user is allowed to have the element. And if the user is not permitted we add a css class that disables the ui element.
The next class will be the most important. Our CodeDecorator that initializes the Listener and adds the elements for all classes with a SecuredPageComponent annotation.
@CodeDecorator
public class SecuredPageComponentCodeDecorator extends IOCDecoratorExtension<SecuredPageComponent> {
public SecuredPageComponentCodeDecorator(Class<SecuredPageComponent> decoratesWith) {
super(decoratesWith);
}
@Override
public List<? extends Statement> generateDecorator(InjectableInstance<SecuredPageComponent> ctx) {
final List<Statement> stmts = new ArrayList<>();
MetaClass enclosingType = ctx.getEnclosingType();
if (!enclosingType.isAssignableTo(HasResource.class)) {
throw new RuntimeException("Found an error at " + enclosingType.getCanonicalName()
+ ". SecuredPageComponent is only allowed on classes that implements the interface: "
+ HasResource.class.getCanonicalName());
}
List<MetaField> securedFields = enclosingType.getFieldsAnnotatedWith(RequireResourcePermission.class);
if (securedFields.size() > 0) {
registerSecuredPageComponentLifecycleListener(ctx, securedFields);
}
return stmts;
}
private void registerSecuredPageComponentLifecycleListener(InjectableInstance<SecuredPageComponent> ctx,
List<MetaField> metaFields) {
final String securedPageComponentListenerVar = ctx.getInjector().getInstanceVarName()
+ "_securedPageComponentListener";
Statement[] initStatements = new Statement[metaFields.size() + 1];
Statement valueAccessor;
Statement element;
// Register all secured fields at the listener
for (int i = 0; i < metaFields.size(); i++) {
RequireResourcePermission permission = (RequireResourcePermission) metaFields.get(i).getAnnotation(
RequireResourcePermission.class);
if (metaFields.get(i).getType().isAssignableTo(Element.class)) {
// field is already an element
element = InjectUtil.getPublicOrPrivateFieldValue(ctx.getInjectionContext(), Refs.get(ctx.getInjector().getInstanceVarName()), metaFields.get(i));
} else {
valueAccessor = InjectUtil.getPublicOrPrivateFieldValue(ctx.getInjectionContext(),
Refs.get(ctx.getInjector().getInstanceVarName()), metaFields.get(i));
// get element that should be secured
element = Stmt.nestedCall(valueAccessor).invoke("getElement");
}
// Register statement
initStatements[i] = Stmt.loadVariable(Refs.get(securedPageComponentListenerVar)).invoke("addElementPermission",
element, permission);
}
initStatements[metaFields.size()] = Stmt.invokeStatic(
IOC.class,
"registerLifecycleListener",
Refs.get(ctx.getInjector().getInstanceVarName()),
Refs.get(securedPageComponentListenerVar));
ctx.getTargetInjector().addStatementToEndOfInjector(
Stmt.declareFinalVariable(
securedPageComponentListenerVar,
MetaClassFactory.parameterizedAs(SecuredPageComponentLifecycleListener.class,
MetaClassFactory.typeParametersOf(ctx.getInjector().getInjectedType())),
Stmt.newObject(SecuredPageComponentLifecycleListener.class,
Refs.get(ctx.getInjector().getInstanceVarName()))));
ctx.getTargetInjector().addStatementToEndOfInjector(
Stmt.loadVariable("context")
.invoke("addInitializationCallback",
Refs.get(ctx.getInjector().getInstanceVarName()),
createInitializationCallback(
ctx,
initStatements)));
ctx.getTargetInjector().addStatementToEndOfInjector(
Stmt.loadVariable("context")
.invoke("addDestructionCallback",
Refs.get(ctx.getInjector().getInstanceVarName()),
createDestructionCallback(
ctx,
Stmt.invokeStatic(
IOC.class,
"unregisterLifecycleListener",
Refs.get(ctx.getInjector().getInstanceVarName()),
Refs.get(securedPageComponentListenerVar)))));
}
private Statement createInitializationCallback(final InjectableInstance<SecuredPageComponent> ctx,
final Statement... statements) {
return createCallback(InitializationCallback.class, "init", ctx, statements);
}
private Statement createDestructionCallback(final InjectableInstance<SecuredPageComponent> ctx,
final Statement... statements) {
return createCallback(DestructionCallback.class, "destroy", ctx, statements);
}
/**
* Creates a callback inside a statement that executes all given statements.
* @param callbackType
* @param methodName name of the method that should be overwritten for the interface
* @param ctx
* @param statements that are executed in the callback
* @return callback creation statement
*/
private Statement createCallback(final Class<?> callbackType, final String methodName,
final InjectableInstance<SecuredPageComponent> ctx, final Statement... statements) {
BlockBuilder<AnonymousClassStructureBuilder> callbackMethod = Stmt.newObject(
MetaClassFactory.parameterizedAs(callbackType,
MetaClassFactory.typeParametersOf(ctx.getInjector().getInjectedType())))
.extend()
.publicOverridesMethod(
methodName,
Parameter.finalOf(ctx.getInjector().getInjectedType(), "obj"));
for (int i = 0; i < statements.length; i++) {
callbackMethod = callbackMethod.append(statements[i]);
}
return callbackMethod.finish().finish();
}
}
/**
* Creates a callback inside a statement that executes all given statements.
* @param callbackType
* @param methodName name of the method that should be overwritten for the interface
* @param ctx
* @param statements that are executed in the callback
* @return callback creation statement
*/
private Statement createCallback(final Class<?> callbackType, final String methodName,
final InjectableInstance<SecuredPageComponent> ctx, final Statement... statements) {
BlockBuilder<AnonymousClassStructureBuilder> callbackMethod = Stmt.newObject(
MetaClassFactory.parameterizedAs(callbackType,
MetaClassFactory.typeParametersOf(ctx.getInjector().getInjectedType())))
.extend()
.publicOverridesMethod(
methodName,
Parameter.finalOf(ctx.getInjector().getInjectedType(), "obj"));
for (int i = 0; i < statements.length; i++) {
callbackMethod = callbackMethod.append(statements[i]);
}
return callbackMethod.finish().finish();
}
}
This class needs to be outside of your gwt compile path.
This CodeDecorator is created and called for all SecuredPageComponent annotated classes. It checks that the class implements the HasResource and if not it throws an exception.
It will create the SecuredPageComponentLifecycleListener and gives the annotated class as a parameter on construct.
After the creation of the SecuredPageComponentLifecycleListener it will add all elements with their permissions to the listener.
And it register and unregisters the listener on init and descruct. This CodeDecorator is in many things really similar to the errai/errai-security/errai-security-client/src/main/java/org/jboss/errai/security/rebind/PageSecurityCodeDecorator.java …
But able to check against resource bound permission.
If you have any more question post them and i will try to answer them as fast as possible
PS: To secure whole pages this way we have another CodeDecorator that works in the same way the one in the errai-security does but with the HasResource interface and StateChange event and another listener. If you'll interested in the code i can also write it down here.
Regards,
Dennis