This is an automated email from the ASF dual-hosted git repository. papegaaij pushed a commit to branch WICKET-7200 in repository https://gitbox.apache.org/repos/asf/wicket.git
commit c8fa4527ab912662ff0578fb7c5c69ec370990bd Author: Emond Papegaaij <[email protected]> AuthorDate: Mon Aug 31 10:53:31 2026 +0200 WICKET-7200 align class-vs-package resolution in the role annotations The five annotations in wicket-auth-roles implemented three different rules for combining a class level annotation with a package level one, and none of them were documented. Standardise on the rule that was affirmed in WICKET-3240: the rules on a class replace the rules on its package, and rules at the same level are combined with AND. - @AuthorizeAction and @AuthorizeActions now honour package level annotations, resolved per action name, so a class level rule for ENABLE leaves the package rule for RENDER in place. @AuthorizeActions gains ElementType.PACKAGE, without which a package cannot express more than one action rule. - @AuthorizeResource now replaces the annotation of its package instead of being AND-ed with it, which also removes an unguarded getPackage() dereference. - @AuthorizeInstantiations now participates in the override, and gains ElementType.PACKAGE. Document the resolution rules on AnnotationsRoleAuthorizationStrategy, on each of the annotations and in the user guide. The pitfall that prompted this is called out explicitly: because the annotations are @Inherited, an annotation on a superclass in another package counts as an annotation on the class and therefore suppresses the annotation of the subclass' own package. Add AnnotationsRolePackageTest, which pins each of these rules with real classes in real annotated packages. Mocks cannot be used for this, since a generated mock does not live in the package of the class it mocks. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --- .../AnnotationsRoleAuthorizationStrategy.java | 320 ++++++++++++++------- .../role/annotations/AuthorizeAction.java | 30 +- .../role/annotations/AuthorizeActions.java | 18 +- .../role/annotations/AuthorizeInstantiation.java | 11 +- .../role/annotations/AuthorizeInstantiations.java | 9 +- .../role/annotations/AuthorizeResource.java | 12 +- .../annotations/AnnotationsRolePackageTest.java | 161 +++++++++++ .../role/annotations/base/ActionAnnotatedBase.java | 34 +++ .../role/annotations/base/AnnotatedBase.java | 34 +++ .../role/annotations/pkg/EnableOnlyComponent.java | 34 +++ .../annotations/pkg/InheritingActionComponent.java | 33 +++ .../role/annotations/pkg/InheritingComponent.java | 34 +++ .../role/annotations/pkg/OptedOutComponent.java | 34 +++ .../role/annotations/pkg/OverridingComponent.java | 34 +++ .../role/annotations/pkg/OverridingResource.java | 34 +++ .../annotations/pkg/PackageProtectedResource.java | 32 +++ .../role/annotations/pkg/PlainComponent.java | 32 +++ .../role/annotations/pkg/RulesetComponent.java | 36 +++ .../role/annotations/pkg/package-info.java | 29 ++ .../role/annotations/pkg/sub/NestedComponent.java | 33 +++ .../src/main/asciidoc/security/security_2.adoc | 60 +++- 21 files changed, 927 insertions(+), 127 deletions(-) diff --git a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRoleAuthorizationStrategy.java b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRoleAuthorizationStrategy.java index 96163e1549..43c26d77df 100644 --- a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRoleAuthorizationStrategy.java +++ b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRoleAuthorizationStrategy.java @@ -16,6 +16,12 @@ */ package org.apache.wicket.authroles.authorization.strategies.role.annotations; +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + import org.apache.wicket.Component; import org.apache.wicket.authorization.Action; import org.apache.wicket.authroles.authorization.strategies.role.AbstractRoleAuthorizationStrategy; @@ -27,8 +33,85 @@ import org.apache.wicket.request.resource.IResource; /** - * Strategy that checks the {@link AuthorizeInstantiation} annotation. - * + * Strategy that checks the role annotations in this package: + * <ul> + * <li>{@link AuthorizeInstantiation} and {@link AuthorizeInstantiations} guard the instantiation of + * a component,</li> + * <li>{@link AuthorizeAction} and {@link AuthorizeActions} guard an {@link Action} on a component + * instance, such as {@link org.apache.wicket.Component#RENDER} and + * {@link org.apache.wicket.Component#ENABLE},</li> + * <li>{@link AuthorizeResource} guards the request of a resource.</li> + * </ul> + * <p> + * Each of these annotations can be placed on a class or on a package. A package is annotated through + * its <code>package-info.java</code> file: + * + * <pre> + * // only users with role ADMIN are allowed to create instances of pages in this package + * @AuthorizeInstantiation("ADMIN") + * package com.example.admin; + * + * import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation; + * </pre> + * + * <p> + * <b>Annotations on a class replace the annotations on its package.</b> The annotations on a package + * are a default that a class can override: when a class carries any annotation relevant to the check + * being performed, the annotations on its package are not consulted at all. Annotations at the same + * level are combined with AND, so every one of them has to grant access. For actions this is decided + * per action name, which means that a class restricting only <code>ENABLE</code> still inherits the + * <code>RENDER</code> restriction of its package. Within a single {@link AuthorizeAction}, + * {@link AuthorizeAction#deny()} is evaluated before {@link AuthorizeAction#roles()}: a user holding + * a denied role is refused even when that user also holds an accepted one. + * <p> + * Because all of these annotations are {@link java.lang.annotation.Inherited}, the annotations + * "on a class" include those declared on any of its superclasses, in any package. That is + * worth spelling out, because it is easy to be caught by: + * + * <pre> + * // com/example/base/SecuredPage.java + * @AuthorizeInstantiation("USER") + * public class SecuredPage extends WebPage + * + * // com/example/admin/package-info.java + * @AuthorizeInstantiation("ADMIN") + * package com.example.admin; + * + * // com/example/admin/ReportPage.java -- requires USER, not ADMIN! + * public class ReportPage extends SecuredPage + * </pre> + * + * <p> + * <code>ReportPage</code> inherits <code>@AuthorizeInstantiation("USER")</code> from + * its superclass, that inherited annotation counts as an annotation on the class, and therefore the + * ADMIN restriction of its own package is never applied. Annotate such a subclass explicitly to give + * it the roles of its package. + * <p> + * Further points to keep in mind when annotating packages: + * <ul> + * <li>A package annotation applies to that one package only. Java has no annotation inheritance + * between packages, so <code>com.example</code> does not pass its annotations on to + * <code>com.example.admin</code>.</li> + * <li>{@link java.lang.annotation.Inherited} has no meaning on a package, and inheritance between + * classes never crosses over to interfaces.</li> + * <li>The <code>package-info.java</code> file has to be compiled and shipped, and to be loaded by + * the same class loader as the classes of the package, or its annotations cannot be found at + * runtime.</li> + * <li>An annotation without any roles, such as <code>@AuthorizeInstantiation()</code>, + * authorizes everybody. Because it still replaces the annotations on the package, it is the way to + * exempt a single class from the restrictions of its package.</li> + * </ul> + * <p> + * Note that + * {@link org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy} + * resolves its permissions quite differently: it looks up the exact component class and has no + * package or superclass fallback at all. When both strategies are combined, as + * {@link org.apache.wicket.authroles.authorization.strategies.role.RoleAuthorizationStrategy} does, + * they are combined with AND: both have to grant access. + * + * @see org.apache.wicket.authorization.IAuthorizationStrategy + * @see org.apache.wicket.authroles.authorization.strategies.role.RoleAuthorizationStrategy + * * @author Eelco Hillenius */ public class AnnotationsRoleAuthorizationStrategy extends AbstractRoleAuthorizationStrategy @@ -51,69 +134,18 @@ public class AnnotationsRoleAuthorizationStrategy extends AbstractRoleAuthorizat public <T extends IRequestableComponent> boolean isInstantiationAuthorized( final Class<T> componentClass) { - // We are authorized unless we are found not to be - boolean authorized = true; - - // Check class annotation first because it is more specific than package annotation - final AuthorizeInstantiation classAnnotation = componentClass.getAnnotation(AuthorizeInstantiation.class); - if (classAnnotation != null) - { - authorized = check(classAnnotation); - } - else - { - // Check package annotation if there is no one on the the class - final Package componentPackage = componentClass.getPackage(); - if (componentPackage != null) - { - final AuthorizeInstantiation packageAnnotation = componentPackage.getAnnotation(AuthorizeInstantiation.class); - if (packageAnnotation != null) - { - authorized = check(packageAnnotation); - } - } - } - - // Check for multiple instantiations - final AuthorizeInstantiations authorizeInstantiationsAnnotation = componentClass - .getAnnotation(AuthorizeInstantiations.class); - if (authorizeInstantiationsAnnotation != null) + for (final AuthorizeInstantiation rule : resolve(componentClass, + AnnotationsRoleAuthorizationStrategy::instantiationRules)) { - for (final AuthorizeInstantiation authorizeInstantiationAnnotation : authorizeInstantiationsAnnotation - .ruleset()) + if (!hasAny(new Roles(rule.value()))) { - if (!check(authorizeInstantiationAnnotation)) - { - authorized = false; - } + return false; } } - return authorized; + return true; } - /** - * Check if annotated instantiation is allowed. - * - * @param authorizeInstantiationAnnotation - * The annotations information - * @return False if the instantiation is not authorized - */ - private <T extends IRequestableComponent> boolean check( - final AuthorizeInstantiation authorizeInstantiationAnnotation) - { - // We are authorized unless we are found not to be - boolean authorized = true; - - // Check class annotation first because it is more specific than package annotation - if (authorizeInstantiationAnnotation != null) - { - authorized = hasAny(new Roles(authorizeInstantiationAnnotation.value())); - } - - return authorized; - } - /** * @see org.apache.wicket.authorization.IAuthorizationStrategy#isActionAuthorized(org.apache.wicket.Component, * org.apache.wicket.authorization.Action) @@ -127,24 +159,46 @@ public class AnnotationsRoleAuthorizationStrategy extends AbstractRoleAuthorizat return isActionAuthorized(componentClass, action); } + /** + * Checks whether the given action is authorized for the given component class. Rules are + * resolved per action: the rules on the class replace the rules of its package only for the + * action they name. + * + * @param componentClass + * the class of the component the action is performed on + * @param action + * the action to check + * @return false if the action is not authorized + */ protected boolean isActionAuthorized(final Class<?> componentClass, final Action action) { - // Check for a single action - if (!check(action, componentClass.getAnnotation(AuthorizeAction.class))) + for (final AuthorizeAction rule : resolve(componentClass, + element -> actionRules(element, action))) { - return false; + final Roles deniedRoles = new Roles(rule.deny()); + if (isEmpty(deniedRoles) == false && hasAny(deniedRoles)) + { + return false; + } + + if (!hasAny(new Roles(rule.roles()))) + { + return false; + } } - // Check for multiple actions - final AuthorizeActions authorizeActionsAnnotation = componentClass.getAnnotation(AuthorizeActions.class); - if (authorizeActionsAnnotation != null) + return true; + } + + @Override + public boolean isResourceAuthorized(IResource resource, PageParameters pageParameters) + { + for (final AuthorizeResource rule : resolve(resource.getClass(), + AnnotationsRoleAuthorizationStrategy::resourceRules)) { - for (final AuthorizeAction authorizeActionAnnotation : authorizeActionsAnnotation.actions()) + if (!hasAny(new Roles(rule.value()))) { - if (!check(action, authorizeActionAnnotation)) - { - return false; - } + return false; } } @@ -152,56 +206,114 @@ public class AnnotationsRoleAuthorizationStrategy extends AbstractRoleAuthorizat } /** - * @param action - * The action to check - * @param authorizeActionAnnotation - * The annotations information - * @return False if the action is not authorized + * Resolves the rules that apply to the given class: the rules declared on the class itself if + * there are any, and the rules declared on its package otherwise. Since all annotations in this + * package are {@link java.lang.annotation.Inherited}, the rules declared on the class include + * those it inherits from a superclass in another package, and such an inherited rule therefore + * suppresses the rules of the class' own package. + * + * @param annotatedClass + * the component or resource class to resolve the rules for + * @param rules + * extracts the rules relevant to the check being performed from a class or a package + * @return the rules that apply, empty if the class is not restricted */ - private boolean check(final Action action, final AuthorizeAction authorizeActionAnnotation) + private static <R> List<R> resolve(final Class<?> annotatedClass, + final Function<AnnotatedElement, List<R>> rules) { - if (authorizeActionAnnotation != null) + List<R> resolved = rules.apply(annotatedClass); + + if (resolved.isEmpty()) { - if (action.getName().equals(authorizeActionAnnotation.action())) + // Only fall back to the package when the class itself says nothing + final Package annotatedPackage = annotatedClass.getPackage(); + if (annotatedPackage != null) { - Roles deniedRoles = new Roles(authorizeActionAnnotation.deny()); - if (isEmpty(deniedRoles) == false && hasAny(deniedRoles)) - { - return false; - } - - Roles acceptedRoles = new Roles(authorizeActionAnnotation.roles()); - if (!hasAny(acceptedRoles)) - { - return false; - } + resolved = rules.apply(annotatedPackage); } } - return true; + + return resolved; } - @Override - public boolean isResourceAuthorized(IResource resource, PageParameters pageParameters) + /** + * @param element + * a class or a package + * @return the instantiation rules declared on the given element, both the single + * {@link AuthorizeInstantiation} and the {@link AuthorizeInstantiations} ruleset + */ + private static List<AuthorizeInstantiation> instantiationRules(final AnnotatedElement element) { - Class<? extends IResource> resourceClass = resource.getClass(); - boolean allowedByResourceItself = isResourceAnnotationSatisfied( - resourceClass.getAnnotation(AuthorizeResource.class)); - boolean allowedByPackage = isResourceAnnotationSatisfied( - resourceClass.getPackage().getAnnotation(AuthorizeResource.class)); - return allowedByResourceItself && allowedByPackage; + final AuthorizeInstantiation single = element.getAnnotation(AuthorizeInstantiation.class); + final AuthorizeInstantiations ruleset = element.getAnnotation(AuthorizeInstantiations.class); + + if (single == null && ruleset == null) + { + return Collections.emptyList(); + } + + final List<AuthorizeInstantiation> rules = new ArrayList<>(); + if (single != null) + { + rules.add(single); + } + if (ruleset != null) + { + Collections.addAll(rules, ruleset.ruleset()); + } + + return rules; } - private boolean isResourceAnnotationSatisfied(AuthorizeResource annotation) + /** + * @param element + * a class or a package + * @param action + * the action being checked + * @return the rules declared on the given element that apply to the given action, both the + * single {@link AuthorizeAction} and the ones grouped by {@link AuthorizeActions} + */ + private static List<AuthorizeAction> actionRules(final AnnotatedElement element, + final Action action) { - if (annotation != null) + final AuthorizeAction single = element.getAnnotation(AuthorizeAction.class); + final AuthorizeActions grouped = element.getAnnotation(AuthorizeActions.class); + + if (grouped == null) { - // we have an annotation => we must check for the required roles - return hasAny(new Roles(annotation.value())); + // The common case: at most one annotation, so avoid building a list for it + if (single != null && action.getName().equals(single.action())) + { + return Collections.singletonList(single); + } + return Collections.emptyList(); } - else + + final List<AuthorizeAction> rules = new ArrayList<>(); + if (single != null && action.getName().equals(single.action())) { - // no annotation => no required roles => this resource can be accessed - return true; + rules.add(single); } + for (final AuthorizeAction rule : grouped.actions()) + { + if (action.getName().equals(rule.action())) + { + rules.add(rule); + } + } + + return rules; + } + + /** + * @param element + * a class or a package + * @return the {@link AuthorizeResource} declared on the given element, if any + */ + private static List<AuthorizeResource> resourceRules(final AnnotatedElement element) + { + final AuthorizeResource rule = element.getAnnotation(AuthorizeResource.class); + + return rule != null ? Collections.singletonList(rule) : Collections.emptyList(); } } diff --git a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeAction.java b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeAction.java index 87610c9ce7..f2b72a8d28 100644 --- a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeAction.java +++ b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeAction.java @@ -24,8 +24,21 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * A mapping of 1..n roles to an action. This annotions must be embedded in the - * {@link AuthorizeActions} annotation. + * A mapping of 1..n roles to an action. It can be used on its own, and it can be grouped with other + * actions in an {@link AuthorizeActions} annotation when more than one action has to be restricted. + * + * <pre> + * // a panel that only users with role ADMIN are allowed to see + * @AuthorizeAction(action = "RENDER", roles = "ADMIN") + * public class ForAdmins extends Panel + * </pre> + * + * It can be placed on a class or on a package, the latter by specifying it in the + * <code>package-info.java</code> file of that package. Restrictions are resolved per action, so an + * annotation on a class replaces the annotations of its package only for the action it names: a class + * restricting <code>ENABLE</code> still inherits the <code>RENDER</code> restriction of its package. + * See {@link AnnotationsRoleAuthorizationStrategy} for the complete resolution rules and for the + * pitfalls of annotating packages. * * @see org.apache.wicket.authorization.IAuthorizationStrategy * @see AnnotationsRoleAuthorizationStrategy @@ -42,7 +55,7 @@ public @interface AuthorizeAction { /** * The action that is allowed. The default actions that are supported by Wicket are - * <code>RENDER</code> and <code>ENABLE<code> as defined as constants + * <code>RENDER</code> and <code>ENABLE</code> as defined as constants * of {@link org.apache.wicket.Component}. * * @see org.apache.wicket.Component#RENDER @@ -55,16 +68,17 @@ public @interface AuthorizeAction { /** * The roles for this action. * - * @return the roles for this action. The default is an empty string (annotations do not allow - * null default values) + * @return the roles for this action. The default is a zero length array (annotations do not + * allow null default values), which allows the action for everybody */ String[] roles() default { }; /** - * The roles to deny for this action. + * The roles to deny for this action. Denying takes precedence over allowing: a user holding one + * of these roles is refused even when that user also holds one of the {@link #roles()}. * - * @return the roles to deny for this action. The default is an empty string (annotations do not - * allow null default values) + * @return the roles to deny for this action. The default is a zero length array (annotations do + * not allow null default values), which denies the action for nobody */ String[] deny() default { }; } diff --git a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeActions.java b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeActions.java index d463121689..8b2f042d6e 100644 --- a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeActions.java +++ b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeActions.java @@ -24,12 +24,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Groups a set (technically an array) of {@link AuthorizeAction}s for authorization. This - * annotation works on a class level, and can be used like this: + * Groups a set (technically an array) of {@link AuthorizeAction}s for authorization, for when more + * than one action has to be restricted. A single action does not need this annotation and can be + * declared with {@link AuthorizeAction} directly. This annotation can be used like this: * * <pre> - * // A panel that is only visible for users with role ADMIN - * @AuthorizeAction(action = "RENDER", roles = { "ADMIN", "USER" }) + * // a panel that users with role ADMIN and USER are allowed to see, but that only + * // users with role ADMIN are allowed to interact with + * @AuthorizeActions(actions = { + * @AuthorizeAction(action = "RENDER", roles = { "ADMIN", "USER" }), + * @AuthorizeAction(action = "ENABLE", roles = "ADMIN") }) * public class ForAdminsAndUsers extends Panel * { * public ForAdminsAndUsers(String id) @@ -39,6 +43,10 @@ import java.lang.annotation.Target; * } * </pre> * + * It can be placed on a class or on a package, the latter by specifying it in the + * <code>package-info.java</code> file of that package. Restrictions are resolved per action; see + * {@link AnnotationsRoleAuthorizationStrategy} for the complete resolution rules. + * * @see org.apache.wicket.authorization.IAuthorizationStrategy * @see AnnotationsRoleAuthorizationStrategy * @see AuthorizeAction @@ -46,7 +54,7 @@ import java.lang.annotation.Target; * @author Eelco Hillenius */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.TYPE }) +@Target({ ElementType.PACKAGE, ElementType.TYPE }) @Documented @Inherited public @interface AuthorizeActions { diff --git a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeInstantiation.java b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeInstantiation.java index f2cf20decb..0f68d8777f 100644 --- a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeInstantiation.java +++ b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeInstantiation.java @@ -44,10 +44,18 @@ import java.lang.annotation.Target; * import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation; * </pre> * + * An annotation on a class replaces the annotations on its package, rather than adding to them. Since + * this annotation is {@link Inherited}, that also holds for an annotation inherited from a superclass + * in another package: such an inherited annotation suppresses the annotation of the subclass' own + * package. See {@link AnnotationsRoleAuthorizationStrategy} for the complete resolution rules and for + * the pitfalls of annotating packages. + * * @see org.apache.wicket.authorization.IAuthorizationStrategy * @see AnnotationsRoleAuthorizationStrategy + * @see AuthorizeInstantiations * @see AuthorizeActions * @see AuthorizeAction + * @see AuthorizeResource * * @author Eelco hillenius */ @@ -60,7 +68,8 @@ public @interface AuthorizeInstantiation { /** * Gets the roles that are allowed to take the action. * - * @return the roles that are allowed. Returns a zero length array by default + * @return the roles that are allowed. Returns a zero length array by default, which authorizes + * everybody */ String[] value() default { }; } diff --git a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeInstantiations.java b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeInstantiations.java index de708a7d89..c55694ffef 100644 --- a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeInstantiations.java +++ b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeInstantiations.java @@ -43,14 +43,19 @@ import java.lang.annotation.Target; * } * </pre> * + * It can be placed on a package as well, by specifying it in the <code>package-info.java</code> file + * of that package. A ruleset on a class replaces the instantiation rules of its package, just like a + * single {@link AuthorizeInstantiation} does; see {@link AnnotationsRoleAuthorizationStrategy} for + * the complete resolution rules. + * * @see org.apache.wicket.authorization.IAuthorizationStrategy * @see AnnotationsRoleAuthorizationStrategy * @see AuthorizeInstantiation - * @see AuthorizeInstantiations + * @see AuthorizeResource * @author René Dieckmann ([email protected]) */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.TYPE }) +@Target({ ElementType.PACKAGE, ElementType.TYPE }) @Documented @Inherited public @interface AuthorizeInstantiations { diff --git a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeResource.java b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeResource.java index 9162f51a6b..dd7b631e6d 100644 --- a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeResource.java +++ b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeResource.java @@ -24,12 +24,17 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Annotation for configuring what roles are allowed for requesting the annotated resource. It works analogously - * to {@link org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation}. + * Annotation for configuring what roles are allowed for requesting the annotated resource. It can be + * placed on a class or on a package, the latter by specifying it in the + * <code>package-info.java</code> file of that package, and it is resolved just like + * {@link AuthorizeInstantiation}: an annotation on a resource class replaces the annotation of its + * package rather than adding to it. See {@link AnnotationsRoleAuthorizationStrategy} for the complete + * resolution rules and for the pitfalls of annotating packages. * * @author Carl-Eric Menzel * @see org.apache.wicket.authorization.IAuthorizationStrategy * @see AnnotationsRoleAuthorizationStrategy + * @see AuthorizeInstantiation */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PACKAGE, ElementType.TYPE}) @@ -41,7 +46,8 @@ public @interface AuthorizeResource /** * Gets the roles that are allowed to take the action. * - * @return the roles that are allowed. Returns a zero length array by default + * @return the roles that are allowed. Returns a zero length array by default, which authorizes + * everybody */ String[] value() default {}; } diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRolePackageTest.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRolePackageTest.java new file mode 100644 index 0000000000..c3911db917 --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRolePackageTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.wicket.Component; +import org.apache.wicket.authroles.authorization.strategies.role.IRoleCheckingStrategy; +import org.apache.wicket.authroles.authorization.strategies.role.Roles; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.EnableOnlyComponent; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.InheritingActionComponent; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.InheritingComponent; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.OptedOutComponent; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.OverridingComponent; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.OverridingResource; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.PackageProtectedResource; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.PlainComponent; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.RulesetComponent; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.sub.NestedComponent; +import org.junit.jupiter.api.Test; + +/** + * Tests how {@link AnnotationsRoleAuthorizationStrategy} resolves annotations on a package against + * annotations on a class. The fixtures are real classes in real packages rather than mocks, because a + * mock does not live in the package of the class it mocks. + * <p> + * Package <code>pkg</code> restricts instantiation to <code>role1</code>, RENDER to + * <code>role1</code> and resources to <code>role1</code>; package <code>base</code> and package + * <code>pkg.sub</code> carry no annotations at all. + */ +class AnnotationsRolePackageTest +{ + @Test + void packageAnnotationAppliesToClassWithoutOneOfItsOwn() + { + assertTrue(strategy("role1").isInstantiationAuthorized(PlainComponent.class)); + assertFalse(strategy("role2").isInstantiationAuthorized(PlainComponent.class)); + } + + @Test + void classAnnotationReplacesPackageAnnotation() + { + assertTrue(strategy("role2").isInstantiationAuthorized(OverridingComponent.class)); + assertFalse(strategy("role1").isInstantiationAuthorized(OverridingComponent.class)); + } + + @Test + void classAnnotationWithoutRolesExemptsFromPackageAnnotation() + { + assertTrue(strategy().isInstantiationAuthorized(OptedOutComponent.class)); + } + + @Test + void classRulesetReplacesPackageAnnotation() + { + assertTrue(strategy("role2", "role3").isInstantiationAuthorized(RulesetComponent.class)); + assertFalse(strategy("role2").isInstantiationAuthorized(RulesetComponent.class)); + assertFalse(strategy("role1").isInstantiationAuthorized(RulesetComponent.class)); + } + + /** + * The annotations are {@link java.lang.annotation.Inherited}, so an annotation on a superclass in + * another package counts as an annotation on the class and suppresses the annotation of the + * class' own package. + */ + @Test + void inheritedClassAnnotationSuppressesPackageAnnotation() + { + assertTrue(strategy("role2").isInstantiationAuthorized(InheritingComponent.class)); + assertFalse(strategy("role1").isInstantiationAuthorized(InheritingComponent.class)); + } + + /** + * Java has no annotation inheritance between packages. + */ + @Test + void packageAnnotationDoesNotApplyToSubpackage() + { + assertTrue(strategy().isInstantiationAuthorized(NestedComponent.class)); + assertTrue(strategy().isActionAuthorized(NestedComponent.class, Component.RENDER)); + } + + @Test + void packageAnnotationAppliesToActionOfClassWithoutOneOfItsOwn() + { + assertTrue(strategy("role1").isActionAuthorized(PlainComponent.class, Component.RENDER)); + assertFalse(strategy("role2").isActionAuthorized(PlainComponent.class, Component.RENDER)); + } + + /** + * Actions are resolved per action name, so restricting one action on the class leaves the rule of + * the package for another action in place. + */ + @Test + void classActionAnnotationOnlyReplacesThePackageRuleForTheSameAction() + { + assertTrue(strategy("role2").isActionAuthorized(EnableOnlyComponent.class, Component.ENABLE)); + assertFalse(strategy("role1").isActionAuthorized(EnableOnlyComponent.class, Component.ENABLE)); + + assertTrue(strategy("role1").isActionAuthorized(EnableOnlyComponent.class, Component.RENDER)); + assertFalse(strategy("role2").isActionAuthorized(EnableOnlyComponent.class, Component.RENDER)); + } + + @Test + void inheritedActionAnnotationSuppressesPackageAnnotation() + { + assertTrue( + strategy("role2").isActionAuthorized(InheritingActionComponent.class, Component.RENDER)); + assertFalse( + strategy("role1").isActionAuthorized(InheritingActionComponent.class, Component.RENDER)); + } + + @Test + void packageAnnotationAppliesToResourceWithoutOneOfItsOwn() + { + assertTrue(strategy("role1").isResourceAuthorized(new PackageProtectedResource(), null)); + assertFalse(strategy("role2").isResourceAuthorized(new PackageProtectedResource(), null)); + } + + /** + * Resources are resolved like everything else: the annotation on the class replaces the one on + * the package instead of being combined with it. + */ + @Test + void classAnnotationReplacesPackageAnnotationForResources() + { + assertTrue(strategy("role2").isResourceAuthorized(new OverridingResource(), null)); + assertFalse(strategy("role1").isResourceAuthorized(new OverridingResource(), null)); + } + + /** + * Create a strategy whose role checker is given a list of roles and returns true if that list + * contains any of the asked-for roles. + * + * @param availableRoles + * the roles the current user has + * @return the strategy to test + */ + private AnnotationsRoleAuthorizationStrategy strategy(final String... availableRoles) + { + IRoleCheckingStrategy roleChecker = requiredRoles -> requiredRoles + .hasAnyRole(new Roles(availableRoles)); + + return new AnnotationsRoleAuthorizationStrategy(roleChecker); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/base/ActionAnnotatedBase.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/base/ActionAnnotatedBase.java new file mode 100644 index 0000000000..fa8712d97e --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/base/ActionAnnotatedBase.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.base; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeAction; +import org.apache.wicket.markup.html.WebComponent; + +/** + * A superclass restricting RENDER, in a package that carries no annotations of its own. + */ +@AuthorizeAction(action = "RENDER", roles = "role2") +public class ActionAnnotatedBase extends WebComponent +{ + private static final long serialVersionUID = 1L; + + public ActionAnnotatedBase() + { + super("notUsed"); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/base/AnnotatedBase.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/base/AnnotatedBase.java new file mode 100644 index 0000000000..3ce4e82b07 --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/base/AnnotatedBase.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.base; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation; +import org.apache.wicket.markup.html.WebComponent; + +/** + * An annotated superclass in a package that carries no annotations of its own. + */ +@AuthorizeInstantiation("role2") +public class AnnotatedBase extends WebComponent +{ + private static final long serialVersionUID = 1L; + + public AnnotatedBase() + { + super("notUsed"); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/EnableOnlyComponent.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/EnableOnlyComponent.java new file mode 100644 index 0000000000..4af0d8f746 --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/EnableOnlyComponent.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeAction; +import org.apache.wicket.markup.html.WebComponent; + +/** + * Restricts ENABLE itself, and so still inherits the RENDER rule of its package. + */ +@AuthorizeAction(action = "ENABLE", roles = "role2") +public class EnableOnlyComponent extends WebComponent +{ + private static final long serialVersionUID = 1L; + + public EnableOnlyComponent() + { + super("notUsed"); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/InheritingActionComponent.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/InheritingActionComponent.java new file mode 100644 index 0000000000..5d671ca0a8 --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/InheritingActionComponent.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.base.ActionAnnotatedBase; + +/** + * Inherits its RENDER rule from a superclass in another package, which suppresses the RENDER rule + * of this component's own package. + */ +public class InheritingActionComponent extends ActionAnnotatedBase +{ + private static final long serialVersionUID = 1L; + + public InheritingActionComponent() + { + super(); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/InheritingComponent.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/InheritingComponent.java new file mode 100644 index 0000000000..a7b4c443a4 --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/InheritingComponent.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.base.AnnotatedBase; + +/** + * Inherits its instantiation rule from a superclass in another package. Because the annotation is + * inherited, it counts as an annotation on this class, and the rule of this component's own package + * is therefore never applied. + */ +public class InheritingComponent extends AnnotatedBase +{ + private static final long serialVersionUID = 1L; + + public InheritingComponent() + { + super(); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OptedOutComponent.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OptedOutComponent.java new file mode 100644 index 0000000000..4ab3039e9b --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OptedOutComponent.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation; +import org.apache.wicket.markup.html.WebComponent; + +/** + * Exempts itself from the instantiation rule of its package by requiring no roles at all. + */ +@AuthorizeInstantiation() +public class OptedOutComponent extends WebComponent +{ + private static final long serialVersionUID = 1L; + + public OptedOutComponent() + { + super("notUsed"); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OverridingComponent.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OverridingComponent.java new file mode 100644 index 0000000000..0021461edb --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OverridingComponent.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation; +import org.apache.wicket.markup.html.WebComponent; + +/** + * Replaces the instantiation rule of its package with one of its own. + */ +@AuthorizeInstantiation("role2") +public class OverridingComponent extends WebComponent +{ + private static final long serialVersionUID = 1L; + + public OverridingComponent() + { + super("notUsed"); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OverridingResource.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OverridingResource.java new file mode 100644 index 0000000000..f62941a6f2 --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/OverridingResource.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeResource; +import org.apache.wicket.request.resource.IResource; + +/** + * Replaces the AuthorizeResource of its package with one of its own. + */ +@AuthorizeResource("role2") +public class OverridingResource implements IResource +{ + private static final long serialVersionUID = 1L; + + @Override + public void respond(Attributes attributes) + { + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/PackageProtectedResource.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/PackageProtectedResource.java new file mode 100644 index 0000000000..672817d302 --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/PackageProtectedResource.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.request.resource.IResource; + +/** + * Carries no annotation of its own, so the AuthorizeResource of its package applies. + */ +public class PackageProtectedResource implements IResource +{ + private static final long serialVersionUID = 1L; + + @Override + public void respond(Attributes attributes) + { + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/PlainComponent.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/PlainComponent.java new file mode 100644 index 0000000000..ce95f37a8c --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/PlainComponent.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.markup.html.WebComponent; + +/** + * Carries no annotation of its own, so the annotations of its package apply. + */ +public class PlainComponent extends WebComponent +{ + private static final long serialVersionUID = 1L; + + public PlainComponent() + { + super("notUsed"); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/RulesetComponent.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/RulesetComponent.java new file mode 100644 index 0000000000..b8b71f825a --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/RulesetComponent.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiations; +import org.apache.wicket.markup.html.WebComponent; + +/** + * Replaces the instantiation rule of its package with a combined ruleset. + */ +@AuthorizeInstantiations(ruleset = { @AuthorizeInstantiation("role2"), + @AuthorizeInstantiation("role3") }) +public class RulesetComponent extends WebComponent +{ + private static final long serialVersionUID = 1L; + + public RulesetComponent() + { + super("notUsed"); + } +} diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/package-info.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/package-info.java new file mode 100644 index 0000000000..d79b26c256 --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/package-info.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Fixture package for {@link org.apache.wicket.authroles.authorization.strategies.role.annotations.AnnotationsRolePackageTest}: + * every kind of role annotation, declared on the package. Note the order of the declarations: the + * package annotations precede the package declaration, and the imports they use follow it. + */ +@AuthorizeInstantiation("role1") +@AuthorizeAction(action = "RENDER", roles = "role1") +@AuthorizeResource("role1") +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeAction; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation; +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeResource; diff --git a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/sub/NestedComponent.java b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/sub/NestedComponent.java new file mode 100644 index 0000000000..5ef0ce791c --- /dev/null +++ b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/pkg/sub/NestedComponent.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.authroles.authorization.strategies.role.annotations.pkg.sub; + +import org.apache.wicket.markup.html.WebComponent; + +/** + * Lives in a subpackage of an annotated package, which has no package-info.java of its own. Java + * has no annotation inheritance between packages, so nothing restricts this component. + */ +public class NestedComponent extends WebComponent +{ + private static final long serialVersionUID = 1L; + + public NestedComponent() + { + super("notUsed"); + } +} diff --git a/wicket-user-guide/src/main/asciidoc/security/security_2.adoc b/wicket-user-guide/src/main/asciidoc/security/security_2.adoc index 5bc1b116ae..9445a8c7c4 100644 --- a/wicket-user-guide/src/main/asciidoc/security/security_2.adoc +++ b/wicket-user-guide/src/main/asciidoc/security/security_2.adoc @@ -116,11 +116,11 @@ class BasicAuthenticationRolesSession extends AuthenticatedWebSession { } ---- -Roles can be adopted to apply security restrictions on our pages and components. This can be done using one of the two built-in authorization strategies that extend super class _AbstractRoleAuthorizationStrategyWicket_: _MetaDataRoleAuthorizationStrategy_ and _AnnotationsRoleAuthorizationStrategy_ +Roles can be adopted to apply security restrictions on our pages and components. This can be done using one of the two built-in authorization strategies that extend super class _AbstractRoleAuthorizationStrategy_: _MetaDataRoleAuthorizationStrategy_ and _AnnotationsRoleAuthorizationStrategy_ The difference between these two strategies is that _MetaDataRoleAuthorizationStrategy_ handles role-based authorizations with Wicket metadata while _AnnotationsRoleAuthorizationStrategy_ uses Java annotations. -NOTE: Application class _AuthenticatedWebApplication_ already sets _MetaDataRoleAuthorizationStrategy_ and _AnnotationsRoleAuthorizationStrategy_ as its own authorization strategies (it uses a compound strategy as we will see in <<security.adoc#_authorizations,paragraph 22.2>>). +NOTE: Application class _AuthenticatedWebApplication_ already sets _MetaDataRoleAuthorizationStrategy_ and _AnnotationsRoleAuthorizationStrategy_ as its own authorization strategies (it uses a compound strategy, as we will see in <<_strategy_roleauthorizationstrategy,the last paragraph of this chapter>>). The code that we will see in the next examples is for illustrative purpose only. If our application class inherits from _AuthenticatedWebApplication_ we won't need to configure anything to use these two strategies. @@ -203,7 +203,7 @@ public class BasicAuthenticationSession extends AuthenticatedWebSession { The code that instantiates _MetaDataRoleAuthorizationStrategy_ and set it as application's strategy is inside application class method _init()_. -Any subclass of _AbstractRoleAuthorizationStrategyWicket_ needs an implementation of interface _IRoleCheckingStrategy_ to be instantiated. For this purpose in the code above we used the application class itself because its base class _AuthenticatedWebApplication_ already implements interface _IRoleCheckingStrategy_. By default _AuthenticatedWebApplication_ checks for authorizations using the roles returned by the current _AbstractAuthenticatedWebSession_. As final step inside init we gra [...] +Any subclass of _AbstractRoleAuthorizationStrategy_ needs an implementation of interface _IRoleCheckingStrategy_ to be instantiated. For this purpose in the code above we used the application class itself because its base class _AuthenticatedWebApplication_ already implements interface _IRoleCheckingStrategy_. By default _AuthenticatedWebApplication_ checks for authorizations using the roles returned by the current _AbstractAuthenticatedWebSession_. As final step inside init we grant the [...] The code from session class has three interesting methods. The first is _authenticate()_ which considers as valid credentials every pair of username and password having the same value. The second notable method is _getRoles()_ which returns role SIGNED_IN if user is authenticated and it adds role ADMIN if username is equal to superuser. Finally, we have method _signOut()_ which has been overridden in order to clean the username field used internally to generate roles. @@ -227,7 +227,16 @@ Just like custom “Page expired” page (see <<versioningCaching.adoc#_stateful ==== Using roles with annotations -Strategy _AnnotationsRoleAuthorizationStrategy_ relies on two built-in annotations to handle role-based authorizations. These annotations are _AuthorizeInstantiation_ and _AuthorizeAction_. As their names suggest the first annotation specifies which roles are allowed to instantiate the annotated component while the second must be used to indicate which roles are allowed to perform a specific action on the annotated component. +Strategy _AnnotationsRoleAuthorizationStrategy_ relies on a set of built-in annotations to handle role-based authorizations: + +|=== +|Annotation | Restricts +|*_AuthorizeInstantiation_* |which roles are allowed to instantiate the annotated component +|*_AuthorizeInstantiations_* |a group of _AuthorizeInstantiation_ rules that must all be satisfied +|*_AuthorizeAction_* |which roles are allowed to perform a given action on the annotated component +|*_AuthorizeActions_* |a group of _AuthorizeAction_ rules, one for each action +|*_AuthorizeResource_* |which roles are allowed to request the annotated resource +|=== In the following example we use annotations to make a page accessible only to signed-in users and to enable it only if user has the ADMIN role: @@ -252,6 +261,49 @@ public class AdminOnlyPage extends WebPage { } ---- +An annotation that lists no roles at all, like _@AuthorizeInstantiation()_, authorizes everybody. The next paragraph explains why that is useful. + +===== Annotations on packages + +Each of these annotations can be applied to an entire package instead of to a single class. A package is annotated in its _package-info.java_ file. Note the unusual order of the declarations: the annotations come before the package declaration, and the imports they need come after it. + +[source,java] +---- +// only users with role ADMIN are allowed to create instances of pages in this package +@AuthorizeInstantiation("ADMIN") +package org.mycompany.admin; + +import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation; +---- + +The annotations on a package are a default that a class can override: *the annotations on a class replace the annotations on its package*, they are not added to them. Annotations at the same level are combined with AND, so every one of them has to grant access. For actions this is decided per action, which means that a class restricting only ENABLE still inherits the RENDER restriction of its package. + +This is what makes an annotation without roles useful: because _@AuthorizeInstantiation()_ replaces the rule of its package and requires no roles at all, it is the way to exempt a single class from the restrictions of its package. + +All of these annotations are _@Inherited_, and that has a consequence which is easy to overlook. The annotations *on a class* include the ones it inherits from a superclass, in whatever package that superclass happens to live. Such an inherited annotation counts as an annotation on the class, so it suppresses the annotation of the class' own package: + +[source,java] +---- +//org/mycompany/base/SecuredPage.java +@AuthorizeInstantiation("USER") +public class SecuredPage extends WebPage { +} + +//org/mycompany/admin/package-info.java +@AuthorizeInstantiation("ADMIN") +package org.mycompany.admin; + +//org/mycompany/admin/ReportPage.java +public class ReportPage extends SecuredPage { +} +---- + +WARNING: In the example above _ReportPage_ requires role USER and *not* role ADMIN. It inherits the annotation of _SecuredPage_, so the ADMIN restriction of its own package is never applied. This means that moving a page into a restricted package has no effect at all when one of its superclasses is annotated. Annotate such a subclass explicitly to give it the roles of its package. + +NOTE: A package annotation applies to that one package and to nothing else. Java has no annotation inheritance between packages, so annotating _org.mycompany_ does not restrict the classes in _org.mycompany.admin_. + +NOTE: The _package-info.java_ file has to be compiled and shipped with the application, and it has to be loaded by the same class loader as the classes of its package, or its annotations cannot be found at runtime. + === Catching an unauthorized component instantiation Interface _IUnauthorizedComponentInstantiationListener_ (in package _org.apache.wicket.authorization_) is provided to give the chance to handle the case in which a user tries to instantiate a component without having the permissions to do it. The method defined inside this interface is _onUnauthorizedInstantiation(Component)_ and it is executed whenever a user attempts to execute an unauthorized instantiation.
