GUACAMOLE-1219: Add support for disabling TOTP for specific users and groups.

This commit is contained in:
Virtually Nick
2023-03-13 21:17:45 -04:00
parent b283341846
commit 4dbf35766f
5 changed files with 218 additions and 1 deletions

View File

@@ -33,6 +33,12 @@ import org.apache.guacamole.net.auth.User;
*/ */
public class TOTPUser extends DelegatingUser { 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. * 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 * The string value used by TOTP user attributes to represent the boolean
* value "true". * 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. * The form which contains all configurable properties for this user.
*/ */
public static final Form TOTP_ENROLLMENT_STATUS = new Form("totp-enrollment-status", public static final Form TOTP_ENROLLMENT_STATUS = new Form("totp-enrollment-status",
Arrays.asList( 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_SECRET_GENERATED_ATTRIBUTE_NAME, TRUTH_VALUE),
new BooleanField(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, TRUTH_VALUE) new BooleanField(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, TRUTH_VALUE)
) )
@@ -96,6 +103,9 @@ public class TOTPUser extends DelegatingUser {
// Create independent, mutable copy of attributes // Create independent, mutable copy of attributes
Map<String, String> attributes = new HashMap<>(super.getAttributes()); Map<String, String> 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 // Replace secret key with simple boolean attribute representing
// whether a key has been generated at all // whether a key has been generated at all
String secret = attributes.remove(TOTP_KEY_SECRET_ATTRIBUTE_NAME); String secret = attributes.remove(TOTP_KEY_SECRET_ATTRIBUTE_NAME);

View File

@@ -23,12 +23,14 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.totp.usergroup.TOTPUserGroup;
import org.apache.guacamole.form.Form; import org.apache.guacamole.form.Form;
import org.apache.guacamole.net.auth.DecoratingDirectory; import org.apache.guacamole.net.auth.DecoratingDirectory;
import org.apache.guacamole.net.auth.DelegatingUserContext; import org.apache.guacamole.net.auth.DelegatingUserContext;
import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup;
/** /**
* TOTP-specific UserContext implementation which wraps the UserContext of * TOTP-specific UserContext implementation which wraps the UserContext of
@@ -65,6 +67,24 @@ public class TOTPUserContext extends DelegatingUserContext {
}; };
} }
@Override
public Directory<UserGroup> getUserGroupDirectory() throws GuacamoleException {
return new DecoratingDirectory<UserGroup>(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 @Override
public Collection<Form> getUserAttributes() { public Collection<Form> getUserAttributes() {
Collection<Form> userAttrs = new HashSet<>(super.getUserAttributes()); Collection<Form> userAttrs = new HashSet<>(super.getUserAttributes());
@@ -72,4 +92,11 @@ public class TOTPUserContext extends DelegatingUserContext {
return Collections.unmodifiableCollection(userAttrs); return Collections.unmodifiableCollection(userAttrs);
} }
@Override
public Collection<Form> getUserGroupAttributes() {
Collection<Form> userGroupAttrs = new HashSet<>(super.getUserGroupAttributes());
userGroupAttrs.add(TOTPUserGroup.TOTP_USER_GROUP_CONFIG);
return Collections.unmodifiableCollection(userGroupAttrs);
}
} }

View File

@@ -26,19 +26,23 @@ import java.security.InvalidKeyException;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleUnsupportedException; import org.apache.guacamole.GuacamoleUnsupportedException;
import org.apache.guacamole.auth.totp.conf.ConfigurationService; import org.apache.guacamole.auth.totp.conf.ConfigurationService;
import org.apache.guacamole.auth.totp.form.AuthenticationCodeField; 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.form.Field;
import org.apache.guacamole.language.TranslatableGuacamoleClientException; import org.apache.guacamole.language.TranslatableGuacamoleClientException;
import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException; import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials; 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.User;
import org.apache.guacamole.net.auth.UserContext; 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.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.totp.TOTPGenerator; import org.apache.guacamole.totp.TOTPGenerator;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -204,6 +208,65 @@ public class UserVerificationService {
} }
/**
* 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<String, String> 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<String> userGroups = authenticatedUser.getEffectiveUserGroups();
Directory<UserGroup> directoryGroups = context.getPrivileged().getUserGroupDirectory();
for (String userGroup : userGroups) {
UserGroup thisGroup = directoryGroups.get(userGroup);
if (thisGroup == null)
continue;
Map<String, String> 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 * Verifies the identity of the given user using TOTP. If a authentication
* code from the user's TOTP device has not already been provided, a code is * code from the user's TOTP device has not already been provided, a code is
@@ -230,6 +293,10 @@ public class UserVerificationService {
if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER)) if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return; return;
// Check if TOTP has been disabled for this user
if (totpDisabled(context, authenticatedUser))
return;
// Ignore users which do not have an associated key // Ignore users which do not have an associated key
UserTOTPKey key = getKey(context, username); UserTOTPKey key = getKey(context, username);
if (key == null) if (key == null)

View File

@@ -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<String, String> getAttributes() {
// Create a mutable copy of the attributes
Map<String, String> attributes = new HashMap<>(super.getAttributes());
if (!attributes.containsKey(TOTP_KEY_DISABLED_ATTRIBUTE_NAME))
attributes.put(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, null);
return attributes;
}
}

View File

@@ -33,11 +33,20 @@
"USER_ATTRIBUTES" : { "USER_ATTRIBUTES" : {
"FIELD_HEADER_GUAC_TOTP_DISABLED" : "Disable TOTP:",
"FIELD_HEADER_GUAC_TOTP_KEY_GENERATED" : "Secret key generated:", "FIELD_HEADER_GUAC_TOTP_KEY_GENERATED" : "Secret key generated:",
"FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "Authentication device confirmed:", "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "Authentication device confirmed:",
"SECTION_HEADER_TOTP_ENROLLMENT_STATUS" : "TOTP Enrollment Status" "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"
} }
} }