diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java index 0c6f567b8..e84b7543b 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java @@ -33,6 +33,12 @@ import org.apache.guacamole.net.auth.User; */ public class TOTPUser extends DelegatingUser { + /** + * The name of the user attribute which disables the TOTP requirement + * for that specific user. + */ + public static final String TOTP_KEY_DISABLED_ATTRIBUTE_NAME = "guac-totp-disabled"; + /** * The name of the user attribute which stores the TOTP key. */ @@ -56,13 +62,14 @@ public class TOTPUser extends DelegatingUser { * The string value used by TOTP user attributes to represent the boolean * value "true". */ - private static final String TRUTH_VALUE = "true"; + public static final String TRUTH_VALUE = "true"; /** * The form which contains all configurable properties for this user. */ public static final Form TOTP_ENROLLMENT_STATUS = new Form("totp-enrollment-status", Arrays.asList( + new BooleanField(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, TRUTH_VALUE), new BooleanField(TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME, TRUTH_VALUE), new BooleanField(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, TRUTH_VALUE) ) @@ -95,6 +102,9 @@ public class TOTPUser extends DelegatingUser { // Create independent, mutable copy of attributes Map attributes = new HashMap<>(super.getAttributes()); + + if (!attributes.containsKey(TOTP_KEY_DISABLED_ATTRIBUTE_NAME)) + attributes.put(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, null); // Replace secret key with simple boolean attribute representing // whether a key has been generated at all diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java index a7a97b022..d3269e78a 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java @@ -23,12 +23,14 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.totp.usergroup.TOTPUserGroup; import org.apache.guacamole.form.Form; import org.apache.guacamole.net.auth.DecoratingDirectory; import org.apache.guacamole.net.auth.DelegatingUserContext; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.auth.UserGroup; /** * TOTP-specific UserContext implementation which wraps the UserContext of @@ -65,11 +67,36 @@ public class TOTPUserContext extends DelegatingUserContext { }; } + @Override + public Directory getUserGroupDirectory() throws GuacamoleException { + return new DecoratingDirectory(super.getUserGroupDirectory()) { + + @Override + protected UserGroup decorate(UserGroup object) { + return new TOTPUserGroup(object); + } + + @Override + protected UserGroup undecorate(UserGroup object) { + assert(object instanceof TOTPUserGroup); + return ((TOTPUserGroup) object).getUndecorated(); + } + + }; + } + @Override public Collection
getUserAttributes() { Collection userAttrs = new HashSet<>(super.getUserAttributes()); userAttrs.add(TOTPUser.TOTP_ENROLLMENT_STATUS); return Collections.unmodifiableCollection(userAttrs); } + + @Override + public Collection getUserGroupAttributes() { + Collection userGroupAttrs = new HashSet<>(super.getUserGroupAttributes()); + userGroupAttrs.add(TOTPUserGroup.TOTP_USER_GROUP_CONFIG); + return Collections.unmodifiableCollection(userGroupAttrs); + } } diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java index 2fca74229..027a228cd 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java @@ -26,19 +26,23 @@ import java.security.InvalidKeyException; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleUnsupportedException; import org.apache.guacamole.auth.totp.conf.ConfigurationService; import org.apache.guacamole.auth.totp.form.AuthenticationCodeField; +import org.apache.guacamole.auth.totp.usergroup.TOTPUserGroup; import org.apache.guacamole.form.Field; import org.apache.guacamole.language.TranslatableGuacamoleClientException; import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.totp.TOTPGenerator; import org.slf4j.Logger; @@ -203,6 +207,65 @@ public class UserVerificationService { return true; } + + /** + * Checks the user in question, via both UserContext and AuthenticatedUser, + * to see if TOTP has been disabled for this user, either directly or via + * membership in a group that has had TOTP marked as disabled. + * + * @param context + * The UserContext for the user being verified. + * + * @param authenticatedUser + * The AuthenticatedUser for the user being verified. + * + * @return + * True if TOTP access has been disabled for the user, otherwise + * false. + * + * @throws GuacamoleException + * If the extension handling storage fails internally while attempting + * to update the user. + */ + private boolean totpDisabled(UserContext context, + AuthenticatedUser authenticatedUser) + throws GuacamoleException { + + // If TOTP is disabled for this user, return, allowing login to continue + Map myAttributes = context.self().getAttributes(); + if (myAttributes != null + && TOTPUser.TRUTH_VALUE.equals(myAttributes.get(TOTPUser.TOTP_KEY_DISABLED_ATTRIBUTE_NAME))) { + + logger.warn("TOTP validation has been disabled for user \"{}\"", + context.self().getIdentifier()); + return true; + + } + + // Check if any effective user groups have TOTP marked as disabled + Set userGroups = authenticatedUser.getEffectiveUserGroups(); + Directory directoryGroups = context.getPrivileged().getUserGroupDirectory(); + for (String userGroup : userGroups) { + UserGroup thisGroup = directoryGroups.get(userGroup); + if (thisGroup == null) + continue; + + Map grpAttributes = thisGroup.getAttributes(); + if (grpAttributes != null + && TOTPUserGroup.TRUTH_VALUE.equals(grpAttributes.get(TOTPUserGroup.TOTP_KEY_DISABLED_ATTRIBUTE_NAME))) { + + logger.warn("TOTP validation will be bypassed for user \"{}\"" + + " because it has been disabled for group \"{}\"", + context.self().getIdentifier(), userGroup); + return true; + + } + } + + // TOTP has not been disabled + return false; + + } /** * Verifies the identity of the given user using TOTP. If a authentication @@ -230,6 +293,10 @@ public class UserVerificationService { if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER)) return; + // Check if TOTP has been disabled for this user + if (totpDisabled(context, authenticatedUser)) + return; + // Ignore users which do not have an associated key UserTOTPKey key = getKey(context, username); if (key == null) diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/usergroup/TOTPUserGroup.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/usergroup/TOTPUserGroup.java new file mode 100644 index 000000000..5b6921bab --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/usergroup/TOTPUserGroup.java @@ -0,0 +1,104 @@ +/* + * 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.totp.usergroup; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.apache.guacamole.form.BooleanField; +import org.apache.guacamole.form.Form; +import org.apache.guacamole.net.auth.DelegatingUserGroup; +import org.apache.guacamole.net.auth.UserGroup; + +/** + * A UserGroup that wraps another UserGroup implementation, decorating it with + * attributes that control TOTP configuration for users that are members of that + * group. + */ +public class TOTPUserGroup extends DelegatingUserGroup { + + /** + * The attribute associated with a group that disables the TOTP requirement + * for any users that are a member of that group, or are members of any + * groups that are members of this group. + */ + public static final String TOTP_KEY_DISABLED_ATTRIBUTE_NAME = "guac-totp-disabled"; + + /** + * The string value used by TOTP user attributes to represent the boolean + * value "true". + */ + public static final String TRUTH_VALUE = "true"; + + /** + * The form that contains fields for configuring TOTP for members of this + * group. + */ + public static final Form TOTP_USER_GROUP_CONFIG = new Form("totp-user-group-config", + Arrays.asList( + new BooleanField(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, TRUTH_VALUE) + ) + ); + + /** + * Create a new instance of this user group implementation, wrapping the + * provided UserGroup. + * + * @param userGroup + * The UserGroup to be wrapped. + */ + public TOTPUserGroup(UserGroup userGroup) { + super(userGroup); + } + + /** + * Return the original UserGroup that this implementation is wrapping. + * + * @return + * The original UserGroup that this implementation wraps. + */ + public UserGroup getUndecorated() { + return getDelegateUserGroupGroup(); + } + + /** + * Returns whether or not TOTP has been disabled for members of this group. + * + * @return + * True if TOTP has been disabled for members of this group, otherwise + * false. + */ + public boolean totpDisabled() { + return (TRUTH_VALUE.equals(getAttributes().get(TOTP_KEY_DISABLED_ATTRIBUTE_NAME))); + } + + @Override + public Map getAttributes() { + + // Create a mutable copy of the attributes + Map attributes = new HashMap<>(super.getAttributes()); + + if (!attributes.containsKey(TOTP_KEY_DISABLED_ATTRIBUTE_NAME)) + attributes.put(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, null); + + return attributes; + + } + +} diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/en.json b/extensions/guacamole-auth-totp/src/main/resources/translations/en.json index 80bdaf06a..e9f4b7140 100644 --- a/extensions/guacamole-auth-totp/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-totp/src/main/resources/translations/en.json @@ -33,11 +33,20 @@ "USER_ATTRIBUTES" : { + "FIELD_HEADER_GUAC_TOTP_DISABLED" : "Disable TOTP:", "FIELD_HEADER_GUAC_TOTP_KEY_GENERATED" : "Secret key generated:", "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "Authentication device confirmed:", "SECTION_HEADER_TOTP_ENROLLMENT_STATUS" : "TOTP Enrollment Status" + }, + + "USER_GROUP_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_TOTP_DISABLED" : "Disable TOTP:", + + "SECTION_HEADER_TOTP_USER_GROUP_CONFIG" : "TOTP Configuration" + } }