From 2a670d0f4560d9982fc709ab04a1d7687fca2924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Mal=C3=A9=C5=99?= Date: Tue, 17 Jan 2023 17:22:38 +0100 Subject: [PATCH] Enhancing the Authorization of Web endpoints guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michal Maléř Enhancing the Authorization of Web endpoints guide Signed-off-by: Michal Maléř Fixes Signed-off-by: Michal Maléř Fixes Signed-off-by: Michal Maléř Apply suggestions from code JHerrmann review Co-authored-by: jherrman Review Signed-off-by: Michal Maléř Adding back an annotation example Signed-off-by: Michal Maléř Apply suggestions from code review Co-authored-by: Rolfe Dlugy-Hegwer Fixes Signed-off-by: Michal Maléř --- ...horization-of-web-endpoints-reference.adoc | 190 +++++++++++------- 1 file changed, 117 insertions(+), 73 deletions(-) diff --git a/docs/src/main/asciidoc/security-authorization-of-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorization-of-web-endpoints-reference.adoc index e39525d881292..4de7f00b044c5 100644 --- a/docs/src/main/asciidoc/security-authorization-of-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorization-of-web-endpoints-reference.adoc @@ -8,55 +8,68 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: security,web -Quarkus has an integrated pluggable web security layer. If security is enabled all HTTP requests will have a permission -check performed to make sure they are allowed to continue. +Quarkus has an integrated pluggable web security layer. If security is enabled, all HTTP requests will have a permission check performed to make sure they are allowed to continue. +This means you cannot use `@PermitAll` to open a path if the path is blocked by the `quarkus.http.auth.` configuration. -NOTE: Configuration authorization checks are executed before any annotation-based authorization check is done, so both -checks have to pass for a request to be allowed. This means you cannot use `@PermitAll` to open up a path if the path has -been blocked using `quarkus.http.auth.` configuration. If you are using JAX-RS you may want to consider using the -`quarkus.security.jaxrs.deny-unannotated-endpoints` or `quarkus.security.jaxrs.default-roles-allowed` to set default security -requirements instead of HTTP path level matching, as these properties can be overridden by annotations on an individual -endpoint. +[NOTE] +==== +If you are using JAX-RS, consider using `quarkus.security.jaxrs.deny-unannotated-endpoints` or `quarkus.security.jaxrs.default-roles-allowed` to set default security requirements instead of HTTP path-level matching because annotations can override these properties on an individual endpoint. +==== -Authorization is based on user roles that are provided by the security provider. +Authorization is based on user roles that the security provider provides. To customize these roles, a `SecurityIdentityAugmentor` can be created, see xref:security-customization.adoc#security-identity-customization[Security Identity Customization]. -== Authorization using Configuration +== Authorization using configuration + +Permissions are defined in the Quarkus configuration using permission sets, with each permission set specifying a policy for access control. -The default implementation allows you to define permissions using config in `application.properties`. An example -config is shown below: +.{project-name} policies summary +|=== +s| Built-in policy s| Description +s| `deny` | This policy denies all users. +s| `permit` | This policy permits all users. +s| `authenticated` | This policy permits only authenticated users. +|=== +You can define role-based policies that allow users with specified roles to access the resources. + +.Example of a role-based policy [source,properties] ---- -quarkus.http.auth.policy.role-policy1.roles-allowed=user,admin <1> +quarkus.http.auth.policy.role-policy1.roles-allowed=user,admin <1> +---- +<1> This defines a role-based policy that allows users with the `user` and `admin` roles. +Such a custom policy can be referenced by permission sets just like the built-in ones, as shown in the example below. -quarkus.http.auth.permission.roles1.paths=/roles-secured/*,/other/*,/api/* <2> -quarkus.http.auth.permission.roles1.policy=role-policy1 +Permission sets are defined in `application.properties` as follows: -quarkus.http.auth.permission.permit1.paths=/public/* <3> +.Example of policy configuration +[source,properties] +---- +quarkus.http.auth.permission.permit1.paths=/public/* <1> quarkus.http.auth.permission.permit1.policy=permit quarkus.http.auth.permission.permit1.methods=GET -quarkus.http.auth.permission.deny1.paths=/forbidden <4> +quarkus.http.auth.permission.deny1.paths=/forbidden <2> quarkus.http.auth.permission.deny1.policy=deny ----- -<1> This defines a role based policy that allows users with the `user` and `admin` roles. This is referenced by later rules. -<2> This is a permission set that references the previously defined policy. `roles1` is an arbitrary name, you can call the permission sets whatever you want. -<3> This permission references the default `permit` built-in policy to allow `GET` methods to `/public`. This is actually a no-op in this example, as this request would have been allowed anyway. -<4> This permission references the built-in `deny` policy for `/forbidden`. This is an exact path match as it does not end with `*`. -Permissions are defined in config using permission sets. These are arbitrarily named permission grouping. Each permission -set must specify a policy that is used to control access. There are three built-in policies: `deny`, `permit` and `authenticated`, -which respectively permits all, denies all and only allows authenticated users. +quarkus.http.auth.permission.roles1.paths=/roles-secured/*,/other/*,/api/* <3> +quarkus.http.auth.permission.roles1.policy=role-policy1 +---- +<1> This permission references the default `permit` built-in policy to allow `GET` methods to `/public`. +In this case, the demonstrated setting would not affect this example because this request is allowed anyway. +<2> This permission references the built-in `deny` policy for `/forbidden`. +This is an exact path match as it does not end with `*`. +<3> This is a permission set that references the previously defined policy. +`roles1` is an example name; you can call the permission sets whatever you want. -It is also possible to define role based policies, as shown in the example. These policies will only allow users with the -specified roles to access the resources. === Matching on paths, methods -Permission sets can also specify paths and methods as a comma separated list. If a path ends with `*` then it is considered -to be a wildcard match and will match all sub paths, otherwise it is an exact match and will only match that specific path: +Permission sets can also specify paths and methods as a comma-separated list. +If a path ends with the `*` wildcard, the query it generates matches all sub-paths. +Otherwise, it queries for an exact match and will only match that specific path: [source,properties] ---- @@ -65,18 +78,17 @@ quarkus.http.auth.permission.permit1.policy=permit quarkus.http.auth.permission.permit1.methods=GET,HEAD ---- -=== Matching path but not method +=== Matching a path but not a method -If a request would match one or more permission sets based on the path, but does not match any due to method requirements -then the request is rejected. +The request is rejected if a request matches one or more permission sets based on the path but does not match any due to method requirements. TIP: Given the above permission set, `GET /public/foo` would match both the path and method and thus be allowed, -whereas `POST /public/foo` would match the path but not the method and would thus be rejected. +whereas `POST /public/foo` would match the path but not the method and would therefore be rejected. === Matching multiple paths: longest path wins -Matching is always done on a longest path wins basis, less specific permission sets are not considered if a more specific one -has been matched: +Matching is always done on the "longest path wins" basis. +Less specific permission sets are not considered if a more specific one has been matched: [source,properties] ---- @@ -89,13 +101,13 @@ quarkus.http.auth.permission.deny1.policy=deny ---- TIP: Given the above permission set, `GET /public/forbidden-folder/foo` would match both permission sets' paths, -but because it matches the `deny1` permission set's path on a longer match, `deny1` will be chosen and the request will +but because it matches the `deny1` permission set's path on a longer match, `deny1` will be chosen, and the request will be rejected. [NOTE] ==== -Subpath permissions always win against the root path permissions as explained above in the `deny1` versus `permit1` permission example. -Here is another example showing a subpath permission allowing a public resource access with the root path permission requiring the authorization: +Subpath permissions always win against the root path permissions, as explained above in the `deny1` versus `permit1` permission example. +Here is another example showing subpath permission allowing a public resource access with the root path permission requiring the authorization: [source,properties] ---- @@ -110,10 +122,9 @@ quarkus.http.auth.permission.public.policy=permit === Matching multiple paths: most specific method wins -If a path is registered with multiple permission sets then any permission sets that specify an HTTP method will take -precedence and permissions sets without a method will not be considered (assuming of course the method matches). In this -instance, the permission sets without methods will only come into effect if the request method does not match any of the -sets with method permissions. +When a path is registered with multiple permission sets, +the permission sets that explicitly specify an HTTP method that matches the request will take precedence. +In this instance, the permission sets without methods will only come into effect if the request method does not match permission sets with the method specification. [source,properties] ---- @@ -125,16 +136,18 @@ quarkus.http.auth.permission.deny1.paths=/public/* quarkus.http.auth.permission.deny1.policy=deny ---- -TIP: Given the above permission set, `GET /public/foo` would match both permission sets' paths, -but because it matches the `permit1` permission set's explicit method, `permit1` will be chosen and the request will -be accepted. `PUT /public/foo` on the other hand, will not match the method permissions of `permit1` and so -`deny1` will be activated and reject the request. +[NOTE] +==== +Given the above permission set, `GET /public/foo` would match the paths of both permission sets, but because it fits the explicit method of the `permit1` permission set, `permit1` is chosen, and the request is accepted. + +`PUT /public/foo`, on the other hand, will not match the method permissions of `permit1`, so `deny1` will be activated and reject the request. +==== === Matching multiple paths and methods: both win -If multiple permission sets specify the same path and method (or multiple have no method) then both permissions have to -allow access for the request to proceed. Note that for this to happen both have to either have specified the method, or -have no method, method specific matches take precedence as stated above: +Sometimes, the previously described rules allow multiple permission sets to win at the same time. +In that case, for the request to proceed, all the permissions must allow access. +Note that for this to happen, both have to either have specified the method or have no method. Method-specific matches take precedence. [source,properties] ---- @@ -148,29 +161,31 @@ quarkus.http.auth.permission.roles2.paths=/api/*,/admin/* quarkus.http.auth.permission.roles2.policy=admin-policy1 ---- -TIP: Given the above permission set, `GET /api/foo` would match both permission sets' paths, -so would require both the `user` and `admin` roles. +TIP: Given the above permission set, `GET /api/foo` would match both permission sets' paths, requiring both the `user` and `admin` roles. === Configuration Properties to Deny access -There are three configuration settings that alter the RBAC Deny behavior: +The following configuration settings alter the RBAC Deny behavior: `quarkus.security.jaxrs.deny-unannotated-endpoints=true|false`:: -If set to true, the access will be denied for all JAX-RS endpoints by default, so if a JAX-RS endpoint does not have any security annotations -then it will default to `@DenyAll` behaviour. This is useful to ensure you cannot accidentally expose an endpoint that is supposed to be secured. Defaults to `false`. +If set to true, the access will be denied for all JAX-RS endpoints by default, so if a JAX-RS endpoint does not have any security annotations, it will default to the `@DenyAll` behavior. +This is useful to ensure you cannot accidentally expose an endpoint that is supposed to be secured. +Defaults to `false`. `quarkus.security.jaxrs.default-roles-allowed=role1,role2`:: -Defines the default role requirements for unannotated endpoints. The role '**' is a special role that means any authenticated user. This cannot be combined with -`deny-unannotated-endpoints`, as the deny will take effect instead. +Defines the default role requirements for unannotated endpoints. +The `**` role is a special role that means any authenticated user. +This cannot be combined with `deny-unannotated-endpoints`, as `deny` takes the effect instead. `quarkus.security.deny-unannotated-members=true|false`:: - if set to true, the access will be denied to all CDI methods and JAX-RS endpoints that do not have security annotations but are defined in classes that contain methods with -security annotations. Defaults to `false`. +security annotations. +Defaults to `false`. === Disabling permissions -Permissions can be disabled at build time with an `enabled` property for each declared permission, for example: +Permissions can be disabled at build time with an `enabled` property for each declared permission, such as: [source,properties] ---- @@ -180,9 +195,10 @@ quarkus.http.auth.permission.permit1.policy=permit quarkus.http.auth.permission.permit1.methods=GET,HEAD ---- -and enabled at runtime with a system property or environment variable, for example: `-Dquarkus.http.auth.permission.permit1.enabled=true`. +Permissions can be reenabled at runtime with a system property or environment variable, such as: +`-Dquarkus.http.auth.permission.permit1.enabled=true`. -== Permission paths and http root path +=== Permission paths and HTTP root path The `quarkus.http.root-path` configuration property is used to change the xref:http-reference.adoc#context-path[http endpoint context path]. @@ -200,7 +216,9 @@ This configuration is equivalent to the following: quarkus.http.auth.permission.permit1.paths=${quarkus.http.root-path}/public/*,${quarkus.http.root-path}/css/*,${quarkus.http.root-path}/js/*,${quarkus.http.root-path}/robots.txt ---- -A leading slash will change how the configured permission path is interpreted. The configured URL will be used as-is, and paths will not be adjusted if the value of `quarkus.http.root-path` is changed. For example: +A leading slash will change how the configured permission path is interpreted. +The configured URL will be used as-is, and paths will not be adjusted if the value of `quarkus.http.root-path` is changed. +For example: [source,properties] ---- @@ -211,14 +229,26 @@ This configuration will only impact resources served from the fixed/static URL ` See link:https://quarkus.io/blog/path-resolution-in-quarkus/[Path Resolution in Quarkus] for more information. + [#standard-security-annotations] -== Authorization using Annotations +== Authorization using annotations -Quarkus comes with built-in security to allow for Role-Based Access Control (link:https://en.wikipedia.org/wiki/Role-based_access_control[RBAC]) +{project-name} comes with built-in security to allow for Role-Based Access Control (link:https://en.wikipedia.org/wiki/Role-based_access_control[RBAC]) based on the common security annotations `@RolesAllowed`, `@DenyAll`, `@PermitAll` on REST endpoints and CDI beans. -An example of an endpoint that makes use of both JAX-RS and Common Security annotations to describe and secure its endpoints is given in <>. Quarkus also provides -the `io.quarkus.security.Authenticated` annotation that will permit any authenticated user to access the resource -(equivalent to `@RolesAllowed("**")`). + +.{project-name} annotation types summary +|=== +s| Annotation type s| Description +s| @DenyAll | Specifies that no security roles are allowed to invoke the specified methods. +s| @PermitAll | Specifies that all security roles are allowed to invoke the specified methods. + +`@PermitAll` lets everybody in even without authentication. +s| @RolesAllowed | Specifies the list of security roles permitted to access methods in an application. + +As an equivalent to `@RolesAllowed("**")`, {project-name} also provides the `io.quarkus.security.Authenticated` annotation that permits any authenticated user to access the resource. +|=== + +<> featured in this chapter demonstrates an endpoint that uses both JAX-RS and Common Security annotations to describe and secure its endpoints. [#subject-example] .SubjectExposingResource Example @@ -265,17 +295,30 @@ public class SubjectExposingResource { } } ---- -<1> This `/subject/secured` endpoint requires an authenticated user that has been granted the role "Tester" through the use of the `@RolesAllowed("Tester")` annotation. +<1> The `/subject/secured` endpoint requires an authenticated user with the granted "Tester" role through the use of the `@RolesAllowed("Tester")` annotation. <2> The endpoint obtains the user principal from the JAX-RS `SecurityContext`. This will be non-null for a secured endpoint. <3> The `/subject/unsecured` endpoint allows for unauthenticated access by specifying the `@PermitAll` annotation. -<4> This call to obtain the user principal will return null if the caller is unauthenticated, non-null if the caller is authenticated. -<5> The `/subject/denied` endpoint disallows any access regardless of whether the call is authenticated by specifying the `@DenyAll` annotation. +<4> The call to obtain the user principal returns null if the caller is unauthenticated and non-null if the caller is authenticated. +<5> The `/subject/denied` endpoint declares the `@DenyAll` annotation, thus disallowing all direct access to it as a REST method, regardless of the user calling it. The method is still invokable internally by other methods in this class. -CAUTION: Please refer to the xref:security-built-in-authentication-support-concept.adoc#proactive-authentication[Proactive Authentication] section of the Built-In Authentication Support guide if you plan to use standard security annotations on IO thread. +CAUTION: Please refer to the xref:security-built-in-authentication-support-concept.adoc#proactive-authentication[Proactive Authentication] section of the Built-In Authentication Support guide if you plan to use standard security annotations on the IO thread. The `@RolesAllowed` annotation value supports <> including default values and nested Property Expressions. Configuration properties used with the annotation are resolved at runtime. +.Annotation value examples +|=== +s| Annotation s| Value explanation +s| `@RolesAllowed("${admin-role}")` | The endpoint will allow users with the role denoted by the value of the `admin-role` property. +s| `@RolesAllowed("${tester.group}-${tester.role}")` | An example showing that the value can contain multiple variables. + +s| `@RolesAllowed("${customer:User}")` | A default value demonstration. +The required role will be denoted by the value of the `customer` property, but if that property is not specified, a role named `User` will be required as a default. +|=== + + +.Example of a property expressions usage in the `@RolesAllowed` annotation + [source,properties] ---- admin=Administrator @@ -337,10 +380,11 @@ public class SubjectExposingResource { } } ---- -<1> The `@RolesAllowed` annotation value is set to the value of the `admin`. -<2> This `/subject/software-tester` endpoint requires an authenticated user that has been granted the role "Software-Tester". It is possible to use multiple expressions in the role definition. +<1> The `@RolesAllowed` annotation value is set to the value of `Administrator`. +<2> This `/subject/software-tester` endpoint requires an authenticated user that has been granted the role "Software-Tester". +It is possible to use multiple expressions in the role definition. <3> This `/subject/user` endpoint requires an authenticated user that has been granted the role "User" through the use of the `@RolesAllowed("${customer:User}")` annotation, as we did not set the configuration property `customer`. -<4> This `/subject/secured` endpoint requires an authenticated user that has been granted the role `User` in production, but allows any authenticated user in development mode. +<4> This `/subject/secured` endpoint requires an authenticated user that has been granted the role `User` in production but allows any authenticated user in development mode. == References