mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 09:03:21 +00:00 
			
		
		
		
	GUACAMOLE-839: Allow accepted subject DNs to be restricted via configuration.
This commit is contained in:
		| @@ -34,6 +34,7 @@ import java.util.List; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import javax.naming.InvalidNameException; | ||||
| import javax.naming.ldap.LdapName; | ||||
| import javax.naming.ldap.Rdn; | ||||
| import javax.ws.rs.GET; | ||||
| import javax.ws.rs.core.Response; | ||||
| import javax.ws.rs.HeaderParam; | ||||
| @@ -45,10 +46,11 @@ import javax.ws.rs.core.UriBuilder; | ||||
| import org.apache.guacamole.GuacamoleClientException; | ||||
| import org.apache.guacamole.GuacamoleException; | ||||
| import org.apache.guacamole.GuacamoleResourceNotFoundException; | ||||
| import org.apache.guacamole.GuacamoleServerException; | ||||
| import org.apache.guacamole.auth.ssl.conf.ConfigurationService; | ||||
| import org.apache.guacamole.auth.sso.NonceService; | ||||
| import org.apache.guacamole.auth.sso.SSOResource; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| /** | ||||
|  * REST API resource that allows the user to retrieve an opaque state value | ||||
| @@ -66,6 +68,25 @@ public class SSLClientAuthenticationResource extends SSOResource { | ||||
|      */ | ||||
|     private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; | ||||
|  | ||||
|     /** | ||||
|      * The string value that the SSL termination service uses for its client | ||||
|      * verification header to represent that the client certificate is absent. | ||||
|      */ | ||||
|     private static final String CLIENT_VERIFIED_HEADER_NONE_VALUE = "NONE"; | ||||
|  | ||||
|     /** | ||||
|      * The string prefix that the SSL termination service uses for its client | ||||
|      * verification header to represent that the client certificate has failed | ||||
|      * validation. The error message describing the nature of the failure is | ||||
|      * provided by the SSL termination service after this prefix. | ||||
|      */ | ||||
|     private static final String CLIENT_VERIFIED_HEADER_FAILED_PREFIX = "FAILED:"; | ||||
|  | ||||
|     /** | ||||
|      * Logger for this class. | ||||
|      */ | ||||
|     private static final Logger logger = LoggerFactory.getLogger(SSLClientAuthenticationResource.class); | ||||
|  | ||||
|     /** | ||||
|      * Service for retrieving configuration information. | ||||
|      */ | ||||
| @@ -143,6 +164,67 @@ public class SSLClientAuthenticationResource extends SSOResource { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Extracts a user's username from the X.509 subject name, which should be | ||||
|      * in LDAP DN format. If specific username attributes are configured, only | ||||
|      * those username attributes are used to determine the name. If a specific | ||||
|      * base DN is configured, only subject names that are formatted as LDAP DNs | ||||
|      * within that base DN will be accepted. | ||||
|      * | ||||
|      * @param name | ||||
|      *     The subject name to extract the username from. | ||||
|      * | ||||
|      * @return | ||||
|      *     The username of the user represented by the given subject name. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If any configuration parameters related to retrieving certificates | ||||
|      *     from HTTP request cannot be parsed, or if the provided subject name | ||||
|      *     cannot be parsed or is not acceptable (wrong base DN or wrong | ||||
|      *     username attribute). | ||||
|      */ | ||||
|     public String getUsername(String name) throws GuacamoleException { | ||||
|  | ||||
|         // Extract user's DN from their X.509 certificate | ||||
|         LdapName dn; | ||||
|         try { | ||||
|             dn = new LdapName(name); | ||||
|         } | ||||
|         catch (InvalidNameException e) { | ||||
|             throw new GuacamoleClientException("Subject \"" + name + "\" is " | ||||
|                     + "not a valid DN: " + e.getMessage(), e); | ||||
|         } | ||||
|  | ||||
|         // Verify DN actually contains components | ||||
|         int numComponents = dn.size(); | ||||
|         if (numComponents < 1) | ||||
|             throw new GuacamoleClientException("Subject DN is empty."); | ||||
|  | ||||
|         // Verify DN is within configured base DN (if any) | ||||
|         LdapName baseDN = confService.getSubjectBaseDN(); | ||||
|         if (baseDN != null && !(numComponents > baseDN.size() && dn.startsWith(baseDN))) | ||||
|             throw new GuacamoleClientException("Subject DN \"" + dn + "\" is " | ||||
|                     + "not within the configured base DN."); | ||||
|  | ||||
|         // Retrieve the least significant attribute from the parsed DN - this | ||||
|         // will be the username | ||||
|         Rdn nameRdn = dn.getRdn(numComponents - 1); | ||||
|  | ||||
|         // Verify that the username is specified with one of the allowed | ||||
|         // attributes | ||||
|         List<String> usernameAttributes = confService.getSubjectUsernameAttributes(); | ||||
|         if (usernameAttributes != null && !usernameAttributes.stream().anyMatch(nameRdn.getType()::equalsIgnoreCase)) | ||||
|             throw new GuacamoleClientException("Subject DN \"" + dn + "\" " | ||||
|                     + "does not contain an acceptable username attribute."); | ||||
|  | ||||
|         // The DN is valid - extract the username from the least significant | ||||
|         // component | ||||
|         String username = nameRdn.getValue().toString(); | ||||
|         logger.debug("Username \"{}\" extracted from subject DN \"{}\".", username, dn); | ||||
|         return username; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Authenticates a user using HTTP headers containing that user's verified | ||||
|      * X.509 certificate. It is assumed that this certificate is being passed | ||||
| @@ -154,15 +236,13 @@ public class SSLClientAuthenticationResource extends SSOResource { | ||||
|      *     The raw bytes of the X.509 certificate retrieved from the request. | ||||
|      * | ||||
|      * @return | ||||
|      *     A new SSOAuthenticatedUser representing the identity of the user | ||||
|      *     asserted by the SSL termination service via that user's X.509 | ||||
|      *     certificate. | ||||
|      *     The username of the user asserted by the SSL termination service via | ||||
|      *     that user's X.509 certificate. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If the provided X.509 certificate is not valid or cannot be parsed. | ||||
|      *     It is expected that the SSL termination service will already have | ||||
|      *     validated the certificate; this function validates only the | ||||
|      *     certificate timestamps. | ||||
|      *     If any configuration parameters related to retrieving certificates | ||||
|      *     from HTTP request cannot be parsed, or if the certificate is not | ||||
|      *     valid/present. | ||||
|      */ | ||||
|     public String getUsername(byte[] certificate) throws GuacamoleException { | ||||
|  | ||||
| @@ -179,36 +259,15 @@ public class SSLClientAuthenticationResource extends SSOResource { | ||||
|  | ||||
|         } | ||||
|         catch (CertificateException e) { | ||||
|             throw new GuacamoleClientException("The X.509 certificate " | ||||
|                     + "presented is not valid.", e); | ||||
|             throw new GuacamoleClientException("Certificate is not valid: " + e.getMessage(), e); | ||||
|         } | ||||
|         catch (IOException e) { | ||||
|             throw new GuacamoleServerException("Provided X.509 certificate " | ||||
|                     + "could not be read.", e); | ||||
|             throw new GuacamoleClientException("Certificate could not be read: " + e.getMessage(), e); | ||||
|         } | ||||
|  | ||||
|         // Extract user's DN from their X.509 certificate | ||||
|         LdapName dn; | ||||
|         try { | ||||
|             Principal principal = cert.getSubjectX500Principal(); | ||||
|             dn = new LdapName(principal.getName()); | ||||
|         } | ||||
|         catch (InvalidNameException e) { | ||||
|             throw new GuacamoleClientException("The X.509 certificate " | ||||
|                     + "presented does not contain a valid subject DN.", e); | ||||
|         } | ||||
|  | ||||
|         // Verify DN actually contains components | ||||
|         int numComponents = dn.size(); | ||||
|         if (numComponents < 1) | ||||
|             throw new GuacamoleClientException("The X.509 certificate " | ||||
|                     + "presented contains an empty subject DN."); | ||||
|  | ||||
|         // Simply use first component of DN as username (TODO: Enforce | ||||
|         // requirements on the attribute providing the username and the base DN, | ||||
|         // and consider using components following the username to determine | ||||
|         // group memberships) | ||||
|         return dn.getRdn(numComponents - 1).getValue().toString(); | ||||
|         Principal principal = cert.getSubjectX500Principal(); | ||||
|         return getUsername(principal.getName()); | ||||
|  | ||||
|     } | ||||
|  | ||||
| @@ -216,7 +275,7 @@ public class SSLClientAuthenticationResource extends SSOResource { | ||||
|      * Processes the X.509 certificate in the headers of the given HTTP | ||||
|      * request, returning an authentication session token representing the | ||||
|      * identity in that certificate. If the certificate is invalid or not | ||||
|      * present, null is returned. | ||||
|      * present, an invalid session token is returned. | ||||
|      * | ||||
|      * @param credentials | ||||
|      *     The credentials submitted in the HTTP request being processed. | ||||
| @@ -226,14 +285,10 @@ public class SSLClientAuthenticationResource extends SSOResource { | ||||
|      * | ||||
|      * @return | ||||
|      *     An authentication session token representing the identity in the | ||||
|      *     certificate in the given HTTP request, or null if the request does | ||||
|      *     not contain a valid certificate. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If any configuration parameters related to retrieving certificates | ||||
|      *     from HTTP request cannot be parsed. | ||||
|      *     certificate in the given HTTP request, or an invalid session token | ||||
|      *     if no valid identity was asserted. | ||||
|      */ | ||||
|     private String processCertificate(HttpHeaders headers) throws GuacamoleException { | ||||
|     private String processCertificate(HttpHeaders headers) { | ||||
|  | ||||
|         // | ||||
|         // NOTE: A result with an associated state is ALWAYS returned by | ||||
| @@ -245,18 +300,46 @@ public class SSLClientAuthenticationResource extends SSOResource { | ||||
|         // failures. | ||||
|         // | ||||
|  | ||||
|         // Verify that SSL termination has already verified the certificate | ||||
|         String verified = getHeader(headers, confService.getClientVerifiedHeader()); | ||||
|         if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) | ||||
|             return sessionManager.generateInvalid(); | ||||
|         try { | ||||
|  | ||||
|         String certificate = getHeader(headers, confService.getClientCertificateHeader()); | ||||
|         if (certificate == null) | ||||
|             return sessionManager.generateInvalid(); | ||||
|             // Verify that SSL termination has already verified the certificate | ||||
|             String verified = getHeader(headers, confService.getClientVerifiedHeader()); | ||||
|             if (verified != null && verified.startsWith(CLIENT_VERIFIED_HEADER_FAILED_PREFIX)) { | ||||
|                 String message = verified.substring(CLIENT_VERIFIED_HEADER_FAILED_PREFIX.length()); | ||||
|                 throw new GuacamoleClientException("Client certificate did " | ||||
|                         + "not pass validation. SSL termination reports the " | ||||
|                         + "following failure: \"" + message + "\""); | ||||
|             } | ||||
|             else if (CLIENT_VERIFIED_HEADER_NONE_VALUE.equals(verified)) { | ||||
|                 throw new GuacamoleClientException("No client certificate was presented."); | ||||
|             } | ||||
|             else if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) { | ||||
|                 throw new GuacamoleClientException("Client certificate did not pass validation."); | ||||
|             } | ||||
|  | ||||
|         String username = getUsername(decode(certificate)); | ||||
|         long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); | ||||
|         return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); | ||||
|             String certificate = getHeader(headers, confService.getClientCertificateHeader()); | ||||
|             if (certificate == null) | ||||
|                 throw new GuacamoleClientException("Client certificate missing from request."); | ||||
|  | ||||
|             String username = getUsername(decode(certificate)); | ||||
|             long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); | ||||
|             return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); | ||||
|  | ||||
|         } | ||||
|         catch (GuacamoleClientException e) { | ||||
|             logger.warn("SSL/TLS client authentication attempt rejected: {}", e.getMessage()); | ||||
|             logger.debug("SSL/TLS client authentication failed.", e); | ||||
|         } | ||||
|         catch (GuacamoleException e) { | ||||
|             logger.error("SSL/TLS client authentication attempt could not be processed: {}", e.getMessage()); | ||||
|             logger.debug("SSL/TLS client authentication failed.", e); | ||||
|         } | ||||
|         catch (RuntimeException | Error e) { | ||||
|             logger.error("SSL/TLS client authentication attempt failed internally: {}", e.getMessage()); | ||||
|             logger.debug("Internal failure processing SSL/TLS client authentication attempt.", e); | ||||
|         } | ||||
|  | ||||
|         return sessionManager.generateInvalid(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -22,12 +22,15 @@ package org.apache.guacamole.auth.ssl.conf; | ||||
| import com.google.inject.Inject; | ||||
| import java.net.URI; | ||||
| import java.net.URISyntaxException; | ||||
| import java.util.List; | ||||
| import javax.naming.ldap.LdapName; | ||||
| import javax.ws.rs.core.UriBuilder; | ||||
| import org.apache.guacamole.GuacamoleException; | ||||
| import org.apache.guacamole.GuacamoleServerException; | ||||
| import org.apache.guacamole.environment.Environment; | ||||
| import org.apache.guacamole.properties.IntegerGuacamoleProperty; | ||||
| import org.apache.guacamole.properties.StringGuacamoleProperty; | ||||
| import org.apache.guacamole.properties.StringListProperty; | ||||
| import org.apache.guacamole.properties.URIGuacamoleProperty; | ||||
|  | ||||
| /** | ||||
| @@ -136,6 +139,34 @@ public class ConfigurationService { | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * The property defining the LDAP attribute or attributes that may be used | ||||
|      * to represent a username within the subject DN of a user's X.509 | ||||
|      * certificate. If the least-significant attribute of the subject DN is not | ||||
|      * one of these attributes, the certificate will be rejected. By default, | ||||
|      * any attribute is accepted. | ||||
|      */ | ||||
|     private static final StringListProperty SSL_SUBJECT_USERNAME_ATTRIBUTE = | ||||
|             new StringListProperty () { | ||||
|  | ||||
|         @Override | ||||
|         public String getName() { return "ssl-subject-username-attribute"; } | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * The property defining the base DN containing all valid subject DNs. If | ||||
|      * specified, only certificates asserting subject DNs beneath this base DN | ||||
|      * will be accepted. By default, all DNs are accepted. | ||||
|      */ | ||||
|     private static final LdapNameGuacamoleProperty SSL_SUBJECT_BASE_DN = | ||||
|             new LdapNameGuacamoleProperty () { | ||||
|  | ||||
|         @Override | ||||
|         public String getName() { return "ssl-subject-base-dn"; } | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * The property representing the amount of time that the temporary, unique | ||||
|      * subdomain generated for SSL/TLS authentication may remain valid, in | ||||
| @@ -374,4 +405,36 @@ public class ConfigurationService { | ||||
|         return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the base DN that contains all valid subject DNs. If there is no | ||||
|      * such base DN (and all subject DNs are valid), null is returned. | ||||
|      * | ||||
|      * @return | ||||
|      *     The base DN that contains all valid subject DNs, or null if all | ||||
|      *     subject DNs are valid. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If the configured base DN cannot be read or is not a valid LDAP DN. | ||||
|      */ | ||||
|     public LdapName getSubjectBaseDN() throws GuacamoleException { | ||||
|         return environment.getProperty(SSL_SUBJECT_BASE_DN); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a list of all attributes that may be used to represent a user's | ||||
|      * username within their subject DN. If all attributes may be accepted, | ||||
|      * null is returned. | ||||
|      * | ||||
|      * @return | ||||
|      *     A list of all attributes that may be used to represent a user's | ||||
|      *     username within their subject DN, or null if any attribute may be | ||||
|      *     used. | ||||
|      * | ||||
|      * @throws GuacamoleException | ||||
|      *     If the configured set of username attributes cannot be read. | ||||
|      */ | ||||
|     public List<String> getSubjectUsernameAttributes() throws GuacamoleException { | ||||
|         return environment.getProperty(SSL_SUBJECT_USERNAME_ATTRIBUTE); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,50 @@ | ||||
| /* | ||||
|  * Licensed to the Apache Software Foundation (ASF) under one | ||||
|  * or more contributor license agreements.  See the NOTICE file | ||||
|  * distributed with this work for additional information | ||||
|  * regarding copyright ownership.  The ASF licenses this file | ||||
|  * to you under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance | ||||
|  * with the License.  You may obtain a copy of the License at | ||||
|  * | ||||
|  *   http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, | ||||
|  * software distributed under the License is distributed on an | ||||
|  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||||
|  * KIND, either express or implied.  See the License for the | ||||
|  * specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  */ | ||||
|  | ||||
| package org.apache.guacamole.auth.ssl.conf; | ||||
|  | ||||
| import javax.naming.InvalidNameException; | ||||
| import javax.naming.ldap.LdapName; | ||||
| import org.apache.guacamole.GuacamoleException; | ||||
| import org.apache.guacamole.GuacamoleServerException; | ||||
| import org.apache.guacamole.properties.GuacamoleProperty; | ||||
|  | ||||
| /** | ||||
|  * A GuacamoleProperty whose value is an LDAP name, such as a distinguished | ||||
|  * name. | ||||
|  */ | ||||
| public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty<LdapName> { | ||||
|  | ||||
|     @Override | ||||
|     public LdapName parseValue(String value) throws GuacamoleException { | ||||
|  | ||||
|         if (value == null) | ||||
|             return null; | ||||
|  | ||||
|         try { | ||||
|             return new LdapName(value); | ||||
|         } | ||||
|         catch (InvalidNameException e) { | ||||
|             throw new GuacamoleServerException("Value \"" + value | ||||
|                 + "\" is not a valid LDAP name.", e); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user