diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/security/SecurityDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/security/SecurityDocs.java new file mode 100644 index 000000000000..43c6310d526e --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/security/SecurityDocs.java @@ -0,0 +1,232 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.security; + +import java.security.Principal; + +import org.eclipse.jetty.ee11.servlet.security.ConstraintMapping; +import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler; +import org.eclipse.jetty.ee11.webapp.WebAppContext; +import org.eclipse.jetty.security.Constraint; +import org.eclipse.jetty.security.HashLoginService; +import org.eclipse.jetty.security.SecurityHandler; +import org.eclipse.jetty.security.authentication.BasicAuthenticator; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.resource.ResourceFactory; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class SecurityDocs +{ + public void pathMapped() throws Exception + { + // tag::pathMapped[] + class AppHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Retrieve the authenticated user for this request. + Principal principal = Request.getAuthenticationState(request).getUserPrincipal(); + System.getLogger("app").log(INFO, "Current user is: {0}", principal); + + callback.succeeded(); + return true; + } + } + + Server server = new Server(); + + // The ContextHandler for the application. + ContextHandler contextHandler = new ContextHandler("/app"); + + // HashLoginService maps users, passwords and roles + // from the realm.properties file in the class-path. + HashLoginService loginService = new HashLoginService(); + loginService.setConfig(ResourceFactory.of(contextHandler).newClassLoaderResource("realm.properties")); + + // Use Basic authentication, which requires a secure transport. + BasicAuthenticator authenticator = new BasicAuthenticator(); + authenticator.setLoginService(loginService); + + // The SecurityHandler.PathMapped maps URI paths to constraints. + SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped(); + securityHandler.setAuthenticator(authenticator); + securityHandler.setLoginService(loginService); + + // Configure constraints. + // Require that all requests use a secure transport. + securityHandler.put("/*", Constraint.SECURE_TRANSPORT); + // URI paths that start with /admin/ can only be accessed by users with the "admin" role. + securityHandler.put("/admin/*", Constraint.from("admin")); + + // Link the Handlers. + server.setHandler(contextHandler); + contextHandler.setHandler(securityHandler); + securityHandler.setHandler(new AppHandler()); + + server.start(); + // end::pathMapped[] + } + + public void pathMethodMapped() throws Exception + { + // tag::pathMethodMapped[] + class AppHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Retrieve the authenticated user for this request. + Principal principal = Request.getAuthenticationState(request).getUserPrincipal(); + System.getLogger("app").log(INFO, "Current user is: {0}", principal); + + callback.succeeded(); + return true; + } + } + + Server server = new Server(); + + // The ContextHandler for the application. + ContextHandler contextHandler = new ContextHandler("/app"); + + // HashLoginService maps users, passwords and roles + // from the realm.properties file in the class-path. + HashLoginService loginService = new HashLoginService(); + loginService.setConfig(ResourceFactory.of(contextHandler).newClassLoaderResource("realm.properties")); + + // Use Basic authentication, which requires a secure transport. + BasicAuthenticator authenticator = new BasicAuthenticator(); + authenticator.setLoginService(loginService); + + // The SecurityHandler.PathMapped maps URI paths to constraints. + SecurityHandler.PathMethodMapped securityHandler = new SecurityHandler.PathMethodMapped(); + securityHandler.setAuthenticator(authenticator); + securityHandler.setLoginService(loginService); + + // Configure constraints. + // Unless otherwise specified, access to resources is forbidden and requires secure transport. + securityHandler.put("/*", "*", Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT)); + // GET /data/* is allowed only to users with the "read" role. + securityHandler.put("/data/*", "GET", Constraint.from("read")); + // PUT /data/* is allowed only to users with the "write" role. + securityHandler.put("/data/*", "PUT", Constraint.from("write")); + + // Link the Handlers. + server.setHandler(contextHandler); + contextHandler.setHandler(securityHandler); + securityHandler.setHandler(new AppHandler()); + + server.start(); + // end::pathMethodMapped[] + } + + public void jakartaPathMapped() throws Exception + { + // tag::jakartaPathMapped[] + Server server = new Server(); + + WebAppContext webApp = new WebAppContext(); + webApp.setContextPath("/app"); + webApp.setWar("/path/to/app.war"); + + // HashLoginService maps users, passwords and roles + // from the realm.properties file in the server class-path. + HashLoginService loginService = new HashLoginService(); + loginService.setConfig(ResourceFactory.of(webApp).newClassLoaderResource("realm.properties")); + + // Use Basic authentication, which requires a secure transport. + BasicAuthenticator authenticator = new BasicAuthenticator(); + authenticator.setLoginService(loginService); + + ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + securityHandler.setAuthenticator(authenticator); + securityHandler.setLoginService(loginService); + + // Configure constraints. + ConstraintMapping constraintMapping = new ConstraintMapping(); + constraintMapping.setPathSpec("/*"); + constraintMapping.setConstraint(Constraint.SECURE_TRANSPORT); + securityHandler.addConstraintMapping(constraintMapping); + constraintMapping = new ConstraintMapping(); + constraintMapping.setPathSpec("/admin/*"); + constraintMapping.setConstraint(Constraint.from("admin")); + securityHandler.addConstraintMapping(constraintMapping); + + // Link the Handlers. + server.setHandler(webApp); + // Note the specific call to setSecurityHandler(). + webApp.setSecurityHandler(securityHandler); + + server.start(); + // end::jakartaPathMapped[] + } + + public void jakartaPathMethodMapped() throws Exception + { + // tag::jakartaPathMethodMapped[] + Server server = new Server(); + + WebAppContext webApp = new WebAppContext(); + webApp.setContextPath("/app"); + webApp.setWar("/path/to/app.war"); + + // HashLoginService maps users, passwords and roles + // from the realm.properties file in the server class-path. + HashLoginService loginService = new HashLoginService(); + loginService.setConfig(ResourceFactory.of(webApp).newClassLoaderResource("realm.properties")); + + // Use Basic authentication, which requires a secure transport. + BasicAuthenticator authenticator = new BasicAuthenticator(); + authenticator.setLoginService(loginService); + + ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + securityHandler.setAuthenticator(authenticator); + securityHandler.setLoginService(loginService); + + // Configure constraints. + // Forbid access for uncovered HTTP methods. + securityHandler.setDenyUncoveredHttpMethods(true); + // No HTTP method specified, therefore applies to all methods. + ConstraintMapping constraintMapping = new ConstraintMapping(); + constraintMapping.setPathSpec("/*"); + constraintMapping.setConstraint(Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT)); + securityHandler.addConstraintMapping(constraintMapping); + // GET /data/* is allowed only to users with the "read" role. + constraintMapping = new ConstraintMapping(); + constraintMapping.setPathSpec("/data/*"); + constraintMapping.setMethod("GET"); + constraintMapping.setConstraint(Constraint.combine(Constraint.SECURE_TRANSPORT, Constraint.from("read"))); + // PUT /data/* is allowed only to users with the "write" role. + constraintMapping = new ConstraintMapping(); + constraintMapping.setPathSpec("/data/*"); + constraintMapping.setMethod("PUT"); + constraintMapping.setConstraint(Constraint.combine(Constraint.SECURE_TRANSPORT, Constraint.from("write"))); + + // Link the Handlers. + server.setHandler(webApp); + // Note the specific call to setSecurityHandler(). + webApp.setSecurityHandler(securityHandler); + + server.start(); + // end::jakartaPathMethodMapped[] + } +} diff --git a/documentation/jetty/modules/programming-guide/pages/security/index.adoc b/documentation/jetty/modules/programming-guide/pages/security/index.adoc new file mode 100644 index 000000000000..e129d864f51b --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/security/index.adoc @@ -0,0 +1,246 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Security + +Web application security restricts access to resources within the web application, or imposes security requirements on the transport with which these resources are delivered to clients. + +The processing of the request may be: + +* _forbidden_; the request is immediately responded with a `403` status code. +* _allowed_; the request processing is allowed to continue to the application, which produces a response. +* _challenged_; the request may be allowed, but the credentials are missing (or wrong), so the request is responded with a `401` status code. + +Jetty implements web application security with 3 components: + +* A subclass of `org.eclipse.jetty.security.SecurityHandler`, see <> for the implementations available out-of-the-box. +* An implementation of `org.eclipse.jetty.security.Authenticator`, see <> for the implementations available out-of-the-box. +* An implementation of `org.eclipse.jetty.security.LoginService`, see <> for the implementations available out-of-the-box. + +These components interact in this way: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant SecurityHandler +participant Authenticator +participant LoginService + +SecurityHandler -> SecurityHandler : getConstraint() +SecurityHandler -> Authenticator : validateRequest() +Authenticator -> LoginService : login() +LoginService -> Authenticator : roles +Authenticator -> SecurityHandler : AuthenticationState +---- + +1. The `SecurityHandler` subclass returns a `Constraint` based on the subclass-specific logic, for example based on the request path, the request method, etc. +2. The `Constraint` is used to check if the request is trivially allowed, or trivially forbidden, or whether it requires a secure transport, and if so immediately handled by `SecurityHandler` with no further actions. +Otherwise, the `Constraint` declares what requirements about authorization, transport and roles are necessary for the request to be allowed. +3. The `SecurityHandler` calls `Authenticator.validateRequest(\...)` that performs implementation-specific logic to retrieve the authentication credentials, for example from HTTP request headers such as `Authorization`. +4. The `Authenticator` calls `LoginService.login(\...)` to verify the credentials and, if the verification is successful, obtain information about the roles associated with these credentials. +5. The `Authenticator` builds an `AuthenticationState` with the results of the call to `LoginService.login(\...)`, and returns it to the `SecurityHandler`. +6. The `SecurityHandler`, based on the received `AuthenticationState`, either allows the request to be processed by its child `Handler`, or sends an appropriate response to the client, for example a `401` challenge response or a `403` forbidden response. + +[[security-core-configuration]] +== Configuring Jetty Core Security + +This section is about configuring security for Jetty Core web applications. +To configure security for Jakarta EE web applications, see <>. + +`SecurityHandler` is typically configured as a child of the `ContextHandler` that represents the web application. + +A simple example uses `SecurityHandler.PathMapped` along with `BasicAuthenticator` and `HashLoginService`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/security/SecurityDocs.java[tags=pathMapped] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── ContextHandler /app + └── SecurityHandler + └── AppHandler +---- + +The `realm.properties` file used by `HashLoginService` is the following: + +.realm.properties +---- +# Format: :, +carol:password +david:password,admin +---- + +The behavior of the example above is the following: + +* For any request, _all_ the request path matches are collected and processed in order from the least significant to the most significant, combining their constraint. +* A non-secure request using the `http` scheme is replied with a `403` response, since secure transport is required. +* A request for `/app/foo` matches the constraint mapped to `+/*+`, and is replied with a `200` response; `AppHandler` would see a `null` request principal. +* A request for `/app/admin/bar` without `Authorization` header matches both the constraints mapped to `+/*+` and `+/admin/*+`, and is replied with a `401` response, since authentication is required. +* A request for `/app/admin/bar` with an `Authorization` header containing invalid or unknown credentials is replied with a `401` response, since authentication is required. +* A request for `/app/admin/bar` with an `Authorization` header containing valid credentials with a role that is not `admin` is replied with a `401` response, since authentication is required. +* A request for `/app/admin/bar` with an `Authorization` header containing valid credentials for user `david`, that has `admin` role, is replied with a `200` response, and `AppHandler` would see a request principal for user `david`. + +[TIP] +==== +It is good practice to have a default constraint for all paths (that is, for path `+/*+`), even if it is `Constraint.ALLOWED`. +In this way it is clear what is the intended behavior for paths that do not match more specific constraint configurations. +==== + +The more complex example below uses `SecurityHandler.PathMethodMapped` to allow users with the `read` role to read resources, and users with the `write` role to write resources: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/security/SecurityDocs.java[tags=pathMethodMapped] +---- + +.realm.properties +---- +# Format: :, +bob:password,read +alice:password,read,write +---- + +In the example above, access to any resource is by default forbidden, and requires secure transport. + +However, access to `/data/*` resources using the HTTP method `GET` is granted but only to users with `read` role. +Both `bob` and `alice` are granted access to these resources, but only if they use the `GET` method. + +Similarly, access to `/data/*` resources using the HTTP method `PUT` is granted but only to users with `write` role. +Only `alice` is granted access to these resources, but only if she uses the `PUT` method. + +[TIP] +==== +It is good practice to have a default constraint for all paths (that is, for path `+/*+`), and for all HTTP methods (that is, for method `+*+`), even if it is `Constraint.ALLOWED`. +In this way it is clear what is the intended behavior for paths and HTTP methods that do not match more specific constraint configurations. +==== + +[IMPORTANT] +==== +The behavior of `SecurityHandler` implementations for the Jetty Core API is to combine all the constraints that match. +This is different from the behavior of the `SecurityHandler` implementations for the Jakarta EE APIs, that only use the constraint of the most specific match. +==== + +[[security-jakarta-configuration]] +== Configuring Jakarta EE Security + +This section is about configuring security for Jakarta EE web applications. +To configure security for Jetty Core web applications, see <>. + +To configure Jakarta EE web application security you must use the `SecurityHandler` subclass `org.eclipse.jetty.{ee-current}.servlet.security.ConstraintSecurityHandler`. + +Below you can find a simple example that sets up a `ConstraintSecurityHandler` to secure your Jakarta EE web application: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/security/SecurityDocs.java[tags=jakartaPathMapped] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── WebAppContext /app + └── ConstraintSecurityHandler + └── ServletHandler + └── AppServlet (defined in app.war) +---- + +The `realm.properties` file used by `HashLoginService` is the following: + +.realm.properties +---- +# Format: :, +carol:password +david:password,admin +---- + +Note how the constraints are configured using `org.eclipse.jetty.ee11.servlet.security.ConstraintMapping`, that allows to configure the path, the HTTP method and the `Constraint`. + +The more complex example below uses `ConstraintSecurityHandler` to allow users with the `read` role to read resources, and users with the `write` role to write resources: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/security/SecurityDocs.java[tags=jakartaPathMethodMapped] +---- + +.realm.properties +---- +# Format: :, +bob:password,read +alice:password,read,write +---- + +In the example above, access to any resource is by default forbidden, and requires secure transport. + +However, access to `/data/*` resources using the HTTP method `GET` is granted but only to users with `read` role. +Both `bob` and `alice` are granted access to these resources, but only if they use the `GET` method. + +Similarly, access to `/data/*` resources using the HTTP method `PUT` is granted but only to users with `write` role. +Only `alice` is granted access to these resources, but only if she uses the `PUT` method. + +Note also that `ConstraintSecurityHandler.denyUncoveredHttpMethods=true` to make sure that requests that match `+/data/*+` but are neither `GET` nor `PUT` are denied. + +[IMPORTANT] +==== +The behavior of `SecurityHandler` implementations for the Jakarta EE API is to only use the constraint of the most specific match. +This is different from the behavior of the `SecurityHandler` implementations for the Jetty Core APIs, that combine all the constraints that match. +==== + +[[security-handler]] +== `SecurityHandler` Implementations + +Jetty provides the following `SecurityHandler` implementations: + +* `SecurityHandler.PathMapped`, that allows you to configure constraints based on the request path. +* `SecurityHandler.PathMethodMapped`, that allows you to configure constraints based on the request path and the request HTTP method. +* `ConstraintSecurityHandler`, that implements the Jakarta EE web application security constraints. + +[[security-authenticator]] +== `Authenticator` Implementations + +Jetty provides the following `Authenticator` implementations: + +* `BasicAuthenticator`, that implements link:https://datatracker.ietf.org/doc/html/rfc7617[Basic authentication]. +* `DigestAuthentication`, that implements link:https://datatracker.ietf.org/doc/html/rfc7616[Digest authentication]. +* `FormAuthenticator`, that implements HTML form authentication using the link:https://jakarta.ee/specifications/servlet/6.1/jakarta-servlet-spec-6.1#form-based-authentication[`j_security_check` mechanism]. +* `SslClientCertAuthenticator`, that implements authentication based on client certificates. +* `SPNEGOAuthenticator`, that implements link:https://datatracker.ietf.org/doc/html/rfc4178[SPNEGO authentication]. +* `EthereumAuthenticator`, that implements link:https://eips.ethereum.org/EIPS/eip-4361[Sign-In with Ethereum authentication] (see also xref:security/siwe-support.adoc[this dedicated section]). +* `OpenIdAuthenticator`, that implements link:https://openid.net/specs/openid-connect-core-1_0-final.html[OpenID Connect Core 1.0 authentication] (see also xref:security/openid-support.adoc[this dedicated section]). +* `MultiAuthenticator`, that supports multiple authentication mechanisms for the same web application. +* `JaspiAuthenticator`, for the link:https://jakarta.ee/specifications/authentication/[Jakarta Authentication API]. + +[[security-login-service]] +== `LoginService` Implementations + +Jetty provides the following `LoginService` implementations: + +* `HashLoginService`, that verifies credentials retrieved from a `+*.properties+` file. +* `JDBCLoginService`, that verifies credentials retrieved from a RDBMS using the JDBC APIs. +* `DataSourceLoginService`, that verifies credentials retrieved from a RDBMS using the `DataSource` APIs. +* `JAASLoginService`, that verifies credentials retrieved using the JAAS APIs. +* `AnyUserLoginService`, that does not verify credentials, but can delegate to another `LoginService` to provide roles for authenticated users. + +Some `Authenticator` implementation require a specific `LoginService`, so the following implementation are also available: + +* `SPNEGOLoginService`, used in conjunction with `SPNEGOAuthenticator`. +* `OpenIdLoginService`, used in conjunction with `OpenIdAuthenticator`. diff --git a/documentation/jetty/modules/programming-guide/pages/server/http.adoc b/documentation/jetty/modules/programming-guide/pages/server/http.adoc index b304a90d1d31..1cfef3c2e583 100644 --- a/documentation/jetty/modules/programming-guide/pages/server/http.adoc +++ b/documentation/jetty/modules/programming-guide/pages/server/http.adoc @@ -1583,6 +1583,11 @@ The ``Handler``s children of `PathMappingsHandler` may extend < + org.eclipse.jetty + jetty-security + {jetty-version} + +---- + +Web application security is primarily provided by <>, and it is covered in details in xref:security/index.adoc[this section]. [[application]] === Writing HTTP Server Applications diff --git a/jetty-core/jetty-security/pom.xml b/jetty-core/jetty-security/pom.xml index b47a437b74dd..4e96c8572dbd 100644 --- a/jetty-core/jetty-security/pom.xml +++ b/jetty-core/jetty-security/pom.xml @@ -27,6 +27,7 @@ org.slf4j slf4j-api + org.apache.directory.api api-asn1-api diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java index 88ff06cf900d..46e9000a8ad7 100644 --- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java +++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java @@ -21,6 +21,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.ServiceLoader; import java.util.Set; @@ -651,6 +652,30 @@ protected Set getKnownRoles() return Collections.emptySet(); } + private static int compareMappedResources(MappedResource mr1, MappedResource mr2) + { + PathSpecGroup g1 = mr1.getPathSpec().getGroup(); + PathSpecGroup g2 = mr2.getPathSpec().getGroup(); + int l1 = mr1.getPathSpec().getSpecLength(); + int l2 = mr2.getPathSpec().getSpecLength(); + if (g1.equals(g2)) + return Integer.compare(l1, l2); + return Integer.compare(pathSpecGroupOrder(g1), pathSpecGroupOrder(g2)); + } + + private static int pathSpecGroupOrder(PathSpecGroup group) + { + return switch (group) + { + case EXACT -> 5; + case ROOT -> 4; + case SUFFIX_GLOB -> 3; + case MIDDLE_GLOB -> 2; + case PREFIX_GLOB -> 1; + case DEFAULT -> 0; + }; + } + public class NotChecked implements Principal { @Override @@ -671,25 +696,23 @@ public SecurityHandler getSecurityHandler() } } - // TODO consider method mapping version - /** *

A concrete implementation of {@link SecurityHandler} that uses a {@link PathMappings} to * match request to a list of {@link Constraint}s, which are applied in the order of * least significant to most significant. *

* An example of using this class is: - *

-     *     SecurityHandler.PathMapped handler = new SecurityHandler.PathMapped();
-     *     handler.put("/*", Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT);
-     *     handler.put("", Constraint.ALLOWED);
-     *     handler.put("/login", Constraint.ALLOWED);
-     *     handler.put("*.png", Constraint.ANY_TRANSPORT);
-     *     handler.put("/admin/*", Constraint.from("admin", "operator"));
-     *     handler.put("/admin/super/*", Constraint.from("operator"));
-     *     handler.put("/user/*", Constraint.ANY_USER);
-     *     handler.put("*.xml", Constraint.FORBIDDEN);
-     * 
+ *
{@code
+     * SecurityHandler.PathMapped handler = new SecurityHandler.PathMapped();
+     * handler.put("/*", Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT));
+     * handler.put("", Constraint.ALLOWED);
+     * handler.put("/login", Constraint.ALLOWED);
+     * handler.put("*.png", Constraint.ANY_TRANSPORT);
+     * handler.put("/admin/*", Constraint.from("admin", "operator"));
+     * handler.put("/admin/super/*", Constraint.from("operator"));
+     * handler.put("/user/*", Constraint.ANY_USER);
+     * handler.put("*.xml", Constraint.FORBIDDEN);
+     * }
*

* When {@link #getConstraint(String, Request)} is called, any matching * constraints are sorted into least to most significant with @@ -698,15 +721,17 @@ public SecurityHandler getSecurityHandler() * For example: *

*
    - *
  • {@code "/admin/index.html"} matches {@code "/*"} and {@code "/admin/*"}, resulting in a - * constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#SECURE}.
  • - *
  • {@code "/admin/logo.png"} matches {@code "/*"}, {@code "/admin/*"} and {@code "*.png"}, resulting in a - * constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#ANY}.
  • - *
  • {@code "/admin/config.xml"} matches {@code "/*"}, {@code "/admin/*"} and {@code "*.xml"}, resulting in a - * constraint of {@link Authorization#FORBIDDEN} and {@link Transport#SECURE}.
  • - *
  • {@code "/admin/super/index.html"} matches {@code "/*"}, {@code "/admin/*"} and {@code "/admin/super/*"}, resulting in a - * constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#SECURE}.
  • + *
  • {@code "/admin/index.html"} matches {@code "/*"} and {@code "/admin/*"}, resulting in a + * constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#SECURE}.
  • + *
  • {@code "/admin/logo.png"} matches {@code "/*"}, {@code "/admin/*"} and {@code "*.png"}, resulting in a + * constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#ANY}.
  • + *
  • {@code "/admin/config.xml"} matches {@code "/*"}, {@code "/admin/*"} and {@code "*.xml"}, resulting in a + * constraint of {@link Authorization#FORBIDDEN} and {@link Transport#SECURE}.
  • + *
  • {@code "/admin/super/index.html"} matches {@code "/*"}, {@code "/admin/*"} and {@code "/admin/super/*"}, + * resulting in a constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#SECURE}.
  • *
+ *

If there is no match for the request path, then the constraint is assumed to be {@link Constraint#ALLOWED}.

+ *

It is therefore good practice to always explicitly configure a constraint for path {@code /*} or {@code /}.

*/ public static class PathMapped extends SecurityHandler implements Comparator { @@ -723,11 +748,27 @@ public PathMapped(Handler handler) super(handler); } + /** + *

Associates the specified request path pattern with the specified {@link Constraint}.

+ * + * @param pathSpec the request path pattern to match + * @param constraint the associated {@link Constraint} + * @return the previous {@link Constraint} associated with the request path pattern, + * or {@code null} if there was no previous association + */ public Constraint put(String pathSpec, Constraint constraint) { return put(PathSpec.from(pathSpec), constraint); } + /** + *

Associates the specified request path pattern with the specified {@link Constraint}.

+ * + * @param pathSpec the request path pattern to match + * @param constraint the associated {@link Constraint} + * @return the previous {@link Constraint} associated with the request path pattern, + * or {@code null} if there was no previous association + */ public Constraint put(PathSpec pathSpec, Constraint constraint) { Set roles = constraint.getRoles(); @@ -798,15 +839,7 @@ public int compare(PathSpec ps1, PathSpec ps2) int compare(MappedResource c1, MappedResource c2) { - PathSpecGroup g1 = c1.getPathSpec().getGroup(); - PathSpecGroup g2 = c2.getPathSpec().getGroup(); - int l1 = c1.getPathSpec().getSpecLength(); - int l2 = c2.getPathSpec().getSpecLength(); - - if (g1.equals(g2)) - return Integer.compare(l1, l2); - - return Integer.compare(pathSpecGroupPrecedence(g1), pathSpecGroupPrecedence(g2)); + return compareMappedResources(c1, c2); } /** @@ -825,15 +858,160 @@ int compare(MappedResource c1, MappedResource c2) */ protected int pathSpecGroupPrecedence(PathSpecGroup group) { - return switch (group) + return pathSpecGroupOrder(group); + } + + @Override + protected Set getKnownRoles() + { + return _knownRoles; + } + } + + /** + *

A concrete implementation of {@link SecurityHandler} that uses a {@link PathMappings} + * to match request paths to a map of an HTTP method to a {@link Constraint}.

+ *

The token {@code *} is used to indicate all HTTP methods.

+ *

Request path matches are sorted from the least significant to the most significant, + * and the associated constraints are combined in order.

+ *

For example:

+ *
{@code
+     * SecurityHandler.PathMethodMapped handler = new SecurityHandler.PathMethodMapped();
+     * handler.put(PathSpec.from("/*"), "*", Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT));
+     * handler.put(PathSpec.from("/releases/*"), "GET", Constraint.from("read"));
+     * handler.put(PathSpec.from("/releases/*"), "PUT", Constraint.from("write"));
+     * }
+ *

For these request paths:

+ *
    + *
  • {@code /foo} matches {@code /*}; + * any HTTP method results in a constraint with {@link Authorization#FORBIDDEN} and {@link Transport#SECURE}
  • + *
  • {@code /releases/jetty-12.1.0.tar.gz} matches both {@code /*} and {@code /releases/*}; + * method {@code GET} results in a constraint with {@link Authorization#SPECIFIC_ROLE} with role {@code read} + * and {@link Transport#SECURE}; + * method {@code PUT} results in a constraint with {@link Authorization#SPECIFIC_ROLE} with role {@code write} + * and {@link Transport#SECURE}; + * any other HTTP method results in a constraint with {@link Authorization#FORBIDDEN} and {@link Transport#SECURE}
  • + *
+ *

If there is no match for the request path, then the constraint is assumed to be {@link Constraint#ALLOWED}.

+ *

If there is no match for the request URI, or no match for the HTTP method, then the constraint is assumed + * to be {@link Constraint#ALLOWED}.

+ *

It is therefore good practice to always explicitly configure a constraint for path {@code /*} or {@code /} + * and HTTP method {@code *}.

+ */ + public static class PathMethodMapped extends SecurityHandler + { + private static final String ALL_METHODS = "*"; + + private final PathMappings> _constraints = new PathMappings<>(); + private final Set _knownRoles = new HashSet<>(); + + public PathMethodMapped() + { + this(null); + } + + public PathMethodMapped(Handler handler) + { + super(handler); + } + + /** + *

Associates the given {@link Constraint} to the given request path patten and HTTP method.

+ * + * @param pathSpec the {@link PathSpec} associated to the given constraint + * @param method the HTTP method associated to the given constraint, or {@code null} or {@code *} + * to indicate all HTTP methods + * @param constraint the constraint to associate + * @return the previous constraint associated with the given path and HTTP method, + * or {@code null} is there was no association + */ + public Constraint put(String pathSpec, String method, Constraint constraint) + { + return put(PathSpec.from(pathSpec), method, constraint); + } + + /** + *

Associates the given {@link Constraint} to the given request path pattern and HTTP method.

+ + * @param pathSpec the {@link PathSpec} associated to the given constraint + * @param method the HTTP method associated to the given constraint, or {@code null} or {@code *} + * to indicate all HTTP methods + * @param constraint the constraint to associate + * @return the previous constraint associated with the given path and HTTP method, + * or {@code null} is there was no association + */ + public Constraint put(PathSpec pathSpec, String method, Constraint constraint) + { + Objects.requireNonNull(pathSpec); + if (method == null) + method = ALL_METHODS; + Objects.requireNonNull(constraint); + Map methodConstraints = _constraints.computeIfAbsent(pathSpec, k -> new HashMap<>()); + Constraint result = methodConstraints.put(method, constraint); + if (result != null) + recomputeKnownRoles(); + else + _knownRoles.addAll(constraint.getRoles()); + return result; + } + + /** + *

Associates the given {@link Constraint} to the given request path pattern and HTTP methods.

+ * + * @param pathSpec the {@link PathSpec} associated to the given constraint + * @param methods the list of HTTP methods associated to the given constraint + * @param constraint the constraint to associate + */ + public void put(PathSpec pathSpec, List methods, Constraint constraint) + { + if (methods.isEmpty() || methods.contains(ALL_METHODS) || methods.contains(null)) + throw new IllegalArgumentException("Invalid method list"); + methods.forEach(method -> put(pathSpec, method, constraint)); + } + + @Override + protected Constraint getConstraint(String pathInContext, Request request) + { + List>> matches = _constraints.getMatches(pathInContext); + + if (matches == null || matches.isEmpty()) + return Constraint.ALLOWED; + + // Sort from least specific to most specific to combine constraints properly. + if (matches.size() > 1) + matches.sort(SecurityHandler::compareMappedResources); + + String method = request.getMethod(); + + Constraint result = null; + for (MappedResource> match : matches) { - case EXACT -> 5; - case ROOT -> 4; - case SUFFIX_GLOB -> 3; - case MIDDLE_GLOB -> 2; - case PREFIX_GLOB -> 1; - case DEFAULT -> 0; - }; + Map methodConstraints = match.getResource(); + + // A constraint for all HTTP methods may be used to establish + // defaults such as Constraint.SECURE_TRANSPORT, so always + // combine it with the Constraint for the specific HTTP method. + Constraint allMethodsConstraint = methodConstraints.get(ALL_METHODS); + Constraint specificMethodConstraint = methodConstraints.get(method); + Constraint constraint = Constraint.combine(allMethodsConstraint, specificMethodConstraint); + + // Combine the constraints from all URI matches. + result = Constraint.combine(result, constraint); + } + + return result; + } + + private void recomputeKnownRoles() + { + _knownRoles.clear(); + for (Map m : _constraints.values()) + { + for (Constraint c : m.values()) + { + _knownRoles.addAll(c.getRoles()); + } + } } @Override diff --git a/jetty-core/jetty-security/src/test/java/org/eclipse/jetty/security/PathMethodMappedTest.java b/jetty-core/jetty-security/src/test/java/org/eclipse/jetty/security/PathMethodMappedTest.java new file mode 100644 index 000000000000..332870d69c6a --- /dev/null +++ b/jetty-core/jetty-security/src/test/java/org/eclipse/jetty/security/PathMethodMappedTest.java @@ -0,0 +1,454 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.security; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; +import java.util.function.Consumer; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.security.authentication.BasicAuthenticator; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.ResourceFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PathMethodMappedTest +{ + private Server server; + private ServerConnector connector; + private ServerConnector tlsConnector; + + private void start(Consumer configurator) throws Exception + { + server = new Server(); + HttpConfiguration httpConfig = new HttpConfiguration(); + connector = new ServerConnector(server, 1, 1, new HttpConnectionFactory(httpConfig)); + server.addConnector(connector); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12")); + sslContextFactory.setKeyStorePassword("storepwd"); + tlsConnector = new ServerConnector(server, 1, 1, sslContextFactory); + server.addConnector(tlsConnector); + + SecurityHandler.PathMethodMapped securityHandler = new SecurityHandler.PathMethodMapped(); + configurator.accept(securityHandler); + + Path realm = MavenPaths.findTestResourceFile("test-realm.properties"); + HashLoginService loginService = new HashLoginService("Test", ResourceFactory.of(server).newResource(realm)); + BasicAuthenticator authenticator = new BasicAuthenticator(); + authenticator.setLoginService(loginService); + securityHandler.setAuthenticator(authenticator); + securityHandler.setLoginService(loginService); + + ContextHandler contextHandler = new ContextHandler(securityHandler); + server.setHandler(contextHandler); + server.start(); + + httpConfig.setSecurePort(tlsConnector.getLocalPort()); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(server); + } + + @Test + public void testNoMappings() throws Exception + { + start(s -> + { + s.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + }); + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("test", "password"))) + ); + + HttpTester.Response response = HttpTester.parseResponse(client); + // No path matches, request is allowed. + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } + + @Test + public void testAllPathsOneMethodMappingRequestWithOtherMethodAllowed() throws Exception + { + start(s -> + { + s.put("/*", "TRACE", Constraint.FORBIDDEN); + s.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + }); + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("test", "password"))) + ); + + HttpTester.Response response = HttpTester.parseResponse(client); + // No method matches, request is allowed. + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } + + @Test + public void testAllPathsAllMethodsForbidden() throws Exception + { + start(s -> + { + s.put("/*", "*", Constraint.FORBIDDEN); + s.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + }); + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("test", "password"))) + ); + + HttpTester.Response response = HttpTester.parseResponse(client); + // All requests are forbidden. + assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + } + } + + @Test + public void testAllPathsOnlyGETAllowed() throws Exception + { + start(s -> + { + s.put("/*", "*", Constraint.FORBIDDEN); + s.put("/*", "GET", Constraint.from("read")); + s.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Request.AuthenticationState state = Request.getAuthenticationState(request); + assertNotNull(state); + assertEquals("reader", state.getUserPrincipal().getName()); + callback.succeeded(); + return true; + } + }); + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("test", "password"))) + ); + + HttpTester.Response response = HttpTester.parseResponse(client); + // User "test" does not have roles, so forbidden. + assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("reader", "password"))) + ); + + response = HttpTester.parseResponse(client); + // User "reader" has role "read", so it can only perform GET requests. + assertEquals(HttpStatus.OK_200, response.getStatus()); + + client.write(UTF_8.encode(""" + PUT /file.txt HTTP/1.1 + Host: localhost + Content-Length: 0 + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("reader", "password"))) + ); + + response = HttpTester.parseResponse(client); + // Method PUT is forbidden. + assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + } + } + + @Test + public void testAllPathsOnlyGETAllowedSecureTransport() throws Exception + { + start(s -> + { + s.put("/*", "*", Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT)); + s.put("/*", "GET", Constraint.from("read")); + s.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + assertTrue(request.isSecure()); + Request.AuthenticationState state = Request.getAuthenticationState(request); + assertNotNull(state); + assertEquals("reader", state.getUserPrincipal().getName()); + callback.succeeded(); + return true; + } + }); + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("test", "password"))) + ); + + HttpTester.Response response = HttpTester.parseResponse(client); + // Clear text, redirect to secure. + assertTrue(HttpStatus.isRedirection(response.getStatus())); + String location = response.get(HttpHeader.LOCATION); + assertNotNull(location); + assertThat(location, startsWith("https://")); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, SslContextFactory.TRUST_ALL_CERTS, null); + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + try (Socket secureClient = sslSocketFactory.createSocket("localhost", tlsConnector.getLocalPort())) + { + String request = """ + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("test", "password")); + OutputStream output = secureClient.getOutputStream(); + output.write(request.getBytes(UTF_8)); + output.flush(); + + InputStream input = secureClient.getInputStream(); + response = HttpTester.parseResponse(input); + // Unauthorized user. + assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + + request = """ + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("reader", "password")); + output.write(request.getBytes(UTF_8)); + output.flush(); + + response = HttpTester.parseResponse(input); + // Authorized user. + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } + } + + @Test + public void testAllPathsOnlyPUTAllowed() throws Exception + { + start(s -> + { + s.put("/*", "*", Constraint.FORBIDDEN); + s.put("/*", "PUT", Constraint.from("write")); + s.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Request.AuthenticationState state = Request.getAuthenticationState(request); + assertNotNull(state); + assertEquals("writer", state.getUserPrincipal().getName()); + callback.succeeded(); + return true; + } + }); + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("test", "password"))) + ); + + HttpTester.Response response = HttpTester.parseResponse(client); + // GET not allowed. + assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + + client.write(UTF_8.encode(""" + PUT / HTTP/1.1 + Host: localhost + Content-Length: 0 + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("reader", "password"))) + ); + + response = HttpTester.parseResponse(client); + // PUT from user with wrong role, forbidden. + assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + + client.write(UTF_8.encode(""" + PUT / HTTP/1.1 + Host: localhost + Content-Length: 0 + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("writer", "password"))) + ); + + response = HttpTester.parseResponse(client); + // PUT from user with right role, allowed. + assertEquals(HttpStatus.OK_200, response.getStatus()); + + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("writer", "password"))) + ); + + response = HttpTester.parseResponse(client); + // GET from writer user, not allowed. + assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + } + } + + @Test + public void testAllPathsGETAndPUTAllowed() throws Exception + { + start(s -> + { + s.put("/*", "*", Constraint.FORBIDDEN); + s.put("/*", "GET", Constraint.from("read")); + s.put("/*", "PUT", Constraint.from("write")); + s.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Request.AuthenticationState state = Request.getAuthenticationState(request); + assertNotNull(state); + assertEquals("admin", state.getUserPrincipal().getName()); + callback.succeeded(); + return true; + } + }); + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + client.write(UTF_8.encode(""" + GET / HTTP/1.1 + Host: localhost + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("admin", "password"))) + ); + + HttpTester.Response response = HttpTester.parseResponse(client); + // User "admin" has both read and write roles, allowed. + assertEquals(HttpStatus.OK_200, response.getStatus()); + + client.write(UTF_8.encode(""" + PUT / HTTP/1.1 + Host: localhost + Content-Length: 0 + Authorization: %s + + """.formatted(BasicAuthenticator.authorization("admin", "password"))) + ); + + response = HttpTester.parseResponse(client); + // User "admin" has both read and write roles, allowed. + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } +} diff --git a/jetty-core/jetty-security/src/test/resources/keystore.p12 b/jetty-core/jetty-security/src/test/resources/keystore.p12 new file mode 100644 index 000000000000..d96d0667bd66 Binary files /dev/null and b/jetty-core/jetty-security/src/test/resources/keystore.p12 differ diff --git a/jetty-core/jetty-security/src/test/resources/test-realm.properties b/jetty-core/jetty-security/src/test/resources/test-realm.properties new file mode 100644 index 000000000000..19f8f39cdc16 --- /dev/null +++ b/jetty-core/jetty-security/src/test/resources/test-realm.properties @@ -0,0 +1,4 @@ +test:password +reader:password,read +writer:password,write +admin:password,read,write diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java index a6e7f50aa902..d95b23f0b56c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java @@ -1236,8 +1236,7 @@ private static void logLatency(StringBuilder b, Request request, TimeUnit unit) @SuppressWarnings("unused") private static void logRequestAuthentication(StringBuilder b, Request request, Response response, boolean quoted) { - Request.AuthenticationState authenticationState = Request.getAuthenticationState(request); - Principal userPrincipal = authenticationState == null ? null : authenticationState.getUserPrincipal(); + Principal userPrincipal = Request.getAuthenticationState(request).getUserPrincipal(); append(b, userPrincipal == null ? null : userPrincipal.getName(), quoted); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 40d31eab1e67..819576117561 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -1134,7 +1134,7 @@ static AuthenticationState getAuthenticationState(Request request) { if (request.getAttribute(AuthenticationState.class.getName()) instanceof AuthenticationState authenticationState) return authenticationState; - return null; + return AuthenticationState.NONE; } /** @@ -1152,6 +1152,8 @@ static void setAuthenticationState(Request request, AuthenticationState state) */ interface AuthenticationState { + AuthenticationState NONE = new AuthenticationState() {}; + /** * @return The authenticated user {@link Principal}, or null if the Authentication is in a non-authenticated state. */ diff --git a/jetty-core/jetty-websocket/jetty-websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/CompletedUpgradeRequest.java b/jetty-core/jetty-websocket/jetty-websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/CompletedUpgradeRequest.java index 7516aa675ed6..91b6b7ad6e50 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/CompletedUpgradeRequest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/CompletedUpgradeRequest.java @@ -77,8 +77,7 @@ public CompletedUpgradeRequest(ServerUpgradeRequest request) _extensions = Collections.unmodifiableList(extensions); // Get the user principal from the request's authentication state. - Request.AuthenticationState authState = Request.getAuthenticationState(request); - _userPrincipal = authState != null ? authState.getUserPrincipal() : null; + _userPrincipal = Request.getAuthenticationState(request).getUserPrincipal(); } @Override