diff --git a/Source/JNA/pom.xml b/Source/JNA/pom.xml index 307167d901..f7707aeb4e 100644 --- a/Source/JNA/pom.xml +++ b/Source/JNA/pom.xml @@ -100,6 +100,7 @@ waffle-tomcat85 waffle-tomcat9 waffle-tomcat10 + waffle-tomcat11 diff --git a/Source/JNA/waffle-bom/pom.xml b/Source/JNA/waffle-bom/pom.xml index d936b267b8..6ef54f5eb6 100644 --- a/Source/JNA/waffle-bom/pom.xml +++ b/Source/JNA/waffle-bom/pom.xml @@ -136,6 +136,11 @@ waffle-tomcat10 ${project.version} + + com.github.waffle + waffle-tomcat11 + ${project.version} + diff --git a/Source/JNA/waffle-distro/pom.xml b/Source/JNA/waffle-distro/pom.xml index 5b9e69ad5d..6391c13ac1 100644 --- a/Source/JNA/waffle-distro/pom.xml +++ b/Source/JNA/waffle-distro/pom.xml @@ -62,6 +62,9 @@ waffle.distro + + + true @@ -157,6 +160,12 @@ ${project.version} runtime + + com.github.waffle + waffle-tomcat11 + ${project.version} + runtime + diff --git a/Source/JNA/waffle-tomcat11/format.xml b/Source/JNA/waffle-tomcat11/format.xml new file mode 100644 index 0000000000..29273afe6f --- /dev/null +++ b/Source/JNA/waffle-tomcat11/format.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/Source/JNA/waffle-tomcat11/pom.xml b/Source/JNA/waffle-tomcat11/pom.xml new file mode 100644 index 0000000000..07ad24aeca --- /dev/null +++ b/Source/JNA/waffle-tomcat11/pom.xml @@ -0,0 +1,128 @@ + + + + 4.0.0 + + + com.github.waffle + waffle + 3.3.1-SNAPSHOT + + + waffle-tomcat11 + 3.3.1-SNAPSHOT + jar + + waffle-tomcat11 + Tomcat 11 integration for WAFFLE + https://waffle.github.io/waffle/ + + + scm:git:ssh://git@github.com/waffle/waffle.git + scm:git:ssh://git@github.com/waffle/waffle.git + HEAD + https://github.com/Waffle/waffle + + + + + 11.0.0-M18 + + + 21 + 21 + + + waffle.tomcat11 + + + true + + + true + + + true + + + + + ${project.groupId} + waffle-jna-jakarta + ${project.version} + compile + + + ${project.groupId} + waffle-tests-jakarta + ${project.version} + test + + + org.apache.tomcat + tomcat-api + ${tomcat.version} + provided + + + org.apache.tomcat + tomcat-catalina + ${tomcat.version} + provided + + + org.apache.tomcat + tomcat-coyote + ${tomcat.version} + provided + + + org.apache.tomcat + tomcat-juli + ${tomcat.version} + provided + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + org.apache.tomcat + tomcat-util + ${tomcat.version} + provided + + + org.apache.tomcat + tomcat-jaspic-api + ${tomcat.version} + provided + + + diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/GenericWindowsPrincipal.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/GenericWindowsPrincipal.java new file mode 100644 index 0000000000..57e7f686ca --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/GenericWindowsPrincipal.java @@ -0,0 +1,208 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.catalina.realm.GenericPrincipal; + +import waffle.windows.auth.IWindowsAccount; +import waffle.windows.auth.IWindowsIdentity; +import waffle.windows.auth.PrincipalFormat; +import waffle.windows.auth.WindowsAccount; + +/** + * A Windows Principal. + */ +public class GenericWindowsPrincipal extends GenericPrincipal { + + /** The Constant serialVersionUID. */ + private static final long serialVersionUID = 1L; + + /** The sid. */ + private final byte[] sid; + + /** The sid string. */ + private final String sidString; + + /** The groups. */ + private final Map groups; + + /** + * A windows principal. + * + * @param windowsIdentity + * Windows identity. + * @param principalFormat + * Principal format. + * @param roleFormat + * Role format. + */ + public GenericWindowsPrincipal(final IWindowsIdentity windowsIdentity, final PrincipalFormat principalFormat, + final PrincipalFormat roleFormat) { + super(windowsIdentity.getFqn(), GenericWindowsPrincipal.getRoles(windowsIdentity, principalFormat, roleFormat)); + this.sid = windowsIdentity.getSid(); + this.sidString = windowsIdentity.getSidString(); + this.groups = GenericWindowsPrincipal.getGroups(windowsIdentity.getGroups()); + } + + /** + * Gets the roles. + * + * @param windowsIdentity + * the windows identity + * @param principalFormat + * the principal format + * @param roleFormat + * the role format + * + * @return the roles + */ + private static List getRoles(final IWindowsIdentity windowsIdentity, final PrincipalFormat principalFormat, + final PrincipalFormat roleFormat) { + final List roles = new ArrayList<>(); + roles.addAll(GenericWindowsPrincipal.getPrincipalNames(windowsIdentity, principalFormat)); + for (final IWindowsAccount group : windowsIdentity.getGroups()) { + roles.addAll(GenericWindowsPrincipal.getRoleNames(group, roleFormat)); + } + return roles; + } + + /** + * Gets the groups. + * + * @param groups + * the groups + * + * @return the groups + */ + private static Map getGroups(final IWindowsAccount[] groups) { + final Map groupMap = new HashMap<>(); + for (final IWindowsAccount group : groups) { + groupMap.put(group.getFqn(), new WindowsAccount(group)); + } + return groupMap; + } + + /** + * Windows groups that the user is a member of. + * + * @return A map of group names to groups. + */ + public Map getGroups() { + return this.groups; + } + + /** + * Byte representation of the SID. + * + * @return Array of bytes. + */ + public byte[] getSid() { + return this.sid.clone(); + } + + /** + * String representation of the SID. + * + * @return String. + */ + public String getSidString() { + return this.sidString; + } + + /** + * Returns a list of role principal objects. + * + * @param group + * Windows group. + * @param principalFormat + * Principal format. + * + * @return List of role principal objects. + */ + private static List getRoleNames(final IWindowsAccount group, final PrincipalFormat principalFormat) { + final List principals = new ArrayList<>(); + switch (principalFormat) { + case FQN: + principals.add(group.getFqn()); + break; + case SID: + principals.add(group.getSidString()); + break; + case BOTH: + principals.add(group.getFqn()); + principals.add(group.getSidString()); + break; + case NONE: + default: + break; + } + return principals; + } + + /** + * Returns a list of user principal objects. + * + * @param windowsIdentity + * Windows identity. + * @param principalFormat + * Principal format. + * + * @return A list of user principal objects. + */ + private static List getPrincipalNames(final IWindowsIdentity windowsIdentity, + final PrincipalFormat principalFormat) { + final List principals = new ArrayList<>(); + switch (principalFormat) { + case FQN: + principals.add(windowsIdentity.getFqn()); + break; + case SID: + principals.add(windowsIdentity.getSidString()); + break; + case BOTH: + principals.add(windowsIdentity.getFqn()); + principals.add(windowsIdentity.getSidString()); + break; + case NONE: + default: + break; + } + return principals; + } + + /** + * Get an array of roles as a string. + * + * @return Role1, Role2, ... + */ + public String getRolesString() { + return String.join(", ", this.getRoles()); + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/MixedAuthenticator.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/MixedAuthenticator.java new file mode 100644 index 0000000000..0e45986c00 --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/MixedAuthenticator.java @@ -0,0 +1,310 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import com.sun.jna.platform.win32.Win32Exception; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; +import java.security.Principal; +import java.util.Base64; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.slf4j.LoggerFactory; + +import waffle.util.AuthorizationHeader; +import waffle.util.NtlmServletRequest; +import waffle.windows.auth.IWindowsIdentity; +import waffle.windows.auth.IWindowsSecurityContext; + +/** + * Mixed Negotiate + Form Authenticator. + */ +public class MixedAuthenticator extends WaffleAuthenticatorBase { + + /** + * Instantiates a new mixed authenticator. + */ + public MixedAuthenticator() { + super(); + this.log = LoggerFactory.getLogger(MixedAuthenticator.class); + this.info = MixedAuthenticator.class.getSimpleName(); + this.log.debug("[waffle.apache.MixedAuthenticator] loaded"); + } + + @Override + public synchronized void startInternal() throws LifecycleException { + this.log.info("[waffle.apache.MixedAuthenticator] started"); + super.startInternal(); + } + + @Override + public synchronized void stopInternal() throws LifecycleException { + super.stopInternal(); + this.log.info("[waffle.apache.MixedAuthenticator] stopped"); + } + + @Override + public boolean authenticate(final Request request, final HttpServletResponse response) { + + // realm: fail if no realm is configured + if (this.context == null || this.context.getRealm() == null) { + this.log.warn("missing context/realm"); + this.sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE); + return false; + } + + this.log.debug("{} {}, contentlength: {}", request.getMethod(), request.getRequestURI(), + Integer.valueOf(request.getContentLength())); + + final boolean negotiateCheck = request.getParameter("j_negotiate_check") != null; + this.log.debug("negotiateCheck: {}", Boolean.valueOf(negotiateCheck)); + final boolean securityCheck = request.getParameter("j_security_check") != null; + this.log.debug("securityCheck: {}", Boolean.valueOf(securityCheck)); + + final Principal principal = request.getUserPrincipal(); + + final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request); + final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader(); + this.log.debug("authorization: {}, ntlm post: {}", authorizationHeader, Boolean.valueOf(ntlmPost)); + + final LoginConfig loginConfig = this.context.getLoginConfig(); + + if (principal != null && !ntlmPost) { + this.log.debug("previously authenticated user: {}", principal.getName()); + return true; + } else if (negotiateCheck) { + if (!authorizationHeader.isNull()) { + boolean negotiateResult = this.negotiate(request, response, authorizationHeader); + if (!negotiateResult) { + this.redirectTo(request, response, loginConfig.getErrorPage()); + } + return negotiateResult; + } + this.log.debug("authorization required"); + this.sendUnauthorized(response); + return false; + } else if (securityCheck) { + final boolean postResult = this.post(request, response); + if (!postResult) { + this.redirectTo(request, response, loginConfig.getErrorPage()); + } + return postResult; + } else { + this.redirectTo(request, response, loginConfig.getLoginPage()); + return false; + } + } + + /** + * Negotiate. + * + * @param request + * the request + * @param response + * the response + * @param authorizationHeader + * the authorization header + * + * @return true, if successful + */ + private boolean negotiate(final Request request, final HttpServletResponse response, + final AuthorizationHeader authorizationHeader) { + + final String securityPackage = authorizationHeader.getSecurityPackage(); + // maintain a connection-based session for NTLM tokens + final String connectionId = NtlmServletRequest.getConnectionId(request); + + this.log.debug("security package: {}, connection id: {}", securityPackage, connectionId); + + final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader(); + + if (ntlmPost) { + // type 1 NTLM authentication message received + this.auth.resetSecurityToken(connectionId); + } + + final byte[] tokenBuffer = authorizationHeader.getTokenBytes(); + this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length)); + + // log the user in using the token + IWindowsSecurityContext securityContext; + try { + securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage); + } catch (final Win32Exception e) { + this.log.warn("error logging in user: {}", e.getMessage()); + this.log.trace("", e); + this.sendUnauthorized(response); + return false; + } + this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue())); + + final byte[] continueTokenBytes = securityContext.getToken(); + if (continueTokenBytes != null && continueTokenBytes.length > 0) { + final String continueToken = Base64.getEncoder().encodeToString(continueTokenBytes); + this.log.debug("continue token: {}", continueToken); + response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken); + } + + try { + if (securityContext.isContinue() || ntlmPost) { + response.setHeader("Connection", "keep-alive"); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + response.flushBuffer(); + return false; + } + } catch (final IOException e) { + this.log.warn("error logging in user: {}", e.getMessage()); + this.log.trace("", e); + this.sendUnauthorized(response); + return false; + } + + // create and register the user principal with the session + final IWindowsIdentity windowsIdentity = securityContext.getIdentity(); + + // disable guest login + if (!this.allowGuestLogin && windowsIdentity.isGuest()) { + this.log.warn("guest login disabled: {}", windowsIdentity.getFqn()); + this.sendUnauthorized(response); + return false; + } + + try { + + this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString()); + + final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity); + + if (this.log.isDebugEnabled()) { + this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles())); + } + + // create a session associated with this request if there's none + final HttpSession session = request.getSession(true); + this.log.debug("session id: {}", session == null ? "null" : session.getId()); + + this.register(request, response, genericPrincipal, securityPackage, genericPrincipal.getName(), null); + this.log.info("successfully logged in user: {}", genericPrincipal.getName()); + + } finally { + windowsIdentity.dispose(); + } + + return true; + } + + /** + * Post. + * + * @param request + * the request + * @param response + * the response + * + * @return true, if successful + */ + private boolean post(final Request request, final HttpServletResponse response) { + + final String username = request.getParameter("j_username"); + final String password = request.getParameter("j_password"); + + this.log.debug("logging in: {}", username); + + IWindowsIdentity windowsIdentity; + try { + windowsIdentity = this.auth.logonUser(username, password); + } catch (final Exception e) { + this.log.error(e.getMessage()); + this.log.trace("", e); + return false; + } + + // disable guest login + if (!this.allowGuestLogin && windowsIdentity.isGuest()) { + this.log.warn("guest login disabled: {}", windowsIdentity.getFqn()); + return false; + } + + try { + this.log.debug("successfully logged in {} ({})", username, windowsIdentity.getSidString()); + + final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity); + + if (this.log.isDebugEnabled()) { + this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles())); + } + + // create a session associated with this request if there's none + final HttpSession session = request.getSession(true); + this.log.debug("session id: {}", session == null ? "null" : session.getId()); + + this.register(request, response, genericPrincipal, "FORM", genericPrincipal.getName(), null); + this.log.info("successfully logged in user: {}", genericPrincipal.getName()); + } finally { + windowsIdentity.dispose(); + } + + return true; + } + + /** + * Redirect to. + * + * @param request + * the request + * @param response + * the response + * @param url + * the url + */ + private void redirectTo(final Request request, final HttpServletResponse response, final String url) { + try { + this.log.debug("redirecting to: {}", url); + final ServletContext servletContext = this.context.getServletContext(); + final RequestDispatcher disp = servletContext.getRequestDispatcher(url); + disp.forward(request.getRequest(), response); + } catch (final IOException | ServletException e) { + throw new RuntimeException(e); + } + } + + /** + * XXX The 'doAuthenticate' is intended to replace 'authenticate' for needs like ours. In order to support old and + * new at this time, we will continue to have both for time being. + */ + @Override + protected boolean doAuthenticate(final Request request, final HttpServletResponse response) throws IOException { + return this.authenticate(request, response); + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/NegotiateAuthenticator.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/NegotiateAuthenticator.java new file mode 100644 index 0000000000..b204fcea7c --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/NegotiateAuthenticator.java @@ -0,0 +1,197 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import com.sun.jna.platform.win32.Win32Exception; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; +import java.security.Principal; +import java.util.Base64; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.realm.GenericPrincipal; +import org.slf4j.LoggerFactory; + +import waffle.util.AuthorizationHeader; +import waffle.util.NtlmServletRequest; +import waffle.windows.auth.IWindowsIdentity; +import waffle.windows.auth.IWindowsSecurityContext; + +/** + * An Apache Negotiate (NTLM, Kerberos) Authenticator. + */ +public class NegotiateAuthenticator extends WaffleAuthenticatorBase { + + /** + * Instantiates a new negotiate authenticator. + */ + public NegotiateAuthenticator() { + super(); + this.log = LoggerFactory.getLogger(NegotiateAuthenticator.class); + this.info = NegotiateAuthenticator.class.getSimpleName(); + this.log.debug("[waffle.apache.NegotiateAuthenticator] loaded"); + } + + @Override + public synchronized void startInternal() throws LifecycleException { + this.log.info("[waffle.apache.NegotiateAuthenticator] started"); + super.startInternal(); + } + + @Override + public synchronized void stopInternal() throws LifecycleException { + super.stopInternal(); + this.log.info("[waffle.apache.NegotiateAuthenticator] stopped"); + } + + @Override + public boolean authenticate(final Request request, final HttpServletResponse response) { + + Principal principal = request.getUserPrincipal(); + final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request); + final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader(); + + this.log.debug("{} {}, contentlength: {}", request.getMethod(), request.getRequestURI(), + Integer.valueOf(request.getContentLength())); + this.log.debug("authorization: {}, ntlm post: {}", authorizationHeader, Boolean.valueOf(ntlmPost)); + + if (principal != null && !ntlmPost) { + // user already authenticated + this.log.debug("previously authenticated user: {}", principal.getName()); + return true; + } + + // authenticate user + if (!authorizationHeader.isNull()) { + + final String securityPackage = authorizationHeader.getSecurityPackage(); + // maintain a connection-based session for NTLM tokens + final String connectionId = NtlmServletRequest.getConnectionId(request); + + this.log.debug("security package: {}, connection id: {}", securityPackage, connectionId); + + if (ntlmPost) { + // type 1 NTLM authentication message received + this.auth.resetSecurityToken(connectionId); + } + + final byte[] tokenBuffer = authorizationHeader.getTokenBytes(); + this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length)); + + // log the user in using the token + IWindowsSecurityContext securityContext; + try { + securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage); + } catch (final Win32Exception e) { + this.log.warn("error logging in user: {}", e.getMessage()); + this.log.trace("", e); + this.sendUnauthorized(response); + return false; + } + this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue())); + + final byte[] continueTokenBytes = securityContext.getToken(); + if (continueTokenBytes != null && continueTokenBytes.length > 0) { + final String continueToken = Base64.getEncoder().encodeToString(continueTokenBytes); + this.log.debug("continue token: {}", continueToken); + response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken); + } + + try { + if (securityContext.isContinue()) { + response.setHeader("Connection", "keep-alive"); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + response.flushBuffer(); + return false; + } + } catch (final IOException e) { + this.log.warn("error logging in user: {}", e.getMessage()); + this.log.trace("", e); + this.sendUnauthorized(response); + return false; + } + + // realm: fail if no realm is configured + if (this.context == null || this.context.getRealm() == null) { + this.log.warn("missing context/realm"); + this.sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE); + return false; + } + + // create and register the user principal with the session + final IWindowsIdentity windowsIdentity = securityContext.getIdentity(); + + // disable guest login + if (!this.allowGuestLogin && windowsIdentity.isGuest()) { + this.log.warn("guest login disabled: {}", windowsIdentity.getFqn()); + this.sendUnauthorized(response); + return false; + } + + try { + this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString()); + + final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity); + + if (this.log.isDebugEnabled()) { + this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles())); + } + + principal = genericPrincipal; + + // create a session associated with this request if there's none + final HttpSession session = request.getSession(true); + this.log.debug("session id: {}", session == null ? "null" : session.getId()); + + // register the authenticated principal + this.register(request, response, principal, securityPackage, principal.getName(), null); + this.log.info("successfully logged in user: {}", principal.getName()); + + } finally { + windowsIdentity.dispose(); + securityContext.dispose(); + } + + return true; + } + + this.log.debug("authorization required"); + this.sendUnauthorized(response); + return false; + } + + /** + * XXX The 'doAuthenticate' is intended to replace 'authenticate' for needs like ours. In order to support old and + * new at this time, we will continue to have both for time being. + */ + @Override + protected boolean doAuthenticate(final Request request, final HttpServletResponse response) throws IOException { + return this.authenticate(request, response); + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WaffleAuthenticatorBase.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WaffleAuthenticatorBase.java new file mode 100644 index 0000000000..4a2db5e88f --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WaffleAuthenticatorBase.java @@ -0,0 +1,305 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.security.Principal; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.authenticator.AuthenticatorBase; +import org.apache.catalina.connector.Request; +import org.apache.catalina.realm.GenericPrincipal; +import org.slf4j.Logger; + +import waffle.windows.auth.IWindowsAuthProvider; +import waffle.windows.auth.IWindowsIdentity; +import waffle.windows.auth.PrincipalFormat; +import waffle.windows.auth.impl.WindowsAuthProviderImpl; + +/** + * The Class WaffleAuthenticatorBase. + */ +abstract class WaffleAuthenticatorBase extends AuthenticatorBase { + + /** The Constant SUPPORTED_PROTOCOLS. */ + private static final Set SUPPORTED_PROTOCOLS = new LinkedHashSet<>(Arrays.asList("Negotiate", "NTLM")); + + /** The info. */ + protected String info; + + /** The log. */ + protected Logger log; + + /** The principal format. */ + protected PrincipalFormat principalFormat = PrincipalFormat.FQN; + + /** The role format. */ + protected PrincipalFormat roleFormat = PrincipalFormat.FQN; + + /** The allow guest login. */ + protected boolean allowGuestLogin = true; + + /** The protocols. */ + protected Set protocols = WaffleAuthenticatorBase.SUPPORTED_PROTOCOLS; + + /** The auth continueContextsTimeout configuration. */ + protected int continueContextsTimeout = WindowsAuthProviderImpl.CONTINUE_CONTEXT_TIMEOUT; + + /** The auth. */ + protected IWindowsAuthProvider auth; + + /** + * Gets the continue context time out configuration. + * + * @return the continue contexts timeout + */ + public int getContinueContextsTimeout() { + return this.continueContextsTimeout; + } + + /** + * Sets the continue context time out configuration. + * + * @param continueContextsTimeout + * the new continue contexts timeout + */ + public void setContinueContextsTimeout(final int continueContextsTimeout) { + this.continueContextsTimeout = continueContextsTimeout; + } + + /** + * Windows authentication provider. + * + * @return IWindowsAuthProvider. + */ + public IWindowsAuthProvider getAuth() { + return this.auth; + } + + /** + * Set Windows auth provider. + * + * @param provider + * Class implements IWindowsAuthProvider. + */ + public void setAuth(final IWindowsAuthProvider provider) { + this.auth = provider; + } + + /** + * Gets the info. + * + * @return the info + */ + public String getInfo() { + return this.info; + } + + /** + * Set the principal format. + * + * @param format + * Principal format. + */ + public void setPrincipalFormat(final String format) { + this.principalFormat = PrincipalFormat.valueOf(format.toUpperCase(Locale.ENGLISH)); + this.log.debug("principal format: {}", this.principalFormat); + } + + /** + * Principal format. + * + * @return Principal format. + */ + public PrincipalFormat getPrincipalFormat() { + return this.principalFormat; + } + + /** + * Set the principal format. + * + * @param format + * Role format. + */ + public void setRoleFormat(final String format) { + this.roleFormat = PrincipalFormat.valueOf(format.toUpperCase(Locale.ENGLISH)); + this.log.debug("role format: {}", this.roleFormat); + } + + /** + * Principal format. + * + * @return Role format. + */ + public PrincipalFormat getRoleFormat() { + return this.roleFormat; + } + + /** + * True if Guest login permitted. + * + * @return True if Guest login permitted, false otherwise. + */ + public boolean isAllowGuestLogin() { + return this.allowGuestLogin; + } + + /** + * Set whether Guest login is permitted. Default is true, if the Guest account is enabled, an invalid + * username/password results in a Guest login. + * + * @param value + * True or false. + */ + public void setAllowGuestLogin(final boolean value) { + this.allowGuestLogin = value; + } + + /** + * Set the authentication protocols. Default is "Negotiate, NTLM". + * + * @param value + * Authentication protocols + */ + public void setProtocols(final String value) { + this.protocols = new LinkedHashSet<>(); + final String[] protocolNames = value.split(",", -1); + for (String protocolName : protocolNames) { + protocolName = protocolName.trim(); + if (!protocolName.isEmpty()) { + this.log.debug("init protocol: {}", protocolName); + if (WaffleAuthenticatorBase.SUPPORTED_PROTOCOLS.contains(protocolName)) { + this.protocols.add(protocolName); + } else { + this.log.error("unsupported protocol: {}", protocolName); + throw new RuntimeException("Unsupported protocol: " + protocolName); + } + } + } + } + + /** + * Send a 401 Unauthorized along with protocol authentication headers. + * + * @param response + * HTTP Response + */ + protected void sendUnauthorized(final HttpServletResponse response) { + try { + for (final String protocol : this.protocols) { + response.addHeader("WWW-Authenticate", protocol); + } + response.setHeader("Connection", "close"); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + response.flushBuffer(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Send an error code. + * + * @param response + * HTTP Response + * @param code + * Error Code + */ + protected void sendError(final HttpServletResponse response, final int code) { + try { + response.sendError(code); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected String getAuthMethod() { + return null; + } + + @Override + protected Principal doLogin(final Request request, final String username, final String password) + throws ServletException { + this.log.debug("logging in: {}", username); + IWindowsIdentity windowsIdentity; + try { + windowsIdentity = this.auth.logonUser(username, password); + } catch (final Exception e) { + this.log.error(e.getMessage()); + this.log.trace("", e); + return super.doLogin(request, username, password); + } + // disable guest login + if (!this.allowGuestLogin && windowsIdentity.isGuest()) { + this.log.warn("guest login disabled: {}", windowsIdentity.getFqn()); + return super.doLogin(request, username, password); + } + try { + this.log.debug("successfully logged in {} ({})", username, windowsIdentity.getSidString()); + final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity); + if (this.log.isDebugEnabled()) { + this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles())); + } + return genericPrincipal; + } finally { + windowsIdentity.dispose(); + } + } + + /** + * This method will create an instance of a IWindowsIdentity based GenericPrincipal. It is used for creating custom + * implementation within subclasses. + * + * @param windowsIdentity + * the windows identity to initialize the principal + * + * @return the Generic Principal + */ + protected GenericPrincipal createPrincipal(final IWindowsIdentity windowsIdentity) { + return new GenericWindowsPrincipal(windowsIdentity, this.principalFormat, this.roleFormat); + } + + /** + * Hook to the start and to set up the dependencies. + * + * @throws LifecycleException + * the lifecycle exception + */ + @Override + public synchronized void startInternal() throws LifecycleException { + this.log.debug("Creating a windows authentication provider with continueContextsTimeout property set to: {}", + this.continueContextsTimeout); + this.auth = new WindowsAuthProviderImpl(this.continueContextsTimeout); + super.startInternal(); + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WindowsRealm.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WindowsRealm.java new file mode 100644 index 0000000000..159f167089 --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WindowsRealm.java @@ -0,0 +1,45 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import java.security.Principal; + +import org.apache.catalina.realm.RealmBase; + +/** + * A rudimentary Windows realm. + */ +public class WindowsRealm extends RealmBase { + + @Override + protected String getPassword(final String value) { + return null; + } + + @Override + protected Principal getPrincipal(final String value) { + return null; + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/package-info.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/package-info.java new file mode 100644 index 0000000000..eb148d4f03 --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/package-info.java @@ -0,0 +1,27 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +/** + * Waffle Tomcat Package. + */ +package waffle.apache; diff --git a/Source/JNA/waffle-tomcat11/src/site/resources/images/waffle.jpg b/Source/JNA/waffle-tomcat11/src/site/resources/images/waffle.jpg new file mode 100644 index 0000000000..00455a8db4 Binary files /dev/null and b/Source/JNA/waffle-tomcat11/src/site/resources/images/waffle.jpg differ diff --git a/Source/JNA/waffle-tomcat11/src/site/site.xml b/Source/JNA/waffle-tomcat11/src/site/site.xml new file mode 100644 index 0000000000..25dd2499bf --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/site/site.xml @@ -0,0 +1,54 @@ + + + + + /images/waffle.jpg + https://github.com/Waffle/waffle + + + /images/waffle.jpg + https://github.com/Waffle/waffle + + + org.apache.maven.skins + maven-fluido-skin + 1.11.2 + + + + true + true + + + + + + + + + diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/MixedAuthenticatorTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/MixedAuthenticatorTest.java new file mode 100644 index 0000000000..22d11ffc3c --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/MixedAuthenticatorTest.java @@ -0,0 +1,400 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sun.jna.platform.win32.Sspi; +import com.sun.jna.platform.win32.SspiUtil.ManagedSecBufferDesc; + +import jakarta.servlet.ServletException; + +import java.util.Base64; +import java.util.Collections; + +import mockit.Expectations; +import mockit.Mocked; + +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.coyote.Response; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import waffle.apache.catalina.SimpleHttpRequest; +import waffle.apache.catalina.SimpleHttpResponse; +import waffle.mock.MockWindowsAuthProvider; +import waffle.windows.auth.IWindowsCredentialsHandle; +import waffle.windows.auth.IWindowsIdentity; +import waffle.windows.auth.PrincipalFormat; +import waffle.windows.auth.impl.WindowsAccountImpl; +import waffle.windows.auth.impl.WindowsCredentialsHandleImpl; +import waffle.windows.auth.impl.WindowsSecurityContextImpl; + +/** + * Waffle Tomcat Mixed Authenticator Test. + */ +class MixedAuthenticatorTest { + + /** The authenticator. */ + MixedAuthenticator authenticator; + + /** The context. */ + @Mocked + Context context; + + /** The engine. */ + @Mocked + Engine engine; + + /** + * Sets the up. + * + * @throws LifecycleException + * the lifecycle exception + */ + @BeforeEach + void setUp() throws LifecycleException { + this.authenticator = new MixedAuthenticator(); + this.authenticator.setContainer(this.context); + Assertions.assertNotNull(new Expectations() { + { + MixedAuthenticatorTest.this.context.getParent(); + this.result = MixedAuthenticatorTest.this.engine; + MixedAuthenticatorTest.this.context.getParent(); + this.result = null; + } + }); + this.authenticator.start(); + } + + /** + * Tear down. + * + * @throws LifecycleException + * the lifecycle exception + */ + @AfterEach + void tearDown() throws LifecycleException { + this.authenticator.stop(); + } + + /** + * Test challenge get. + */ + @Test + void testChallengeGET() { + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.setMethod("GET"); + request.setQueryString("j_negotiate_check"); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + this.authenticator.authenticate(request, response); + final String[] wwwAuthenticates = response.getHeaderValues("WWW-Authenticate"); + Assertions.assertNotNull(wwwAuthenticates); + Assertions.assertEquals(2, wwwAuthenticates.length); + Assertions.assertEquals("Negotiate", wwwAuthenticates[0]); + Assertions.assertEquals("NTLM", wwwAuthenticates[1]); + Assertions.assertEquals("close", response.getHeader("Connection")); + Assertions.assertEquals(2, response.getHeaderNames().size()); + Assertions.assertEquals(401, response.getStatus()); + } + + /** + * Test challenge post. + */ + @Test + void testChallengePOST() { + final String securityPackage = "Negotiate"; + IWindowsCredentialsHandle clientCredentials = null; + WindowsSecurityContextImpl clientContext = null; + try { + // client credentials handle + clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage); + clientCredentials.initialize(); + // initial client security context + clientContext = new WindowsSecurityContextImpl(); + clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername()); + clientContext.setCredentialsHandle(clientCredentials); + clientContext.setSecurityPackage(securityPackage); + clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername()); + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.setQueryString("j_negotiate_check"); + request.setMethod("POST"); + request.setContentLength(0); + final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken()); + request.addHeader("Authorization", securityPackage + " " + clientToken); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + this.authenticator.authenticate(request, response); + Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " ")); + Assertions.assertEquals("keep-alive", response.getHeader("Connection")); + Assertions.assertEquals(2, response.getHeaderNames().size()); + Assertions.assertEquals(401, response.getStatus()); + } finally { + if (clientContext != null) { + clientContext.dispose(); + } + if (clientCredentials != null) { + clientCredentials.dispose(); + } + } + } + + /** + * Test get. + */ + @Test + void testGet() { + final SimpleHttpRequest request = new SimpleHttpRequest(); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + Assertions.assertFalse(this.authenticator.authenticate(request, response)); + } + + /** + * Test get info. + */ + @Test + void testGetInfo() { + assertThat(this.authenticator.getInfo()).isNotEmpty(); + } + + /** + * Test negotiate. + */ + @Test + void testNegotiate() { + final String securityPackage = "Negotiate"; + IWindowsCredentialsHandle clientCredentials = null; + WindowsSecurityContextImpl clientContext = null; + try { + // client credentials handle + clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage); + clientCredentials.initialize(); + // initial client security context + clientContext = new WindowsSecurityContextImpl(); + clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername()); + clientContext.setCredentialsHandle(clientCredentials); + clientContext.setSecurityPackage(securityPackage); + clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername()); + // negotiate + boolean authenticated = false; + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.setQueryString("j_negotiate_check"); + String clientToken; + while (true) { + clientToken = Base64.getEncoder().encodeToString(clientContext.getToken()); + request.addHeader("Authorization", securityPackage + " " + clientToken); + + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + authenticated = this.authenticator.authenticate(request, response); + + if (authenticated) { + assertThat(response.getHeaderNames().size()).isNotNegative(); + break; + } + + Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " ")); + Assertions.assertEquals("keep-alive", response.getHeader("Connection")); + Assertions.assertEquals(2, response.getHeaderNames().size()); + Assertions.assertEquals(401, response.getStatus()); + final String continueToken = response.getHeader("WWW-Authenticate") + .substring(securityPackage.length() + 1); + final byte[] continueTokenBytes = Base64.getDecoder().decode(continueToken); + assertThat(continueTokenBytes).isNotEmpty(); + final ManagedSecBufferDesc continueTokenBuffer = new ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN, + continueTokenBytes); + clientContext.initialize(clientContext.getHandle(), continueTokenBuffer, + WindowsAccountImpl.getCurrentUsername()); + } + Assertions.assertTrue(authenticated); + } finally { + if (clientContext != null) { + clientContext.dispose(); + } + if (clientCredentials != null) { + clientCredentials.dispose(); + } + } + } + + /** + * Test post security check. + */ + @Test + void testPostSecurityCheck() { + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.setQueryString("j_security_check"); + request.addParameter("j_username", "username"); + request.addParameter("j_password", "password"); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + Assertions.assertFalse(this.authenticator.authenticate(request, response)); + } + + /** + * Test programmatic security BOTH. + * + * @param identity + * the identity + * + * @throws ServletException + * the servlet exception + */ + @Test + void testProgrammaticSecurityBoth(@Mocked final IWindowsIdentity identity) throws ServletException { + this.authenticator.setAuth(new MockWindowsAuthProvider()); + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.getMappingData().context = (Context) this.authenticator.getContainer(); + + request.login(WindowsAccountImpl.getCurrentUsername(), ""); + + Assertions.assertNotNull(new Expectations() { + { + identity.getFqn(); + this.result = "fqn"; + identity.getSidString(); + this.result = "S-1234"; + } + }); + request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.BOTH, PrincipalFormat.BOTH)); + + Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal); + final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal(); + Assertions.assertTrue(windowsPrincipal.getSidString().startsWith("S-")); + } + + /** + * Test programmatic security SID. + * + * @param identity + * the identity + * + * @throws ServletException + * the servlet exception + */ + @Test + void testProgrammaticSecuritySID(@Mocked final IWindowsIdentity identity) throws ServletException { + this.authenticator.setAuth(new MockWindowsAuthProvider()); + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.getMappingData().context = (Context) this.authenticator.getContainer(); + + request.login(WindowsAccountImpl.getCurrentUsername(), ""); + + Assertions.assertNotNull(new Expectations() { + { + identity.getSidString(); + this.result = "S-1234"; + } + }); + request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.SID, PrincipalFormat.SID)); + + Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal); + final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal(); + Assertions.assertTrue(windowsPrincipal.getSidString().startsWith("S-")); + } + + /** + * Test programmatic security NONE. + * + * @param identity + * the identity + * + * @throws ServletException + * the servlet exception + */ + @Test + void testProgrammaticSecurityNone(@Mocked final IWindowsIdentity identity) throws ServletException { + this.authenticator.setAuth(new MockWindowsAuthProvider()); + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.getMappingData().context = (Context) this.authenticator.getContainer(); + + request.login(WindowsAccountImpl.getCurrentUsername(), ""); + + request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.NONE, PrincipalFormat.NONE)); + + Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal); + final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal(); + Assertions.assertNull(windowsPrincipal.getSidString()); + } + + /** + * Test security check parameters. + */ + @Test + void testSecurityCheckParameters() { + this.authenticator.setAuth(new MockWindowsAuthProvider()); + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.addParameter("j_security_check", ""); + request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername()); + request.addParameter("j_password", ""); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + Assertions.assertTrue(this.authenticator.authenticate(request, response)); + } + + /** + * Test security check query string. + */ + @Test + void testSecurityCheckQueryString() { + this.authenticator.setAuth(new MockWindowsAuthProvider()); + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.setQueryString("j_security_check"); + request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername()); + request.addParameter("j_password", ""); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + Assertions.assertTrue(this.authenticator.authenticate(request, response)); + } + + @Test + void testCustomPrincipal() throws LifecycleException { + final GenericPrincipal genericPrincipal = new GenericPrincipal("my-principal", Collections.emptyList()); + final MixedAuthenticator customAuthenticator = new MixedAuthenticator() { + @Override + protected GenericPrincipal createPrincipal(final IWindowsIdentity windowsIdentity) { + return genericPrincipal; + } + }; + try { + customAuthenticator.setContainer(this.context); + customAuthenticator.setAlwaysUseSession(true); + customAuthenticator.start(); + + customAuthenticator.setAuth(new MockWindowsAuthProvider()); + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.addParameter("j_security_check", ""); + request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername()); + request.addParameter("j_password", ""); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + Assertions.assertTrue(customAuthenticator.authenticate(request, response)); + + Assertions.assertEquals(genericPrincipal, request.getUserPrincipal()); + } finally { + customAuthenticator.stop(); + } + + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/NegotiateAuthenticatorTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/NegotiateAuthenticatorTest.java new file mode 100644 index 0000000000..427584c0cf --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/NegotiateAuthenticatorTest.java @@ -0,0 +1,330 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sun.jna.platform.win32.Sspi; +import com.sun.jna.platform.win32.SspiUtil.ManagedSecBufferDesc; + +import java.util.Base64; + +import mockit.Expectations; +import mockit.Mocked; + +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.LifecycleException; +import org.apache.coyote.Response; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import waffle.apache.catalina.SimpleHttpRequest; +import waffle.apache.catalina.SimpleHttpResponse; +import waffle.windows.auth.IWindowsCredentialsHandle; +import waffle.windows.auth.PrincipalFormat; +import waffle.windows.auth.impl.WindowsAccountImpl; +import waffle.windows.auth.impl.WindowsAuthProviderImpl; +import waffle.windows.auth.impl.WindowsCredentialsHandleImpl; +import waffle.windows.auth.impl.WindowsSecurityContextImpl; + +/** + * Waffle Tomcat Authenticator Test. + */ +class NegotiateAuthenticatorTest { + + /** The authenticator. */ + private NegotiateAuthenticator authenticator; + + @Mocked + Context context; + + @Mocked + Engine engine; + + /** + * Sets the up. + * + * @throws LifecycleException + * the lifecycle exception + */ + @BeforeEach + void setUp() throws LifecycleException { + this.authenticator = new NegotiateAuthenticator(); + this.authenticator.setContainer(this.context); + Assertions.assertNotNull(new Expectations() { + { + NegotiateAuthenticatorTest.this.context.getParent(); + this.result = NegotiateAuthenticatorTest.this.engine; + NegotiateAuthenticatorTest.this.context.getParent(); + this.result = null; + } + }); + this.authenticator.start(); + } + + /** + * Tear down. + * + * @throws LifecycleException + * the lifecycle exception + */ + @AfterEach + void tearDown() throws LifecycleException { + this.authenticator.stop(); + } + + /** + * Test allow guest login. + */ + @Test + void testAllowGuestLogin() { + Assertions.assertTrue(this.authenticator.isAllowGuestLogin()); + this.authenticator.setAllowGuestLogin(false); + Assertions.assertFalse(this.authenticator.isAllowGuestLogin()); + } + + /** + * Test challenge get. + */ + @Test + void testChallengeGET() { + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.setMethod("GET"); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + this.authenticator.authenticate(request, response); + final String[] wwwAuthenticates = response.getHeaderValues("WWW-Authenticate"); + Assertions.assertNotNull(wwwAuthenticates); + Assertions.assertEquals(2, wwwAuthenticates.length); + Assertions.assertEquals("Negotiate", wwwAuthenticates[0]); + Assertions.assertEquals("NTLM", wwwAuthenticates[1]); + Assertions.assertEquals("close", response.getHeader("Connection")); + Assertions.assertEquals(2, response.getHeaderNames().size()); + Assertions.assertEquals(401, response.getStatus()); + } + + /** + * Test challenge post. + */ + @Test + void testChallengePOST() { + final String securityPackage = "Negotiate"; + IWindowsCredentialsHandle clientCredentials = null; + WindowsSecurityContextImpl clientContext = null; + try { + // client credentials handle + clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage); + clientCredentials.initialize(); + // initial client security context + clientContext = new WindowsSecurityContextImpl(); + clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername()); + clientContext.setCredentialsHandle(clientCredentials); + clientContext.setSecurityPackage(securityPackage); + clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername()); + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.setMethod("POST"); + request.setContentLength(0); + final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken()); + request.addHeader("Authorization", securityPackage + " " + clientToken); + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + this.authenticator.authenticate(request, response); + Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " ")); + Assertions.assertEquals("keep-alive", response.getHeader("Connection")); + Assertions.assertEquals(2, response.getHeaderNames().size()); + Assertions.assertEquals(401, response.getStatus()); + } finally { + if (clientContext != null) { + clientContext.dispose(); + } + if (clientCredentials != null) { + clientCredentials.dispose(); + } + } + } + + /** + * Test get info. + */ + @Test + void testGetInfo() { + assertThat(this.authenticator.getInfo()).isNotEmpty(); + Assertions.assertTrue(this.authenticator.getAuth() instanceof WindowsAuthProviderImpl); + } + + /** + * Test negotiate. + */ + @Test + void testNegotiate() { + final String securityPackage = "Negotiate"; + IWindowsCredentialsHandle clientCredentials = null; + WindowsSecurityContextImpl clientContext = null; + try { + // client credentials handle + clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage); + clientCredentials.initialize(); + // initial client security context + clientContext = new WindowsSecurityContextImpl(); + clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername()); + clientContext.setCredentialsHandle(clientCredentials); + clientContext.setSecurityPackage(securityPackage); + clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername()); + // negotiate + boolean authenticated = false; + final SimpleHttpRequest request = new SimpleHttpRequest(); + while (true) { + final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken()); + request.addHeader("Authorization", securityPackage + " " + clientToken); + + final SimpleHttpResponse response = new SimpleHttpResponse(new Response()); + authenticated = this.authenticator.authenticate(request, response); + + if (authenticated) { + Assertions.assertNotNull(request.getUserPrincipal()); + Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal); + final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request + .getUserPrincipal(); + Assertions.assertTrue(windowsPrincipal.getSidString().startsWith("S-")); + assertThat(windowsPrincipal.getSid()).isNotEmpty(); + Assertions.assertTrue(windowsPrincipal.getGroups().containsKey("Everyone")); + assertThat(response.getHeaderNames()).hasSizeLessThanOrEqualTo(1); + break; + } + + Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " ")); + Assertions.assertEquals("keep-alive", response.getHeader("Connection")); + Assertions.assertEquals(2, response.getHeaderNames().size()); + Assertions.assertEquals(401, response.getStatus()); + final String continueToken = response.getHeader("WWW-Authenticate") + .substring(securityPackage.length() + 1); + final byte[] continueTokenBytes = Base64.getDecoder().decode(continueToken); + assertThat(continueTokenBytes).isNotEmpty(); + final ManagedSecBufferDesc continueTokenBuffer = new ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN, + continueTokenBytes); + clientContext.initialize(clientContext.getHandle(), continueTokenBuffer, + WindowsAccountImpl.getCurrentUsername()); + } + Assertions.assertTrue(authenticated); + } finally { + if (clientContext != null) { + clientContext.dispose(); + } + if (clientCredentials != null) { + clientCredentials.dispose(); + } + } + } + + /** + * Test post empty. + */ + @Test + void testPOSTEmpty() { + final String securityPackage = "Negotiate"; + IWindowsCredentialsHandle clientCredentials = null; + WindowsSecurityContextImpl clientContext = null; + try { + // client credentials handle + clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage); + clientCredentials.initialize(); + // initial client security context + clientContext = new WindowsSecurityContextImpl(); + clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername()); + clientContext.setCredentialsHandle(clientCredentials); + clientContext.setSecurityPackage(securityPackage); + clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername()); + // negotiate + boolean authenticated = false; + final SimpleHttpRequest request = new SimpleHttpRequest(); + request.setMethod("POST"); + request.setContentLength(0); + String clientToken; + String continueToken; + byte[] continueTokenBytes; + SimpleHttpResponse response; + ManagedSecBufferDesc continueTokenBuffer; + while (true) { + clientToken = Base64.getEncoder().encodeToString(clientContext.getToken()); + request.addHeader("Authorization", securityPackage + " " + clientToken); + + response = new SimpleHttpResponse(new Response()); + authenticated = this.authenticator.authenticate(request, response); + + if (authenticated) { + assertThat(response.getHeaderNames().size()).isNotNegative(); + break; + } + + if (response.getHeader("WWW-Authenticate").startsWith(securityPackage + ",")) { + Assertions.assertEquals("close", response.getHeader("Connection")); + Assertions.assertEquals(2, response.getHeaderNames().size()); + Assertions.assertEquals(401, response.getStatus()); + return; + } + + Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " ")); + Assertions.assertEquals("keep-alive", response.getHeader("Connection")); + Assertions.assertEquals(2, response.getHeaderNames().size()); + Assertions.assertEquals(401, response.getStatus()); + continueToken = response.getHeader("WWW-Authenticate").substring(securityPackage.length() + 1); + continueTokenBytes = Base64.getDecoder().decode(continueToken); + assertThat(continueTokenBytes).isNotEmpty(); + continueTokenBuffer = new ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN, continueTokenBytes); + clientContext.initialize(clientContext.getHandle(), continueTokenBuffer, + WindowsAccountImpl.getCurrentUsername()); + } + Assertions.assertTrue(authenticated); + } finally { + if (clientContext != null) { + clientContext.dispose(); + } + if (clientCredentials != null) { + clientCredentials.dispose(); + } + } + } + + /** + * Test principal format. + */ + @Test + void testPrincipalFormat() { + Assertions.assertEquals(PrincipalFormat.FQN, this.authenticator.getPrincipalFormat()); + this.authenticator.setPrincipalFormat("both"); + Assertions.assertEquals(PrincipalFormat.BOTH, this.authenticator.getPrincipalFormat()); + } + + /** + * Test role format. + */ + @Test + void testRoleFormat() { + Assertions.assertEquals(PrincipalFormat.FQN, this.authenticator.getRoleFormat()); + this.authenticator.setRoleFormat("both"); + Assertions.assertEquals(PrincipalFormat.BOTH, this.authenticator.getRoleFormat()); + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WaffleAuthenticatorBaseTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WaffleAuthenticatorBaseTest.java new file mode 100644 index 0000000000..46ba0c7e61 --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WaffleAuthenticatorBaseTest.java @@ -0,0 +1,111 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +import org.apache.catalina.connector.Request; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +/** + * Waffle Authenticator Base Test. + */ +class WaffleAuthenticatorBaseTest { + + /** The waffle authenticator base. */ + private WaffleAuthenticatorBase waffleAuthenticatorBase; + + /** + * Inits the. + */ + @BeforeEach + void init() { + this.waffleAuthenticatorBase = new WaffleAuthenticatorBase() { + { + this.log = LoggerFactory.getLogger(WaffleAuthenticatorBaseTest.class); + } + + @Override + public boolean authenticate(final Request request, final HttpServletResponse response) throws IOException { + return false; + } + + @Override + protected boolean doAuthenticate(final Request request, final HttpServletResponse response) + throws IOException { + return false; + } + }; + } + + /** + * Should_accept_both_protocols. + */ + @Test + void should_accept_both_protocols() { + this.waffleAuthenticatorBase.setProtocols(" NTLM , , Negotiate "); + + Assertions.assertEquals(2, this.waffleAuthenticatorBase.protocols.size(), "Two protocols added"); + Assertions.assertTrue(this.waffleAuthenticatorBase.protocols.contains("NTLM"), "NTLM has been added"); + Assertions.assertTrue(this.waffleAuthenticatorBase.protocols.contains("Negotiate"), "Negotiate has been added"); + } + + /** + * Should_accept_ negotiate_protocol. + */ + @Test + void should_accept_Negotiate_protocol() { + this.waffleAuthenticatorBase.setProtocols(" Negotiate "); + + Assertions.assertEquals(1, this.waffleAuthenticatorBase.protocols.size(), "One protocol added"); + Assertions.assertEquals("Negotiate", this.waffleAuthenticatorBase.protocols.iterator().next()); + } + + /** + * Should_accept_ ntl m_protocol. + */ + @Test + void should_accept_NTLM_protocol() { + this.waffleAuthenticatorBase.setProtocols(" NTLM "); + + Assertions.assertEquals(1, this.waffleAuthenticatorBase.protocols.size(), "One protocol added"); + Assertions.assertEquals("NTLM", this.waffleAuthenticatorBase.protocols.iterator().next()); + } + + /** + * Should_refuse_other_protocol. + */ + @Test + void should_refuse_other_protocol() { + Assertions.assertThrows(RuntimeException.class, () -> { + this.waffleAuthenticatorBase.setProtocols(" NTLM , OTHER, Negotiate "); + }); + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsAccountTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsAccountTest.java new file mode 100644 index 0000000000..fb696f12d2 --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsAccountTest.java @@ -0,0 +1,110 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import waffle.mock.MockWindowsAccount; +import waffle.windows.auth.WindowsAccount; + +/** + * Windows Account Test. + */ +class WindowsAccountTest { + + /** The mock windows account. */ + private final MockWindowsAccount mockWindowsAccount = new MockWindowsAccount("localhost\\Administrator"); + + /** The windows account. */ + private WindowsAccount windowsAccount; + + /** + * Sets the up. + */ + @BeforeEach + void setUp() { + this.windowsAccount = new WindowsAccount(this.mockWindowsAccount); + } + + /** + * Test equals. + */ + @Test + void testEquals() { + Assertions.assertEquals(this.windowsAccount, new WindowsAccount(this.mockWindowsAccount)); + final MockWindowsAccount mockWindowsAccount2 = new MockWindowsAccount("localhost\\Administrator2"); + Assertions.assertNotEquals(this.windowsAccount, new WindowsAccount(mockWindowsAccount2)); + } + + /** + * Test is serializable. + * + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws ClassNotFoundException + * the class not found exception + */ + @Test + void testIsSerializable() throws IOException, ClassNotFoundException { + // serialize + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(out)) { + oos.writeObject(this.windowsAccount); + } + assertThat(out.toByteArray()).isNotEmpty(); + // deserialize + final InputStream in = new ByteArrayInputStream(out.toByteArray()); + final ObjectInputStream ois = new ObjectInputStream(in); + final WindowsAccount copy = (WindowsAccount) ois.readObject(); + // test + Assertions.assertEquals(this.windowsAccount, copy); + Assertions.assertEquals(this.windowsAccount.getDomain(), copy.getDomain()); + Assertions.assertEquals(this.windowsAccount.getFqn(), copy.getFqn()); + Assertions.assertEquals(this.windowsAccount.getName(), copy.getName()); + Assertions.assertEquals(this.windowsAccount.getSidString(), copy.getSidString()); + } + + /** + * Test properties. + */ + @Test + void testProperties() { + Assertions.assertEquals("localhost", this.windowsAccount.getDomain()); + Assertions.assertEquals("localhost\\Administrator", this.windowsAccount.getFqn()); + Assertions.assertEquals("Administrator", this.windowsAccount.getName()); + Assertions.assertTrue(this.windowsAccount.getSidString().startsWith("S-")); + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsRealmTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsRealmTest.java new file mode 100644 index 0000000000..2e2973c65c --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsRealmTest.java @@ -0,0 +1,45 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Windows Realm Test. + */ +class WindowsRealmTest { + + /** + * Test properties. + */ + @Test + void testProperties() { + final WindowsRealm realm = new WindowsRealm(); + Assertions.assertNull(realm.getPassword(null)); + Assertions.assertNull(realm.getPrincipal(null)); + Assertions.assertEquals("WindowsRealm", realm.getClass().getSimpleName()); + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpRequest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpRequest.java new file mode 100644 index 0000000000..b24a731fed --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpRequest.java @@ -0,0 +1,258 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache.catalina; + +import jakarta.servlet.http.HttpSession; + +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; + +import mockit.Mocked; + +import org.apache.catalina.connector.Request; + +/** + * Simple HTTP Request. + */ +public class SimpleHttpRequest extends Request { + + /** The remote port s. */ + private static int remotePortS; + + /** + * Next remote port. + * + * @return the int + */ + public synchronized static int nextRemotePort() { + return ++SimpleHttpRequest.remotePortS; + } + + /** + * Reset remote port. + */ + public synchronized static void resetRemotePort() { + SimpleHttpRequest.remotePortS = 0; + } + + /** The request uri. */ + private String requestURI; + + /** The query string. */ + private String queryString; + + /** The remote user. */ + private String remoteUser; + + /** The method. */ + private String method = "GET"; + + /** The headers. */ + private final Map headers = new HashMap<>(); + + /** The parameters. */ + private final Map parameters = new HashMap<>(); + + /** The content. */ + private byte[] content; + + /** The http session. */ + @Mocked + private HttpSession httpSession; + + /** The principal. */ + private Principal principal; + + /** + * Instantiates a new simple http request. + */ + public SimpleHttpRequest() { + // Tomcat notes that null on connector here may be ok for testing + super(null, null); + this.remotePort = SimpleHttpRequest.nextRemotePort(); + } + + /** + * Adds the header. + * + * @param headerName + * the header name + * @param headerValue + * the header value + */ + public void addHeader(final String headerName, final String headerValue) { + this.headers.put(headerName, headerValue); + } + + /** + * Adds the parameter. + * + * @param parameterName + * the parameter name + * @param parameterValue + * the parameter value + */ + public void addParameter(final String parameterName, final String parameterValue) { + this.parameters.put(parameterName, parameterValue); + } + + @Override + public int getContentLength() { + return this.content == null ? -1 : this.content.length; + } + + @Override + public String getHeader(final String headerName) { + return this.headers.get(headerName); + } + + @Override + public String getMethod() { + return this.method; + } + + @Override + public String getParameter(final String parameterName) { + return this.parameters.get(parameterName); + } + + @Override + public String getQueryString() { + return this.queryString; + } + + @Override + public String getRemoteAddr() { + return this.remoteAddr; + } + + @Override + public String getRemoteHost() { + return this.remoteHost; + } + + @Override + public int getRemotePort() { + return this.remotePort; + } + + @Override + public String getRemoteUser() { + return this.remoteUser; + } + + @Override + public String getRequestURI() { + return this.requestURI; + } + + @Override + public HttpSession getSession() { + return this.httpSession; + } + + @Override + public HttpSession getSession(final boolean create) { + return this.httpSession; + } + + @Override + public Principal getUserPrincipal() { + return this.principal; + } + + /** + * Sets the content length. + * + * @param length + * the new content length + */ + public void setContentLength(final int length) { + this.content = new byte[length]; + } + + /** + * Sets the method. + * + * @param value + * the new method + */ + public void setMethod(final String value) { + this.method = value; + } + + /** + * Sets the query string. + * + * @param queryValue + * the new query string + */ + public void setQueryString(final String queryValue) { + this.queryString = queryValue; + if (this.queryString != null) { + for (final String eachParameter : this.queryString.split("[&]", -1)) { + final String[] pair = eachParameter.split("=", -1); + final String value = pair.length == 2 ? pair[1] : ""; + this.addParameter(pair[0], value); + } + } + } + + @Override + public void setRemoteAddr(final String value) { + this.remoteAddr = value; + } + + @Override + public void setRemoteHost(final String value) { + this.remoteHost = value; + } + + /** + * Sets the remote user. + * + * @param value + * the new remote user + */ + public void setRemoteUser(final String value) { + this.remoteUser = value; + } + + /** + * Sets the request uri. + * + * @param value + * the new request uri + */ + public void setRequestURI(final String value) { + this.requestURI = value; + } + + @Override + public void setUserPrincipal(final Principal value) { + this.principal = value; + } + +} diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpResponse.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpResponse.java new file mode 100644 index 0000000000..cb117c037f --- /dev/null +++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpResponse.java @@ -0,0 +1,145 @@ +/* + * MIT License + * + * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package waffle.apache.catalina; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.catalina.connector.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple HTTP Response. + */ +public class SimpleHttpResponse extends Response { + + /** + * Instantiates a new simple http response. + * + * @param coyoteResponse + * the coyote response + */ + public SimpleHttpResponse(org.apache.coyote.Response coyoteResponse) { + super(coyoteResponse); + } + + /** The Constant LOGGER. */ + private static final Logger LOGGER = LoggerFactory.getLogger(SimpleHttpResponse.class); + + /** The status. */ + private int status = 500; + + /** The headers. */ + private final Map> headers = new HashMap<>(); + + @Override + public void addHeader(final String headerName, final String headerValue) { + List current = this.headers.get(headerName); + if (current == null) { + current = new ArrayList<>(); + } + current.add(headerValue); + this.headers.put(headerName, current); + } + + @Override + public void flushBuffer() { + SimpleHttpResponse.LOGGER.info("{} {}", Integer.valueOf(this.status), this.getStatusString()); + for (final String header : this.headers.keySet()) { + for (final String headerValue : this.headers.get(header)) { + SimpleHttpResponse.LOGGER.info("{}: {}", header, headerValue); + } + } + } + + @Override + public String getHeader(final String headerName) { + final List headerValues = this.headers.get(headerName); + return headerValues == null ? null : String.join(", ", headerValues); + } + + @Override + public Collection getHeaderNames() { + return this.headers.keySet(); + } + + /** + * Gets the header values. + * + * @param headerName + * the header name + * + * @return the header values + */ + public String[] getHeaderValues(final String headerName) { + final List headerValues = this.headers.get(headerName); + return headerValues == null ? null : headerValues.toArray(new String[0]); + } + + @Override + public int getStatus() { + return this.status; + } + + /** + * Gets the status string. + * + * @return the status string + */ + public String getStatusString() { + return this.status == 401 ? "Unauthorized" : "Unknown"; + } + + @Override + public void sendError(final int rc) { + this.status = rc; + } + + @Override + public void sendError(final int rc, final String message) { + this.status = rc; + } + + @Override + public void setHeader(final String headerName, final String headerValue) { + List current = this.headers.get(headerName); + if (current == null) { + current = new ArrayList<>(); + } else { + current.clear(); + } + current.add(headerValue); + this.headers.put(headerName, current); + } + + @Override + public void setStatus(final int value) { + this.status = value; + } + +}