diff --git a/doc/licenses/apache-commons-beanutils-1.9.4/NOTICE b/doc/licenses/apache-commons-beanutils-1.9.4/NOTICE
new file mode 100644
index 0000000000..3f59805ce4
--- /dev/null
+++ b/doc/licenses/apache-commons-beanutils-1.9.4/NOTICE
@@ -0,0 +1,2 @@
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/doc/licenses/apache-commons-beanutils-1.9.4/README b/doc/licenses/apache-commons-beanutils-1.9.4/README
new file mode 100644
index 0000000000..e021ee3ef5
--- /dev/null
+++ b/doc/licenses/apache-commons-beanutils-1.9.4/README
@@ -0,0 +1,9 @@
+Apache Commons BeanUtils
+(http://commons.apache.org/proper/commons-beanutils/)
+-----------------------------------------------------
+
+ Version: 1.9.4
+ From: 'Apache Software Foundation' (https://www.apache.org/)
+ License(s):
+ Apache v2.0
+
diff --git a/doc/licenses/apache-commons-beanutils-1.9.4/dep-coordinates.txt b/doc/licenses/apache-commons-beanutils-1.9.4/dep-coordinates.txt
new file mode 100644
index 0000000000..0930475b0b
--- /dev/null
+++ b/doc/licenses/apache-commons-beanutils-1.9.4/dep-coordinates.txt
@@ -0,0 +1,2 @@
+commons-beanutils:commons-beanutils-core:jar:1.9.4
+commons-beanutils:commons-beanutils:jar:1.9.4
diff --git a/doc/licenses/apache-commons-digester-2.1/NOTICE b/doc/licenses/apache-commons-digester-2.1/NOTICE
new file mode 100644
index 0000000000..c5585462c3
--- /dev/null
+++ b/doc/licenses/apache-commons-digester-2.1/NOTICE
@@ -0,0 +1,5 @@
+Apache Jakarta Commons Digester
+Copyright 2001-2006 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/doc/licenses/apache-commons-digester-2.1/README b/doc/licenses/apache-commons-digester-2.1/README
new file mode 100644
index 0000000000..4d35f0da89
--- /dev/null
+++ b/doc/licenses/apache-commons-digester-2.1/README
@@ -0,0 +1,9 @@
+Apache Commons Digester
+(http://commons.apache.org/proper/commons-digester/)
+----------------------------------------------------
+
+ Version: 2.1
+ From: 'Apache Software Foundation' (https://www.apache.org/)
+ License(s):
+ Apache v2.0
+
diff --git a/doc/licenses/apache-commons-digester-2.1/dep-coordinates.txt b/doc/licenses/apache-commons-digester-2.1/dep-coordinates.txt
new file mode 100644
index 0000000000..00d115e08e
--- /dev/null
+++ b/doc/licenses/apache-commons-digester-2.1/dep-coordinates.txt
@@ -0,0 +1 @@
+commons-digester:commons-digester:jar:2.1
diff --git a/doc/licenses/apache-commons-net-3.9.0/NOTICE b/doc/licenses/apache-commons-net-3.9.0/NOTICE
new file mode 100644
index 0000000000..6efae5d4e8
--- /dev/null
+++ b/doc/licenses/apache-commons-net-3.9.0/NOTICE
@@ -0,0 +1,5 @@
+Apache Commons Net
+Copyright 2001-2023 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (https://www.apache.org/).
diff --git a/doc/licenses/apache-commons-net-3.9.0/README b/doc/licenses/apache-commons-net-3.9.0/README
new file mode 100644
index 0000000000..c79a8f1e3f
--- /dev/null
+++ b/doc/licenses/apache-commons-net-3.9.0/README
@@ -0,0 +1,8 @@
+Apache Commons Net (http://commons.apache.org/proper/commons-net/)
+--------------------------------------------------------------------
+
+ Version: 3.9.0
+ From: 'Apache Software Foundation' (https://www.apache.org/)
+ License(s):
+ Apache v2.0
+
diff --git a/doc/licenses/apache-commons-net-3.9.0/dep-coordinates.txt b/doc/licenses/apache-commons-net-3.9.0/dep-coordinates.txt
new file mode 100644
index 0000000000..788a75131b
--- /dev/null
+++ b/doc/licenses/apache-commons-net-3.9.0/dep-coordinates.txt
@@ -0,0 +1 @@
+commons-net:commons-net:jar:3.9.0
\ No newline at end of file
diff --git a/doc/licenses/apache-commons-validator-1.7/NOTICE b/doc/licenses/apache-commons-validator-1.7/NOTICE
new file mode 100644
index 0000000000..3f59805ce4
--- /dev/null
+++ b/doc/licenses/apache-commons-validator-1.7/NOTICE
@@ -0,0 +1,2 @@
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/doc/licenses/apache-commons-validator-1.7/README b/doc/licenses/apache-commons-validator-1.7/README
new file mode 100644
index 0000000000..68839dd3d7
--- /dev/null
+++ b/doc/licenses/apache-commons-validator-1.7/README
@@ -0,0 +1,8 @@
+Apache Commons Validator
+(https://commons.apache.org/proper/commons-validator/)
+-----------------------------------------------------
+
+ Version: 1.7
+ From: 'Apache Software Foundation' (https://www.apache.org/)
+ License(s):
+ Apache v2.0
diff --git a/doc/licenses/apache-commons-validator-1.7/dep-coordinates.txt b/doc/licenses/apache-commons-validator-1.7/dep-coordinates.txt
new file mode 100644
index 0000000000..9931f0c679
--- /dev/null
+++ b/doc/licenses/apache-commons-validator-1.7/dep-coordinates.txt
@@ -0,0 +1 @@
+commons-validator:commons-validator:jar:1.7
\ No newline at end of file
diff --git a/doc/licenses/ipaddress-5.4.0/README b/doc/licenses/ipaddress-5.4.0/README
new file mode 100644
index 0000000000..62c566334b
--- /dev/null
+++ b/doc/licenses/ipaddress-5.4.0/README
@@ -0,0 +1,7 @@
+ipaddress (https://github.com/seancfoley/IPAddress)
+---------------------------------------------
+
+ Version: 5.4.0
+ From: 'Sean C Foley' (https://github.com/seancfoley)
+ License(s):
+ Apache v2.0
\ No newline at end of file
diff --git a/doc/licenses/ipaddress-5.4.0/dep-coordinates.txt b/doc/licenses/ipaddress-5.4.0/dep-coordinates.txt
new file mode 100644
index 0000000000..fbcc0891e0
--- /dev/null
+++ b/doc/licenses/ipaddress-5.4.0/dep-coordinates.txt
@@ -0,0 +1 @@
+com.github.seancfoley:ipaddress:jar:5.4.0
\ No newline at end of file
diff --git a/extensions/guacamole-auth-restrict/.gitignore b/extensions/guacamole-auth-restrict/.gitignore
new file mode 100644
index 0000000000..1de9633aed
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/.gitignore
@@ -0,0 +1,3 @@
+src/main/resources/generated/
+target/
+*~
diff --git a/extensions/guacamole-auth-restrict/.ratignore b/extensions/guacamole-auth-restrict/.ratignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/extensions/guacamole-auth-restrict/pom.xml b/extensions/guacamole-auth-restrict/pom.xml
new file mode 100644
index 0000000000..a291c5b3b7
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/pom.xml
@@ -0,0 +1,191 @@
+
+
+
+
+ 4.0.0
+ org.apache.guacamole
+ guacamole-auth-restrict
+ jar
+ 1.5.1
+ guacamole-auth-restrict
+ http://guacamole.apache.org/
+
+
+ org.apache.guacamole
+ extensions
+ 1.5.1
+ ../
+
+
+
+
+
+
+
+ com.keithbranton.mojo
+ angular-maven-plugin
+ 0.3.4
+
+
+ generate-resources
+
+ html2js
+
+
+
+
+ ${basedir}/src/main/resources
+ **/*.html
+ ${basedir}/src/main/resources/generated/templates-main/templates.js
+ app/ext/restrict
+
+
+
+
+
+ com.github.buckelieg
+ minify-maven-plugin
+
+
+ default-cli
+
+ UTF-8
+
+ ${basedir}/src/main/resources
+ ${project.build.directory}/classes
+
+ /
+ /
+ restrict.css
+
+
+ license.txt
+
+
+
+ **/*.css
+
+
+ /
+ /
+ restrict.js
+
+
+ license.txt
+
+
+
+ **/*.js
+
+
+
+
+ **/*.test.js
+
+ CLOSURE
+
+
+
+ OFF
+ OFF
+
+
+
+
+ minify
+
+
+
+
+
+
+
+
+
+
+
+
+ org.apache.guacamole
+ guacamole-ext
+ 1.5.1
+ provided
+
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ com.google.inject
+ guice
+
+
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+
+
+ junit
+ junit
+ test
+
+
+
+
+ javax.ws.rs
+ javax.ws.rs-api
+ 2.0
+ provided
+
+
+
+
+ commons-validator
+ commons-validator
+ 1.7
+
+
+
+ commons-net
+ commons-net
+ 3.9.0
+
+
+
+
+ com.github.seancfoley
+ ipaddress
+ 5.4.0
+
+
+
+
+
diff --git a/extensions/guacamole-auth-restrict/src/main/assembly/dist.xml b/extensions/guacamole-auth-restrict/src/main/assembly/dist.xml
new file mode 100644
index 0000000000..0b16a71474
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/assembly/dist.xml
@@ -0,0 +1,53 @@
+
+
+
+
+ dist
+ ${project.artifactId}-${project.version}
+
+
+
+ tar.gz
+
+
+
+
+
+
+
+
+ target/licenses
+
+
+
+
+ target
+
+
+ *.jar
+
+
+
+
+
+
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionAuthenticationProvider.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionAuthenticationProvider.java
new file mode 100644
index 0000000000..f6f0d36999
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionAuthenticationProvider.java
@@ -0,0 +1,62 @@
+/*
+ * 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.restrict;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.restrict.user.RestrictUserContext;
+import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
+import org.apache.guacamole.net.auth.AuthenticatedUser;
+import org.apache.guacamole.net.auth.Credentials;
+import org.apache.guacamole.net.auth.UserContext;
+
+/**
+ * AuthenticationProvider implementation which provides additional restrictions
+ * for users, groups of users, connections, and connection groups, allowing
+ * administrators to further control access to Guacamole resources.
+ */
+public class RestrictionAuthenticationProvider extends AbstractAuthenticationProvider {
+
+ @Override
+ public String getIdentifier() {
+ return "restrict";
+ }
+
+ @Override
+ public UserContext decorate(UserContext context,
+ AuthenticatedUser authenticatedUser, Credentials credentials)
+ throws GuacamoleException {
+
+ // Verify identity of user
+ RestrictionVerificationService.verifyLoginRestrictions(context, authenticatedUser);
+
+ // User has been verified, and authentication should be allowed to
+ // continue
+ return new RestrictUserContext(context, credentials.getRemoteAddress());
+
+ }
+
+ @Override
+ public UserContext redecorate(UserContext decorated, UserContext context,
+ AuthenticatedUser authenticatedUser, Credentials credentials)
+ throws GuacamoleException {
+ return new RestrictUserContext(context, credentials.getRemoteAddress());
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java
new file mode 100644
index 0000000000..74395f524a
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java
@@ -0,0 +1,331 @@
+/*
+ * 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.restrict;
+
+import inet.ipaddr.HostName;
+import inet.ipaddr.HostNameException;
+import inet.ipaddr.IPAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.restrict.connection.RestrictConnection;
+import org.apache.guacamole.auth.restrict.user.RestrictUser;
+import org.apache.guacamole.auth.restrict.usergroup.RestrictUserGroup;
+import org.apache.guacamole.calendar.DailyRestriction;
+import org.apache.guacamole.calendar.TimeRestrictionParser;
+import org.apache.guacamole.host.HostRestrictionParser;
+import org.apache.guacamole.language.TranslatableGuacamoleSecurityException;
+import org.apache.guacamole.net.auth.AuthenticatedUser;
+import org.apache.guacamole.net.auth.Directory;
+import org.apache.guacamole.net.auth.UserContext;
+import org.apache.guacamole.net.auth.UserGroup;
+import org.apache.guacamole.net.auth.permission.SystemPermission;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for verifying additional user login restrictions against a given
+ * login attempt.
+ */
+public class RestrictionVerificationService {
+
+ /**
+ * Logger for this class.
+ */
+ private static final Logger LOGGER = LoggerFactory.getLogger(RestrictionVerificationService.class);
+
+ /**
+ * Parse out the provided strings of allowed and denied times, verifying
+ * whether or not a login or connection should be allowed at the current
+ * day and time. A boolean true will be returned if the action should be
+ * allowed, otherwise false will be returned.
+ *
+ * @param allowedTimeString
+ * The string containing the times that should be parsed to determine if
+ * the login or connection should be allowed at the current time, or
+ * null or an empty string if there are no specific allowed times defined.
+ *
+ * @param deniedTimeString
+ * The string containing the times that should be parsed to determine if
+ * the login or connection should be denied at the current time, or null
+ * or an empty string if there are no specific times during which a
+ * action should be denied.
+ *
+ * @return
+ * True if the login or connection should be allowed, otherwise false.
+ */
+ private static boolean allowedByTimeRestrictions(String allowedTimeString,
+ String deniedTimeString) {
+
+ // Check for denied entries, first, returning false if the login or
+ // connection should not be allowed.
+ if (deniedTimeString != null && !deniedTimeString.isEmpty()) {
+ List deniedTimes =
+ TimeRestrictionParser.parseString(deniedTimeString);
+
+ for (DailyRestriction restriction : deniedTimes) {
+ if (restriction.appliesNow())
+ return false;
+ }
+ }
+
+ // If no allowed entries are present, return true, allowing the login
+ // or connection to continue.
+ if (allowedTimeString == null || allowedTimeString.isEmpty())
+ return true;
+
+ List allowedTimes =
+ TimeRestrictionParser.parseString(allowedTimeString);
+
+ // Allowed entries are present, loop through them and check for a valid time.
+ for (DailyRestriction restriction : allowedTimes) {
+ // If this time allows the login or connection return true.
+ if (restriction.appliesNow())
+ return true;
+ }
+
+ // We have allowed entries, but login hasn't matched, so deny it.
+ return false;
+
+ }
+
+ /**
+ * Given the strings of allowed and denied hosts, verify that the login or
+ * connection should be allowed from the given remote address. If the action
+ * should not be allowed, return false - otherwise, return true.
+ *
+ * @param allowedHostsString
+ * The string containing a semicolon-separated list of hosts from
+ * which the login or connection should be allowed, or null or an empty
+ * string if no specific set of allowed hosts is defined.
+ *
+ * @param deniedHostsString
+ * The string containing a semicolon-separated list of hosts from
+ * which the login or connection should be denied, or null or an empty
+ * string if no specific set of denied hosts is defined.
+ *
+ * @param remoteAddress
+ * The IP address from which the user is logging in or has logged in
+ * and is attempting to connect from, if it is known. If it is unknown
+ * and restrictions are defined, the login or connection will be denied.
+ *
+ * @return
+ * True if the login or connection should be allowed by the host-based
+ * restrictions, otherwise false.
+ */
+ private static boolean allowedByHostRestrictions(String allowedHostsString,
+ String deniedHostsString, String remoteAddress) {
+
+ HostName remoteHostName = new HostName(remoteAddress);
+
+ // If attributes do not exist or are empty then the action is allowed.
+ if ((allowedHostsString == null || allowedHostsString.isEmpty())
+ && (deniedHostsString == null || deniedHostsString.isEmpty()))
+ return true;
+
+ // If the remote address cannot be determined, and restrictions are
+ // in effect, log an error and deny the action.
+ if (remoteAddress == null || remoteAddress.isEmpty()) {
+ LOGGER.error("Host-based restrictions are present, but the remote address is invalid or could not be resolved.");
+ return false;
+ }
+
+ // Split denied hosts attribute and process each entry, checking them
+ // against the current remote address, and returning false if a match is
+ // found.
+ List deniedHosts = HostRestrictionParser.parseHostList(deniedHostsString);
+ for (HostName hostName : deniedHosts) {
+ try {
+ if (hostName.isAddress() && hostName.toAddress().contains(remoteHostName.asAddress()))
+ return false;
+
+ else
+ for (IPAddress currAddr : hostName.toAllAddresses())
+ if (currAddr.matches(remoteHostName.asAddressString()))
+ return false;
+ }
+ catch (UnknownHostException | HostNameException e) {
+ LOGGER.error("Unknown or invalid host in denied hosts list: \"{}\"", hostName);
+ LOGGER.debug("Exception while trying to resolve host: \"{}\"", hostName, e);
+ return false;
+ }
+ }
+
+ // If denied hosts have been checked and allowed hosts are empty, we're
+ // good, and can allow the action.
+ if (allowedHostsString == null || allowedHostsString.isEmpty())
+ return true;
+
+ // Run through allowed hosts, if there are any, and return, allowing the
+ // action if there are any matches.
+ List allowedHosts = HostRestrictionParser.parseHostList(allowedHostsString);
+ for (HostName hostName : allowedHosts) {
+ try {
+ // If the entry is an IP or Subnet, check the remote address against it directly
+ if (hostName.isAddress() && hostName.toAddress().contains(remoteHostName.asAddress()))
+ return true;
+
+ // Entry is a hostname, so resolve to IPs and check each one
+ for (IPAddress currAddr : hostName.toAllAddresses())
+ if (currAddr.matches(remoteHostName.asAddressString()))
+ return true;
+
+ }
+ // If an entry cannot be resolved we will log a warning.
+ catch (UnknownHostException | HostNameException e) {
+ LOGGER.warn("Unknown host encountered in allowed host string: {}", hostName);
+ LOGGER.debug("Exception received trying to resolve host: {}", hostName, e);
+ }
+ }
+
+ // If we've made it here, the allowed hosts do not contain the remote
+ // address, and the action should not be allowed;
+ return false;
+
+ }
+
+ /**
+ * Verifies the login restrictions supported by this extension for the user
+ * who is attempting to log in, throwing an exception if any of the
+ * restrictions result in the user not being allowed to log in.
+ *
+ * @param context
+ * The context of the user who is attempting to log in.
+ *
+ * @param authenticatedUser
+ * The AuthenticatedUser object associated with the user who is
+ * attempting to log in.
+ *
+ * @throws GuacamoleException
+ * If any of the restrictions should prevent the user from logging in.
+ */
+ public static void verifyLoginRestrictions(UserContext context,
+ AuthenticatedUser authenticatedUser) throws GuacamoleException {
+
+ // Get user's attributes
+ Map userAttributes = context.self().getAttributes();
+ String remoteAddress = authenticatedUser.getCredentials().getRemoteAddress();
+
+ if (context.self().getEffectivePermissions().getSystemPermissions().hasPermission(SystemPermission.Type.ADMINISTER)) {
+ LOGGER.warn("User \"{}\" has System Administration permissions; additional restrictions will be bypassed.",
+ authenticatedUser.getIdentifier());
+ return;
+ }
+
+ // Verify time-based restrictions specific to the user
+ String allowedTimeString = userAttributes.get(RestrictUser.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String deniedTimeString = userAttributes.get(RestrictUser.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ if (!allowedByTimeRestrictions(allowedTimeString, deniedTimeString))
+ throw new TranslatableInvalidTimeLoginException("User \""
+ + authenticatedUser.getIdentifier()
+ + "\" is not allowed to log in at this time.",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW");
+
+ // Verify host-based restrictions specific to the user
+ String allowedHostString = userAttributes.get(RestrictUser.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String deniedHostString = userAttributes.get(RestrictUser.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ if (!allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress))
+ throw new TranslatableInvalidHostLoginException("User \""
+ + authenticatedUser.getIdentifier()
+ +"\" is not allowed to log in from \""
+ + remoteAddress + "\"",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST");
+
+ // Gather user's effective groups.
+ Set userGroups = authenticatedUser.getEffectiveUserGroups();
+ Directory directoryGroups = context.getPrivileged().getUserGroupDirectory();
+
+ // Loop user's effective groups and verify restrictions
+ for (String userGroup : userGroups) {
+ UserGroup thisGroup = directoryGroups.get(userGroup);
+ if (thisGroup == null) {
+ continue;
+ }
+
+ // Get group's attributes
+ Map grpAttributes = thisGroup.getAttributes();
+
+ // Pull time-based restrictions for this group and verify
+ String grpAllowedTimeString = grpAttributes.get(RestrictUserGroup.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String grpDeniedTimeString = grpAttributes.get(RestrictUserGroup.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ if (!allowedByTimeRestrictions(grpAllowedTimeString, grpDeniedTimeString))
+ throw new TranslatableInvalidTimeLoginException("User \""
+ + authenticatedUser.getIdentifier()
+ +"\" is not allowed to log in at this time due to restrictions on group \""
+ + userGroup + "\".",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW");
+
+ // Pull host-based restrictions for this group and verify
+ String grpAllowedHostString = grpAttributes.get(RestrictUserGroup.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String grpDeniedHostString = grpAttributes.get(RestrictUserGroup.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ if (!allowedByHostRestrictions(grpAllowedHostString, grpDeniedHostString, remoteAddress))
+ throw new TranslatableInvalidHostLoginException("User \""
+ + authenticatedUser.getIdentifier()
+ + "\" is not allowed to log in from this host due to restrictions on group \""
+ + userGroup + "\".",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST");
+
+ }
+
+ }
+
+ /**
+ * Verifies the connection restrictions supported by this extension for the
+ * connection the user is attempting to access, throwing an exception if
+ * any of the restrictions result in the connection being unavailable.
+ *
+ * @param connectionAttributes
+ * The attributes of the connection that may contain any additional
+ * restrictions on use of the connection.
+ *
+ * @param remoteAddress
+ * The remote IP address of the user trying to access the connection.
+ *
+ * @throws GuacamoleException
+ * If any of the restrictions should prevent the connection from being
+ * used by the user at the current time.
+ */
+ public static void verifyConnectionRestrictions(
+ Map connectionAttributes, String remoteAddress)
+ throws GuacamoleException {
+
+ // Verify time-based restrictions specific to this connection.
+ String allowedTimeString = connectionAttributes.get(RestrictConnection.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String deniedTimeString = connectionAttributes.get(RestrictConnection.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ if (!allowedByTimeRestrictions(allowedTimeString, deniedTimeString))
+ throw new TranslatableGuacamoleSecurityException(
+ "Use of this connection is not allowed at this time.",
+ "RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW"
+ );
+
+ // Verify host-based restrictions specific to this connection.
+ String allowedHostString = connectionAttributes.get(RestrictConnection.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String deniedHostString = connectionAttributes.get(RestrictConnection.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ if (!allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress))
+ throw new TranslatableGuacamoleSecurityException(
+ "Use of this connection is not allowed from this remote host: \"" + remoteAddress + "\".",
+ "RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW"
+ );
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidHostConnectionException.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidHostConnectionException.java
new file mode 100644
index 0000000000..285e769c7d
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidHostConnectionException.java
@@ -0,0 +1,73 @@
+/*
+ * 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.restrict;
+
+import org.apache.guacamole.language.TranslatableGuacamoleSecurityException;
+import org.apache.guacamole.language.TranslatableMessage;
+
+/**
+ * An exception that represents an invalid login or connection due to
+ * restrictions based on the host from which the action should be allowed.
+ */
+public class TranslatableInvalidHostConnectionException
+ extends TranslatableGuacamoleSecurityException {
+
+ /**
+ * The serial version ID of this class.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new host-based connection exception with the given message and
+ * translation string that can be processed by Guacamole's translation
+ * service.
+ *
+ * @param message
+ * The non-translatable, human-readable message containing details
+ * of the exception.
+ *
+ * @param translatableMessage
+ * A translatable, human-readable description of the exception that
+ * occurred.
+ */
+ public TranslatableInvalidHostConnectionException(String message,
+ TranslatableMessage translatableMessage) {
+ super(message, translatableMessage);
+ }
+
+ /**
+ * Create a new host-based connection exception with the given message and
+ * translation string that can be processed by Guacamole's translation
+ * service.
+ *
+ * @param message
+ * The non-translatable, human-readable message containing details
+ * of the exception.
+ *
+ * @param translationKey
+ * The arbitrary key which can be used to look up the message to be
+ * displayed in the user's native language.
+ */
+ public TranslatableInvalidHostConnectionException(String message,
+ String translationKey) {
+ super(message, new TranslatableMessage(translationKey));
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidHostLoginException.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidHostLoginException.java
new file mode 100644
index 0000000000..227710948b
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidHostLoginException.java
@@ -0,0 +1,72 @@
+/*
+ * 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.restrict;
+
+import org.apache.guacamole.language.TranslatableGuacamoleClientException;
+import org.apache.guacamole.language.TranslatableMessage;
+
+/**
+ * An exception that represents an invalid login or connection due to
+ * restrictions based on the host from which the action should be allowed.
+ */
+public class TranslatableInvalidHostLoginException
+ extends TranslatableGuacamoleClientException {
+
+ /**
+ * The serial version ID of this class.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new host-based login exception with the given message and
+ * translation string that can be processed by Guacamole's translation
+ * service.
+ *
+ * @param message
+ * The non-translatable, human-readable message containing details
+ * of the exception.
+ *
+ * @param translatableMessage
+ * A translatable, human-readable description of the exception that
+ * occurred.
+ */
+ public TranslatableInvalidHostLoginException(String message,
+ TranslatableMessage translatableMessage) {
+ super(message, translatableMessage);
+ }
+
+ /**
+ * Create a new host-based login exception with the given message and
+ * translation string that can be processed by Guacamole's translation
+ * service.
+ *
+ * @param message
+ * The non-translatable, human-readable message containing details
+ * of the exception.
+ *
+ * @param translationKey
+ * The arbitrary key which can be used to look up the message to be
+ * displayed in the user's native language.
+ */
+ public TranslatableInvalidHostLoginException(String message, String translationKey) {
+ super(message, new TranslatableMessage(translationKey));
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidTimeConnectionException.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidTimeConnectionException.java
new file mode 100644
index 0000000000..f9158b8ee7
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidTimeConnectionException.java
@@ -0,0 +1,73 @@
+/*
+ * 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.restrict;
+
+import org.apache.guacamole.language.TranslatableGuacamoleSecurityException;
+import org.apache.guacamole.language.TranslatableMessage;
+
+/**
+ * An exception that represents an invalid login due to restrictions based
+ * on the time of day and day of week the user is allowed to log in.
+ */
+public class TranslatableInvalidTimeConnectionException
+ extends TranslatableGuacamoleSecurityException {
+
+ /**
+ * The serial version ID of this class.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new time-based login exception with the given message and
+ * translation string that can be processed by Guacamole's translation
+ * service.
+ *
+ * @param message
+ * The non-translatable, human-readable message containing details
+ * of the exception.
+ *
+ * @param translatableMessage
+ * A translatable, human-readable description of the exception that
+ * occurred.
+ */
+ public TranslatableInvalidTimeConnectionException(String message,
+ TranslatableMessage translatableMessage) {
+ super(message, translatableMessage);
+ }
+
+ /**
+ * Create a new time-based login exception with the given message and
+ * translation string that can be processed by Guacamole's translation
+ * service.
+ *
+ * @param message
+ * The non-translatable, human-readable message containing details
+ * of the exception.
+ *
+ * @param translationKey
+ * The arbitrary key which can be used to look up the message to be
+ * displayed in the user's native language.
+ */
+ public TranslatableInvalidTimeConnectionException(String message,
+ String translationKey) {
+ super(message, new TranslatableMessage(translationKey));
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidTimeLoginException.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidTimeLoginException.java
new file mode 100644
index 0000000000..7533a859b0
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidTimeLoginException.java
@@ -0,0 +1,73 @@
+/*
+ * 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.restrict;
+
+import org.apache.guacamole.language.TranslatableGuacamoleClientException;
+import org.apache.guacamole.language.TranslatableMessage;
+
+/**
+ * An exception that represents an invalid login due to restrictions based
+ * on the time of day and day of week the user is allowed to log in.
+ */
+public class TranslatableInvalidTimeLoginException
+ extends TranslatableGuacamoleClientException {
+
+ /**
+ * The serial version ID of this class.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new time-based login exception with the given message and
+ * translation string that can be processed by Guacamole's translation
+ * service.
+ *
+ * @param message
+ * The non-translatable, human-readable message containing details
+ * of the exception.
+ *
+ * @param translatableMessage
+ * A translatable, human-readable description of the exception that
+ * occurred.
+ */
+ public TranslatableInvalidTimeLoginException(String message,
+ TranslatableMessage translatableMessage) {
+ super(message, translatableMessage);
+ }
+
+ /**
+ * Create a new time-based login exception with the given message and
+ * translation string that can be processed by Guacamole's translation
+ * service.
+ *
+ * @param message
+ * The non-translatable, human-readable message containing details
+ * of the exception.
+ *
+ * @param translationKey
+ * The arbitrary key which can be used to look up the message to be
+ * displayed in the user's native language.
+ */
+ public TranslatableInvalidTimeLoginException(String message,
+ String translationKey) {
+ super(message, new TranslatableMessage(translationKey));
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictConnection.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictConnection.java
new file mode 100644
index 0000000000..b997bb002a
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictConnection.java
@@ -0,0 +1,186 @@
+/*
+ * 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.restrict.connection;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.restrict.RestrictionVerificationService;
+import org.apache.guacamole.auth.restrict.form.HostRestrictionField;
+import org.apache.guacamole.auth.restrict.form.TimeRestrictionField;
+import org.apache.guacamole.form.Form;
+import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.DelegatingConnection;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
+
+/**
+ * A Connection implementation that wraps another connection, providing additional
+ * ability to restrict the time that the connection can be accessed and the
+ * remote addresses allowed to use the connection.
+ */
+public class RestrictConnection extends DelegatingConnection {
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times that
+ * this connection can be accessed. The presence of values within this
+ * attribute will automatically restrict use of the connections at any
+ * times that are not specified.
+ */
+ public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed";
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times that
+ * this connection cannot be accessed. Denied times will always take precedence
+ * over allowed times. The presence of this attribute without
+ * guac-restrict-time-allowed will deny access only during the times listed
+ * in this attribute, allowing access at all other times. The presence of
+ * this attribute along with the guac-restrict-time-allowed attribute will
+ * deny access at any times that overlap with the allowed times.
+ */
+ public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied";
+
+ /**
+ * The name of the attribute that contains a list of hosts from which a user
+ * may access this connection. The presence of this attribute will restrict
+ * access to only users accessing Guacamole from the list of hosts contained
+ * in the attribute, subject to further restriction by the
+ * guac-restrict-hosts-denied attribute.
+ */
+ public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed";
+
+ /**
+ * The name of the attribute that contains a list of hosts from which
+ * a user may not access this connection. The presence of this attribute,
+ * absent the guac-restrict-hosts-allowed attribute, will allow access from
+ * all hosts except the ones listed in this attribute. The presence of this
+ * attribute coupled with the guac-restrict-hosts-allowed attribute will
+ * block access from any IPs in this list, overriding any that may be
+ * allowed.
+ */
+ public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied";
+
+ /**
+ * The list of all connection attributes provided by this Connection implementation.
+ */
+ public static final List RESTRICT_CONNECTION_ATTRIBUTES = Arrays.asList(
+ RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME,
+ RESTRICT_TIME_DENIED_ATTRIBUTE_NAME,
+ RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME,
+ RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME
+ );
+
+ /**
+ * The form containing the list of fields for the attributes provided
+ * by this module.
+ */
+ public static final Form RESTRICT_CONNECTION_FORM = new Form("restrict-login-form",
+ Arrays.asList(
+ new TimeRestrictionField(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME),
+ new TimeRestrictionField(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME),
+ new HostRestrictionField(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME),
+ new HostRestrictionField(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME)
+ )
+ );
+
+ /**
+ * The remote address from which the user attempting to access this
+ * connection logged in.
+ */
+ private final String remoteAddress;
+
+ /**
+ * Wraps the given Connection object, providing capability of further
+ * restricting connection access beyond the default access control provided
+ * by other modules.
+ *
+ * @param connection
+ * The Connection object to wrap.
+ *
+ * @param remoteAddress
+ * The remote address from which the user attempting to access this
+ * connection logged in.
+ */
+ public RestrictConnection(Connection connection, String remoteAddress) {
+ super(connection);
+ this.remoteAddress = remoteAddress;
+ }
+
+ /**
+ * Returns the original Connection object wrapped by this RestrictConnection.
+ *
+ * @return
+ * The wrapped Connection object.
+ */
+ public Connection getUndecorated() {
+ return getDelegateConnection();
+ }
+
+ @Override
+ public Map getAttributes() {
+
+ // Create independent, mutable copy of attributes
+ Map attributes = new HashMap<>(super.getAttributes());
+
+ // Loop through extension-specific attributes and add them where no
+ // values exist, so that they show up in the web UI.
+ for (String attribute : RESTRICT_CONNECTION_ATTRIBUTES) {
+ String value = attributes.get(attribute);
+ if (value == null || value.isEmpty())
+ attributes.put(attribute, null);
+ }
+
+ return attributes;
+
+ }
+
+ @Override
+ public void setAttributes(Map attributes) {
+
+ // Create independent, mutable copy of attributes
+ attributes = new HashMap<>(attributes);
+
+ // Loop through extension-specific attributes, only sending ones
+ // that are non-null and non-empty to the underlying storage mechanism.
+ for (String attribute : RESTRICT_CONNECTION_ATTRIBUTES) {
+ String value = attributes.get(attribute);
+ if (value != null && value.isEmpty())
+ attributes.put(attribute, null);
+ }
+
+ super.setAttributes(attributes);
+
+ }
+
+ @Override
+ public GuacamoleTunnel connect(GuacamoleClientInformation info,
+ Map tokens) throws GuacamoleException {
+
+ // Verify the restrictions for this connection.
+ RestrictionVerificationService.verifyConnectionRestrictions(getAttributes(), remoteAddress);
+
+ // Connect
+ return super.connect(info, tokens);
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictConnectionGroup.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictConnectionGroup.java
new file mode 100644
index 0000000000..20e1ce29c2
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictConnectionGroup.java
@@ -0,0 +1,187 @@
+/*
+ * 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.restrict.connectiongroup;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.restrict.RestrictionVerificationService;
+import org.apache.guacamole.auth.restrict.form.HostRestrictionField;
+import org.apache.guacamole.auth.restrict.form.TimeRestrictionField;
+import org.apache.guacamole.form.Form;
+import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.DelegatingConnectionGroup;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
+
+/**
+ * A ConnectionGroup implementation that wraps another connection, providing
+ * additional ability to restrict the time that the connection group can be
+ * accessed and the remote addresses allowed to use the connection group.
+ */
+public class RestrictConnectionGroup extends DelegatingConnectionGroup {
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times that
+ * this connection group can be accessed. The presence of values within this
+ * attribute will automatically restrict use of the connections at any
+ * times that are not specified.
+ */
+ public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed";
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times that
+ * this connection group cannot be accessed. Denied times will always take
+ * precedence over allowed times. The presence of this attribute without
+ * guac-restrict-time-allowed will deny access only during the times listed
+ * in this attribute, allowing access at all other times. The presence of
+ * this attribute along with the guac-restrict-time-allowed attribute will
+ * deny access at any times that overlap with the allowed times.
+ */
+ public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied";
+
+ /**
+ * The name of the attribute that contains a list of hosts from which a user
+ * may access this connection group. The presence of this attribute will
+ * restrict access to only users accessing Guacamole from the list of hosts
+ * contained in the attribute, subject to further restriction by the
+ * guac-restrict-hosts-denied attribute.
+ */
+ public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed";
+
+ /**
+ * The name of the attribute that contains a list of hosts from which
+ * a user may not access this connection group. The presence of this
+ * attribute, absent the guac-restrict-hosts-allowed attribute, will allow
+ * access from all hosts except the ones listed in this attribute. The
+ * presence of this attribute coupled with the guac-restrict-hosts-allowed
+ * attribute will block access from any hosts in this list, overriding any
+ * that may be allowed.
+ */
+ public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied";
+
+ /**
+ * The list of all connection group attributes provided by this
+ * ConnectionGroup implementation.
+ */
+ public static final List RESTRICT_CONNECTIONGROUP_ATTRIBUTES = Arrays.asList(
+ RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME,
+ RESTRICT_TIME_DENIED_ATTRIBUTE_NAME,
+ RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME,
+ RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME
+ );
+
+ /**
+ * The form containing the list of fields for the attributes provided
+ * by this module.
+ */
+ public static final Form RESTRICT_CONNECTIONGROUP_FORM = new Form("restrict-login-form",
+ Arrays.asList(
+ new TimeRestrictionField(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME),
+ new TimeRestrictionField(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME),
+ new HostRestrictionField(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME),
+ new HostRestrictionField(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME)
+ )
+ );
+
+ /**
+ * The remote address from which the user accessing this connection logged in.
+ */
+ private final String remoteAddress;
+
+ /**
+ * Wraps the given Connection object, providing capability of further
+ * restricting connection access beyond the default access control provided
+ * by other modules.
+ *
+ * @param connectionGroup
+ * The ConnectionGroup object to wrap.
+ *
+ * @param remoteAddress
+ * The remote address from which the user accessing this connection
+ * logged in.
+ */
+ public RestrictConnectionGroup(ConnectionGroup connectionGroup, String remoteAddress) {
+ super(connectionGroup);
+ this.remoteAddress = remoteAddress;
+ }
+
+ /**
+ * Returns the original ConnectionGroup object wrapped by this
+ * RestrictConnectionGroup.
+ *
+ * @return
+ * The wrapped ConnectionGroup object.
+ */
+ public ConnectionGroup getUndecorated() {
+ return getDelegateConnectionGroup();
+ }
+
+ @Override
+ public Map getAttributes() {
+
+ // Create independent, mutable copy of attributes
+ Map attributes = new HashMap<>(super.getAttributes());
+
+ // Loop through extension-specific attributes and add them where no
+ // values exist, so that they show up in the web UI.
+ for (String attribute : RESTRICT_CONNECTIONGROUP_ATTRIBUTES) {
+ String value = attributes.get(attribute);
+ if (value == null || value.isEmpty())
+ attributes.put(attribute, null);
+ }
+
+ return attributes;
+
+ }
+
+ @Override
+ public void setAttributes(Map attributes) {
+
+ // Create independent, mutable copy of attributes
+ attributes = new HashMap<>(attributes);
+
+ // Loop through extension-specific attributes, only sending ones
+ // that are non-null and non-empty to the underlying storage mechanism.
+ for (String attribute : RESTRICT_CONNECTIONGROUP_ATTRIBUTES) {
+ String value = attributes.get(attribute);
+ if (value != null && value.isEmpty())
+ attributes.put(attribute, null);
+ }
+
+ super.setAttributes(attributes);
+
+ }
+
+ @Override
+ public GuacamoleTunnel connect(GuacamoleClientInformation info,
+ Map tokens) throws GuacamoleException {
+
+ // Verify restrictions for this connection group.
+ RestrictionVerificationService.verifyConnectionRestrictions(getAttributes(), remoteAddress);
+
+ // Connect
+ return super.connect(info, tokens);
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/HostRestrictionField.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/HostRestrictionField.java
new file mode 100644
index 0000000000..f89c82d6ed
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/HostRestrictionField.java
@@ -0,0 +1,47 @@
+/*
+ * 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.restrict.form;
+
+import org.apache.guacamole.form.Field;
+
+/**
+ * A field that parses out a string of semi-colon separated hosts into
+ * individual entries that can be managed more easily in a web interface.
+ */
+public class HostRestrictionField extends Field {
+
+ /**
+ * The field type.
+ */
+ public static final String FIELD_TYPE = "GUAC_HOST_RESTRICTION";
+
+ /**
+ * Create a new field that tracks host restrictions.
+ *
+ * @param name
+ * The name of the parameter that will be used to pass this field
+ * between the REST API and the web front-end.
+ *
+ */
+ public HostRestrictionField(String name) {
+ super(name, FIELD_TYPE);
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/TimeRestrictionField.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/TimeRestrictionField.java
new file mode 100644
index 0000000000..a3ace4f2c2
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/TimeRestrictionField.java
@@ -0,0 +1,49 @@
+/*
+ * 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.restrict.form;
+
+import org.apache.guacamole.form.Field;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A field that parses a string containing time restrictions into its individual
+ * components for user-friendly display on the web interface.
+ */
+public class TimeRestrictionField extends Field {
+
+ /**
+ * The field type.
+ */
+ public static final String FIELD_TYPE = "GUAC_TIME_RESTRICTION";
+
+ /**
+ * Create a new field that tracks time restrictions.
+ *
+ * @param name
+ * The name of the parameter that will be used to pass this field
+ * between the REST API and the web front-end.
+ *
+ */
+ public TimeRestrictionField(String name) {
+ super(name, FIELD_TYPE);
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUser.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUser.java
new file mode 100644
index 0000000000..6ad64721e2
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUser.java
@@ -0,0 +1,158 @@
+/*
+ * 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.restrict.user;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.guacamole.auth.restrict.form.HostRestrictionField;
+import org.apache.guacamole.auth.restrict.form.TimeRestrictionField;
+import org.apache.guacamole.form.Form;
+import org.apache.guacamole.net.auth.DelegatingUser;
+import org.apache.guacamole.net.auth.User;
+
+/**
+ * User implementation which wraps a User from another extension and enforces
+ * additional restrictions.
+ */
+public class RestrictUser extends DelegatingUser {
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times that
+ * a user is allowed to log in. The presence of this attribute will restrict
+ * the user to logins only during the times that are contained within the
+ * attribute, subject to further restriction by the guac-restrict-time-denied
+ * attribute.
+ */
+ public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed";
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times that
+ * a user is not allowed to log in. Denied times will always take precedence
+ * over allowed times. The presence of this attribute without
+ * guac-restrict-time-allowed will deny logins only during the times listed
+ * in this attribute, allowing logins at all other times. The presence of
+ * this attribute along with the guac-restrict-time-allowed attribute will
+ * deny logins at any times that overlap with the allowed times.
+ */
+ public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied";
+
+ /**
+ * The name of the attribute that contains a list of IP addresses from which
+ * a user is allowed to log in. The presence of this attribute will restrict
+ * users to only the list of IP addresses contained in the attribute, subject
+ * to further restriction by the guac-restrict-hosts-denied attribute.
+ */
+ public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed";
+
+ /**
+ * The name of the attribute that contains a list of IP addresses from which
+ * a user is not allowed to log in. The presence of this attribute, absent
+ * the guac-restrict-hosts-allowed attribute, will allow logins from all
+ * hosts except the ones listed in this attribute. The presence of this
+ * attribute coupled with the guac-restrict-hosts-allowed attribute will
+ * block access from any IPs in this list, overriding any that may be
+ * allowed.
+ */
+ public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied";
+
+ /**
+ * The list of all user attributes provided by this User implementation.
+ */
+ public static final List RESTRICT_USER_ATTRIBUTES = Arrays.asList(
+ RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME,
+ RESTRICT_TIME_DENIED_ATTRIBUTE_NAME,
+ RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME,
+ RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME
+ );
+
+ /**
+ * The form containing the list of fields for the attributes provided
+ * by this module.
+ */
+ public static final Form RESTRICT_LOGIN_FORM = new Form("restrict-login-form",
+ Arrays.asList(
+ new TimeRestrictionField(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME),
+ new TimeRestrictionField(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME),
+ new HostRestrictionField(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME),
+ new HostRestrictionField(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME)
+ )
+ );
+
+
+ /**
+ * Wraps the given User object, providing capability of further restricting
+ * logins beyond the default restrictions provided by default modules.
+ *
+ * @param user
+ * The User object to wrap.
+ */
+ public RestrictUser(User user) {
+ super(user);
+ }
+
+ /**
+ * Returns the User object wrapped by this RestrictUser.
+ *
+ * @return
+ * The wrapped User object.
+ */
+ public User getUndecorated() {
+ return getDelegateUser();
+ }
+
+ @Override
+ public Map getAttributes() {
+
+ // Create independent, mutable copy of attributes
+ Map attributes = new HashMap<>(super.getAttributes());
+
+ // Loop through extension-specific attributes, adding ones that are
+ // empty so that they are displayed in the web UI.
+ for (String attribute : RESTRICT_USER_ATTRIBUTES) {
+ String value = attributes.get(attribute);
+ if (value == null || value.isEmpty())
+ attributes.put(attribute, null);
+ }
+
+ return attributes;
+
+ }
+
+ @Override
+ public void setAttributes(Map attributes) {
+
+ // Create independent, mutable copy of attributes
+ attributes = new HashMap<>(attributes);
+
+ // Loop through extension-specific attributes, only sending ones
+ // that are non-null and non-empty to the underlying storage mechanism.
+ for (String attribute : RESTRICT_USER_ATTRIBUTES) {
+ String value = attributes.get(attribute);
+ if (value != null && value.isEmpty())
+ attributes.put(attribute, null);
+ }
+
+ super.setAttributes(attributes);
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUserContext.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUserContext.java
new file mode 100644
index 0000000000..dbf74aa794
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUserContext.java
@@ -0,0 +1,166 @@
+/*
+ * 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.restrict.user;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.restrict.connection.RestrictConnection;
+import org.apache.guacamole.auth.restrict.connectiongroup.RestrictConnectionGroup;
+import org.apache.guacamole.auth.restrict.usergroup.RestrictUserGroup;
+import org.apache.guacamole.form.Form;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.ConnectionGroup;
+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;
+
+/**
+ * A UserContext implementation for additional login restrictions which wraps
+ * the UserContext of some other extension, allowing for administrators to have
+ * additional controls over user login situations.
+ */
+public class RestrictUserContext extends DelegatingUserContext {
+
+ /**
+ * The remote address from which this user logged in.
+ */
+ String remoteAddress;
+
+ /**
+ * Creates a new RestrictUserContext which wraps the given UserContext,
+ * providing additional control for user logins.
+ *
+ * @param userContext
+ * The UserContext to wrap.
+ *
+ * @param remoteAddress
+ * The address the user is logging in from, if known.
+ */
+ public RestrictUserContext(UserContext userContext, String remoteAddress) {
+ super(userContext);
+ this.remoteAddress = remoteAddress;
+ }
+
+ @Override
+ public Directory getConnectionDirectory() throws GuacamoleException {
+ return new DecoratingDirectory(super.getConnectionDirectory()) {
+
+ @Override
+ protected Connection decorate(Connection object) {
+ return new RestrictConnection(object, remoteAddress);
+ }
+
+ @Override
+ protected Connection undecorate(Connection object) {
+ assert(object instanceof RestrictConnection);
+ return ((RestrictConnection) object).getUndecorated();
+ }
+
+ };
+ }
+
+ @Override
+ public Collection