diff --git a/src/main/java/io/quarkus/security/PermissionsAllowed.java b/src/main/java/io/quarkus/security/PermissionsAllowed.java new file mode 100644 index 0000000..578f2bc --- /dev/null +++ b/src/main/java/io/quarkus/security/PermissionsAllowed.java @@ -0,0 +1,253 @@ +package io.quarkus.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.security.Permission; + +/** + * Indicates that a resource can only be accessed by a user with one of permissions specified through {@link #value()}. + * There are some situations where you want to require more than one permission, this can be achieved by repeating + * annotation. Please see an example below: + * + *
+ * @PermissionsAllowed("create") + * @PermissionsAllowed("update") + * public Resource createOrUpdate(Long id) { + * // business logic + * } + *+ * + * To put it another way, permissions specified by one annotation instance are disjunctive and the permission check is + * only true if all annotation instances are evaluated as true. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Repeatable(PermissionsAllowed.List.class) +public @interface PermissionsAllowed { + + /** + * Constant value for {@link #params()} indicating that the constructor parameters of the {@link #permission()} + * should be autodetected. That is, each constructor argument data type must exactly match a data type of at least + * one argument of the secured method. + * + * For example consider following permission: + * + *
+ * public class UserPermission extends Permission { + * + * private final User user; + * + * public UserPermission(String name, User user) { + * super(name); + * this.user = user; + * } + * + * ... + * } + *+ * + * Constructor parameter {@code user} is in fact object passed to a secured method. + * In the example below, {@code user1} parameter of the 'getResource' method is passed to the constructor. + * + *
+ * @PermissionsAllowed(permission = UserPermission.class, value = "resource") + * public Resource getResource(User user1) { + * // business logic + * } + *+ * + * Constructor parameters are always selected as the first secured method parameter with exactly matching data type. + * There is no limit to a reasonable number of parameters passed to the permission constructor this way. + * Please see {@link #params()} for more complex matching. + */ + String AUTODETECTED = "<
+ * @PermissionsAllowed("resource:retrieve") + * public Resource getResource() { + * // business logic + * } + *+ * is equal to the {@code perm}: + *
+ * var perm = new StringPermission("resource", "retrieve"); + *+ */ + String PERMISSION_TO_ACTION_SEPARATOR = ":"; + + /** + * Specifies a list of permissions that grants the access to the resource. It is also possible to define permission's + * actions that are permitted for the resource. Yet again, consider method 'getResource': + * + *
+ * @PermissionsAllowed({"resource:crud", "resource:retrieve", "system-resource:retrieve"}) + * public Resource getResource() { + * // business logic + * } + *+ * + * Two {@link StringPermission}s will be created: + * + *
+ * var pem1 = new StringPermission("resource", "crud", "retrieve"); + * var pem2 = new StringPermission("system-resource", "retrieve"); + *+ * + * And the permission check will pass if either {@code pem1} or {@code pem2} implies user permissions. + * Technically, it is also possible to both define actions and no action for same-named permission like this: + * + *
+ * @PermissionsAllowed({"resource:crud", "resource:retrieve", "natural-resource"}) + * public Resource getResource() { + * // business logic + * } + *+ * + * Quarkus will create two permissions: + * + *
+ * var pem1 = new StringPermission("resource", "crud", "retrieve"); + * var pem2 = new StringPermission("natural-resource"); + *+ * + * To see how the example above is evaluated, please see "implies" method of your {@link #permission()}. + * + * @see StringPermission#implies(Permission) for more details on how above-mentioned example is evaluated + * + * @return permissions linked to respective actions + */ + String[] value(); + + /** + * Choose a relation between permissions specified via {@link #value()}. By default, at least one of permissions + * is required (please see the example above). You can require all of them by setting `inclusive` to `true`. + * Let's re-use same example and make permissions inclusive: + * + *
+ * @PermissionsAllowed(value = {"resource:crud", "resource:retrieve", "natural-resource"}, inclusive = true) + * public Resource getResource() { + * // business logic + * } + *+ * + * Two {@link StringPermission}s will be created: + * + *
+ * var pem1 = new StringPermission("resource", "crud", "retrieve"); + * var pem2 = new StringPermission("system-resource", "retrieve"); + *+ * + * And the permission check will pass if both {@code pem1} and {@code pem2} implies user permissions. + * + * @return `true` if permissions should be inclusive + */ + boolean inclusive() default false; + + /** + * Mark parameters of the annotated method that should be passed to the constructor of the {@link #permission()}. + * First, let's define ourselves three classes: + * + *
+ * class ResourceIdentity { } + * class User extends ResourceIdentity { } + * class Admin extends ResourceIdentity { } + *+ * + * Now that we have defined parameter data types, please consider the secured method 'getResource': + * + *
+ * @PermissionsAllowed(permission = UserPermission.class, value = "resource", params = {user1, admin1}) + * public Resource getResource(User user, User user1, Admin admin, Admin admin1) { + * // business logic + * } + *+ * + * In the example above, we marked parameters {@code user1} and {@code admin1} as {@link #permission()} constructor + * arguments: + * + *
+ * public class UserPermission extends Permission { + * + * private final ResourceIdentity user; + * private final ResourceIdentity admin; + * + * public UserPermission(String name, ResourceIdentity user1, ResourceIdentity admin1) { + * super(name); + * this.user = user1; + * this.admin = admin1; + * } + * + * ... + * } + *+ * + * Please mention that: + *
+ * WARNING: "params" attribute is only supported in the scenarios explicitly named in the Quarkus documentation. + *
+ * + * @see #AUTODETECTED + * + * @return constructor parameters passed to the {@link #permission()} + */ + String[] params() default AUTODETECTED; + + /** + * The class that extends the {@link Permission} class, used to create permissions specified via {@link #value()}. + * + * For example: + * + *+ * public class UserPermission extends Permission { + * + * private final String[] permissions; + * + * public UserPermission(String name, String... actions) { + * super(name); + * this.actions = actions; + * } + * + * ... + * } + *+ * + * {@code actions} parameter is optional and may be omitted. + * + * @return permission class + */ + Class extends Permission> permission() default StringPermission.class; + + /** + * The repeatable holder for {@link PermissionsAllowed}. The annotation is not repeatable on class-level as + * repeatable interceptor bindings declared on classes are not supported by Quarkus. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface List { + /** + * The {@link PermissionsAllowed} instances. + * + * @return the instances + */ + PermissionsAllowed[] value(); + } +} diff --git a/src/main/java/io/quarkus/security/StringPermission.java b/src/main/java/io/quarkus/security/StringPermission.java new file mode 100644 index 0000000..f2d6fdf --- /dev/null +++ b/src/main/java/io/quarkus/security/StringPermission.java @@ -0,0 +1,122 @@ +package io.quarkus.security; + +import java.security.Permission; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents permission based on simple string comparison. + * + * @see Permission + */ +public final class StringPermission extends Permission { + + public static final String ACTIONS_SEPARATOR = ","; + private final Set
+ * More precisely, this method returns true if: + *