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..b011568d6d
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/pom.xml
@@ -0,0 +1,179 @@
+
+
+
+
+ 4.0.0
+ org.apache.guacamole
+ guacamole-auth-restrict
+ jar
+ 1.6.0
+ guacamole-auth-restrict
+ http://guacamole.apache.org/
+
+
+ org.apache.guacamole
+ extensions
+ 1.6.0
+ ../
+
+
+
+
+
+
+
+ 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.6.0
+ 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
+
+
+
+
+ com.github.seancfoley
+ ipaddress
+ 5.5.0
+ provided
+
+
+
+
+
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/Restrictable.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/Restrictable.java
new file mode 100644
index 0000000000..ff1acf7450
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/Restrictable.java
@@ -0,0 +1,67 @@
+/*
+ * 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.calendar.RestrictionType;
+import org.apache.guacamole.net.auth.Attributes;
+
+/**
+ * An interface which defines methods that apply to items that can have
+ * restrictions applied to them.
+ */
+public interface Restrictable extends Attributes {
+
+ /**
+ * Return the restriction state for this restrictable object at the
+ * current date and time. By default returns an implicit denial.
+ *
+ * @return
+ * The restriction status for the current date and time.
+ */
+ default public RestrictionType getCurrentTimeRestriction() {
+ return RestrictionType.IMPLICIT_DENY;
+ }
+
+ /**
+ * Return the restriction state for this restrictable object for the host
+ * from which the current user is logged in. By default returns an implicit
+ * denial.
+ *
+ * @return
+ * The restriction status for the host from which the current user is
+ * logged in.
+ */
+ default public RestrictionType getCurrentHostRestriction() {
+ return RestrictionType.IMPLICIT_DENY;
+ }
+
+ /**
+ * Returns true if the current item is available based on the restrictions
+ * for the given implementation of this interface, or false if the item is
+ * not currently available. The default implementation checks current time
+ * and host restrictions, allowing if both those restrictions allow access.
+ *
+ * @return
+ * true if the item is available, otherwise false.
+ */
+ default public boolean isAvailable() {
+ return (getCurrentTimeRestriction().isAllowed() && getCurrentHostRestriction().isAllowed());
+ }
+
+}
\ No newline at end of file
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..ec6236432b
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionAuthenticationProvider.java
@@ -0,0 +1,59 @@
+/*
+ * 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.RestrictedUserContext;
+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 {
+
+ String remoteAddress = credentials.getRemoteAddress();
+
+ // Verify identity of user
+ RestrictionVerificationService.verifyLoginRestrictions(context,
+ authenticatedUser.getEffectiveUserGroups(), remoteAddress);
+
+ // User has been verified, and authentication should be allowed to
+ // continue
+ return new RestrictedUserContext(context, remoteAddress,
+ authenticatedUser.getEffectiveUserGroups());
+
+ }
+
+}
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..c0666cd7db
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java
@@ -0,0 +1,538 @@
+/*
+ * 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.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.restrict.connection.RestrictedConnection;
+import org.apache.guacamole.auth.restrict.user.RestrictedUser;
+import org.apache.guacamole.auth.restrict.usergroup.RestrictedUserGroup;
+import org.apache.guacamole.calendar.DailyRestriction;
+import org.apache.guacamole.calendar.RestrictionType;
+import org.apache.guacamole.calendar.TimeRestrictionParser;
+import org.apache.guacamole.host.HostRestrictionParser;
+import org.apache.guacamole.language.TranslatableGuacamoleSecurityException;
+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.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, and returning the appropriate restriction type.
+ *
+ * @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
+ * A RestrictionType based on the provided allowed and denied strings.
+ */
+ public static RestrictionType allowedByTimeRestrictions(String allowedTimeString,
+ String deniedTimeString) {
+
+ // Check for denied entries, first, returning the explicit deny 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 RestrictionType.EXPLICIT_DENY;
+ }
+ }
+
+ // If no allowed entries are present, return the implicit allow, allowing
+ // the login or connection to continue.
+ if (allowedTimeString == null || allowedTimeString.isEmpty())
+ return RestrictionType.IMPLICIT_ALLOW;
+
+ // Pull the list of allowed times.
+ 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 the explicit allow.
+ if (restriction.appliesNow())
+ return RestrictionType.EXPLICIT_ALLOW;
+ }
+
+ // We have allowed entries, but login hasn't matched, so implicitly deny it.
+ return RestrictionType.IMPLICIT_DENY;
+
+ }
+
+ /**
+ * Given the strings of allowed and denied hosts, verify that the login or
+ * connection should be allowed from the given remote address, returning
+ * the RestrictionType that matches the provided allowed and denied strings.
+ *
+ * @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
+ * A RestrictionType that matches the provided allow and deny strings.
+ */
+ public static RestrictionType allowedByHostRestrictions(String allowedHostsString,
+ String deniedHostsString, String remoteAddress) {
+
+ // Convert the string to a HostName
+ 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 RestrictionType.IMPLICIT_ALLOW;
+
+ // 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.warn("Host-based restrictions are present, but the remote "
+ + "address is invalid or could not be resolved. "
+ + "The action will not be allowed.");
+ return RestrictionType.IMPLICIT_DENY;
+ }
+
+ // 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 RestrictionType.EXPLICIT_DENY;
+
+ else
+ for (IPAddress currAddr : hostName.toAllAddresses())
+ if (currAddr.matches(remoteHostName.asAddressString()))
+ return RestrictionType.EXPLICIT_DENY;
+ }
+ catch (UnknownHostException | HostNameException e) {
+ LOGGER.warn("Unknown or invalid host in denied hosts list: \"{}\"", hostName);
+ LOGGER.debug("Exception while trying to resolve host: \"{}\"", hostName, e);
+ return RestrictionType.IMPLICIT_DENY;
+ }
+ }
+
+ // 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 RestrictionType.IMPLICIT_ALLOW;
+
+ // 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 RestrictionType.EXPLICIT_ALLOW;
+
+ // Entry is a hostname, so resolve to IPs and check each one
+ for (IPAddress currAddr : hostName.toAllAddresses())
+ if (currAddr.matches(remoteHostName.asAddressString()))
+ return RestrictionType.EXPLICIT_ALLOW;
+
+ }
+ // 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 RestrictionType.IMPLICIT_DENY;
+
+ }
+
+ /**
+ * Verify the host restrictions for the user associated with the given
+ * UserContext, throwing an exception if any of the restrictions result
+ * in the user not being allowed to be logged in to Guacamole from this
+ * host.
+ *
+ * @param context
+ * The UserContext associated with the user who is being verified.
+ *
+ * @param effectiveUserGroups
+ * The set of identifiers of groups of which the user who is being
+ * verified is a member.
+ *
+ * @param remoteAddress
+ * The remote address of the client from which the current user is
+ * logged in.
+ *
+ * @throws GuacamoleException
+ * If the restrictions on the user should prevent the user from
+ * logging in from the current client, or if an error occurs attempting
+ * to retrieve permissions.
+ */
+ public static void verifyHostRestrictions(UserContext context,
+ Set effectiveUserGroups, String remoteAddress)
+ throws GuacamoleException {
+
+ // Get the current user
+ User currentUser = context.self();
+
+ // Admins always have access.
+ if (currentUser.getEffectivePermissions().getSystemPermissions().hasPermission(SystemPermission.Type.ADMINISTER)) {
+ LOGGER.warn("User \"{}\" has System Administration permissions; additional restrictions will be bypassed.",
+ currentUser.getIdentifier());
+ return;
+ }
+
+ // Get user's attributes
+ Map userAttributes = currentUser.getAttributes();
+
+ // Verify host-based restrictions specific to the user
+ String allowedHostString = userAttributes.get(RestrictedUser.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String deniedHostString = userAttributes.get(RestrictedUser.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ RestrictionType hostRestrictionResult = allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress);
+
+ switch (hostRestrictionResult) {
+ // User-level explicit deny overrides everything
+ case EXPLICIT_DENY:
+ throw new TranslatableInvalidHostLoginException("User \""
+ + currentUser.getIdentifier()
+ +"\" is not allowed to log in from \""
+ + remoteAddress + "\"",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"
+ );
+
+ // User-level explicit allow means the user is allowed.
+ case EXPLICIT_ALLOW:
+ return;
+
+ }
+
+ // Gather user's effective groups.
+ Collection userGroups = context
+ .getPrivileged()
+ .getUserGroupDirectory()
+ .getAll(effectiveUserGroups);
+
+ // Loop user's effective groups and verify restrictions
+ for (UserGroup userGroup : userGroups) {
+
+ // Get group's attributes
+ Map grpAttributes = userGroup.getAttributes();
+
+ // Pull host-based restrictions for this group and verify
+ String grpAllowedHostString = grpAttributes.get(RestrictedUserGroup.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String grpDeniedHostString = grpAttributes.get(RestrictedUserGroup.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ RestrictionType grpRestrictionResult = allowedByHostRestrictions(grpAllowedHostString, grpDeniedHostString, remoteAddress);
+
+ // Any explicit denials are thrown immediately
+ if (grpRestrictionResult == RestrictionType.EXPLICIT_DENY)
+ throw new TranslatableInvalidHostLoginException("User \""
+ + currentUser.getIdentifier()
+ + "\" is not allowed to log in from host \""
+ + remoteAddress
+ + "\" due to restrictions on group \""
+ + userGroup.getIdentifier() + "\".",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"
+ );
+
+ // Compare the two, returning the highest-priority restriction so far.
+ hostRestrictionResult = RestrictionType.getHigherPriority(hostRestrictionResult, grpRestrictionResult);
+
+ }
+
+ // Check the result and log allowed
+ switch (hostRestrictionResult) {
+ // Explicit allow was the highest result, so we log it and return, allowing the user to be logged in.
+ case EXPLICIT_ALLOW:
+ return;
+
+ // Implicit allow was the highest result, so we log it and return, allowing the user to be logged in.
+ case IMPLICIT_ALLOW:
+ return;
+ }
+
+ // If we reach, here, we've reached an implict deny, so we throw an exception.
+ throw new TranslatableInvalidHostLoginException("User \""
+ + currentUser.getIdentifier()
+ + "\" is implicitly denied at this time.",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"
+ );
+
+ }
+
+ /**
+ * Verify the host-based restrictions of the Connection, throwing an
+ * exception if the Connection should be allowed from the host from which
+ * the user is logged in.
+ *
+ * @param restrictable
+ * The Restrictable object that should be verified against host restrictions.
+ *
+ * @param remoteAddress
+ * The remote address of the client from which the current user is
+ * logged in.
+ *
+ * @throws GuacamoleException
+ * If the connection should not be allowed from the remote host from
+ * which the user is logged in.
+ */
+ public static void verifyHostRestrictions(Restrictable restrictable,
+ String remoteAddress) throws GuacamoleException {
+
+ // Verify time-based restrictions specific to this connection.
+ String allowedHostsString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String deniedHostsString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ RestrictionType hostRestrictionResult = allowedByHostRestrictions(allowedHostsString, deniedHostsString, remoteAddress);
+
+ // If the host is not allowed
+ if (!hostRestrictionResult.isAllowed())
+ throw new TranslatableGuacamoleSecurityException(
+ "Use of this connection is not allowed from this remote host: \"" + remoteAddress + "\".",
+ "RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW"
+ );
+
+ }
+
+ /**
+ * Verifies the time restrictions for this extension and whether or not the
+ * account should be allowed to be logged in to Guacamole at the current
+ * day and time, throwing an exception if any of the restrictions result
+ * in a violation of the time constraints of the account.
+ *
+ * @param context
+ * The UserContext of the user whose access to Guacamole is being
+ * checked.
+ *
+ * @param effectiveUserGroups
+ * The set of identifiers of groups of which the user who is being
+ * verified is a member.
+ *
+ * @throws GuacamoleException
+ * If any of the time constraints configured for the user result in the
+ * user not being allowed to be logged in to Guacamole, or if errors
+ * occur trying to retrieve permissions or attributes.
+ */
+ public static void verifyTimeRestrictions(UserContext context,
+ Set effectiveUserGroups) throws GuacamoleException {
+
+ // Retrieve the current User object associated with the UserContext
+ User currentUser = context.self();
+
+ // Admins always have access.
+ if (currentUser.getEffectivePermissions().getSystemPermissions().hasPermission(SystemPermission.Type.ADMINISTER)) {
+ LOGGER.warn("User \"{}\" has System Administration permissions; additional restrictions will be bypassed.",
+ currentUser.getIdentifier());
+ return;
+ }
+
+ // Get user's attributes
+ Map userAttributes = currentUser.getAttributes();
+
+ // Verify time-based restrictions specific to the user
+ String allowedTimeString = userAttributes.get(RestrictedUser.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String deniedTimeString = userAttributes.get(RestrictedUser.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ RestrictionType timeRestrictionResult = allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
+
+ // Check the time restriction for explicit results.
+ switch (timeRestrictionResult) {
+ // User-level explicit deny overrides everything
+ case EXPLICIT_DENY:
+ throw new TranslatableInvalidTimeLoginException("User \""
+ + currentUser.getIdentifier()
+ + "\" is not allowed to log in at this time.",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW"
+ );
+
+ // User-level explicit allow means the user is allowed.
+ case EXPLICIT_ALLOW:
+ return;
+
+ }
+
+ // Gather user's effective groups.
+ Collection userGroups = context
+ .getPrivileged()
+ .getUserGroupDirectory()
+ .getAll(effectiveUserGroups);
+
+ // Loop user's effective groups and verify restrictions
+ for (UserGroup userGroup : userGroups) {
+
+ // Get group's attributes
+ Map grpAttributes = userGroup.getAttributes();
+
+ // Pull time-based restrictions for this group and verify
+ String grpAllowedTimeString = grpAttributes.get(RestrictedUserGroup.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String grpDeniedTimeString = grpAttributes.get(RestrictedUserGroup.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ RestrictionType grpRestrictionResult = allowedByTimeRestrictions(grpAllowedTimeString, grpDeniedTimeString);
+
+ // An explicit deny results in immediate denial of the login.
+ if (grpRestrictionResult == RestrictionType.EXPLICIT_DENY)
+ throw new TranslatableInvalidTimeLoginException("User \""
+ + currentUser.getIdentifier()
+ +"\" is not allowed to log in at this time due to restrictions on group \""
+ + userGroup + "\".",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW"
+ );
+
+ // Compare the two, returning the highest-priority restriction so far.
+ timeRestrictionResult = RestrictionType.getHigherPriority(timeRestrictionResult, grpRestrictionResult);
+
+ }
+
+ switch (timeRestrictionResult) {
+ // Explicit allow was the highest result, so we log it and return, allowing the user to be logged in.
+ case EXPLICIT_ALLOW:
+ return;
+
+ // Implicit allow was the highest result, so we log it and return, allowing the user to be logged in.
+ case IMPLICIT_ALLOW:
+ return;
+ }
+
+ // If we reach, here, we've reached an implict deny, so we throw an exception.
+ throw new TranslatableInvalidTimeLoginException("User \""
+ + currentUser.getIdentifier()
+ + "\" is implicitly denied at this time.",
+ "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW"
+ );
+
+ }
+
+ /**
+ * Verify the time restrictions for the given Connection object, throwing
+ * an exception if the connection should not be allowed, or silently
+ * returning if the connection should be allowed.
+ *
+ * @param restrictable
+ * The item that supports restrictions that is to be verified against
+ * the current time.
+ *
+ * @throws GuacamoleException
+ * If the connection should not be allowed at the current time.
+ */
+ public static void verifyTimeRestrictions(Restrictable restrictable) throws GuacamoleException {
+
+ // Verify time-based restrictions specific to this connection.
+ String allowedTimeString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String deniedTimeString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ RestrictionType timeRestriction = allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
+ if (!timeRestriction.isAllowed())
+ throw new TranslatableGuacamoleSecurityException(
+ "Use of this connection or connection group is not allowed at this time.",
+ "RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW"
+ );
+
+ }
+
+ /**
+ * 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 effectiveUserGroups
+ * The identifiers of the UserGroups of which the user who is logging
+ * in is a member.
+ *
+ * @param remoteAddress
+ * The remote address of the client from which the current user is
+ * logged in.
+ *
+ * @throws GuacamoleException
+ * If any of the restrictions should prevent the user from logging in.
+ */
+ public static void verifyLoginRestrictions(UserContext context,
+ Set effectiveUserGroups, String remoteAddress)
+ throws GuacamoleException {
+
+ verifyTimeRestrictions(context, effectiveUserGroups);
+ verifyHostRestrictions(context, effectiveUserGroups, remoteAddress);
+
+ }
+
+ /**
+ * 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 restrictable
+ * The object that supports restrictions that is to be verified to be
+ * usable within the current restrictions.
+ *
+ * @param remoteAddress
+ * The remote address of the client from which the current user is
+ * logged in.
+ *
+ * @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(Restrictable restrictable,
+ String remoteAddress) throws GuacamoleException {
+ verifyTimeRestrictions(restrictable);
+ verifyHostRestrictions(restrictable, remoteAddress);
+ }
+
+}
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/RestrictedConnection.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictedConnection.java
new file mode 100644
index 0000000000..bdbce0bcc4
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictedConnection.java
@@ -0,0 +1,200 @@
+/*
+ * 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.Restrictable;
+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.calendar.RestrictionType;
+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 control access to the connection.
+ */
+public class RestrictedConnection extends DelegatingConnection implements Restrictable {
+
+ /**
+ * The remote address of the client from which the user logged in.
+ */
+ private final String remoteAddress;
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times (UTC)
+ * 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 (UTC)
+ * 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)
+ )
+ );
+
+ /**
+ * 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 of the client from which the current user logged
+ * in.
+ */
+ public RestrictedConnection(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(this, remoteAddress);
+
+ // Connect
+ return super.connect(info, tokens);
+
+ }
+
+ @Override
+ public RestrictionType getCurrentTimeRestriction() {
+ String allowedTimeString = getAttributes().get(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String deniedTimeString = getAttributes().get(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ return RestrictionVerificationService.allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
+ }
+
+ @Override
+ public RestrictionType getCurrentHostRestriction() {
+ String allowedHostString = getAttributes().get(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String deniedHostString = getAttributes().get(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ return RestrictionVerificationService.allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress);
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java
new file mode 100644
index 0000000000..b6c18144ef
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java
@@ -0,0 +1,202 @@
+/*
+ * 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.Restrictable;
+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.calendar.RestrictionType;
+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 an existing ConnectionGroup,
+ * providing additional ability to control access to the ConnectionGroup.
+ */
+public class RestrictedConnectionGroup extends DelegatingConnectionGroup implements Restrictable {
+
+ /**
+ * The remote address of the client from which the current user logged in.
+ */
+ private final String remoteAddress;
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times (UTC)
+ * that this connection group can be accessed. The presence of values within
+ * this attribute will automatically restrict use of the connection group
+ * 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 (UTC)
+ * 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 ConnectionGroup implementation.
+ */
+ 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)
+ )
+ );
+
+ /**
+ * Wraps the given ConnectionGroup object, providing capability of further
+ * restricting connection group access beyond the default access control
+ * provided by other modules.
+ *
+ * @param connectionGroup
+ * The ConnectionGroup object to wrap.
+ *
+ * @param remoteAddress
+ * The remote address of the client from which the current user logged
+ * in.
+ */
+ public RestrictedConnectionGroup(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(this, remoteAddress);
+
+ // Connect
+ return super.connect(info, tokens);
+
+ }
+
+ @Override
+ public RestrictionType getCurrentTimeRestriction() {
+ String allowedTimeString = getAttributes().get(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String deniedTimeString = getAttributes().get(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ return RestrictionVerificationService.allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
+ }
+
+ @Override
+ public RestrictionType getCurrentHostRestriction() {
+ String allowedHostString = getAttributes().get(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String deniedHostString = getAttributes().get(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ return RestrictionVerificationService.allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress);
+ }
+
+}
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..e02395302c
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/TimeRestrictionField.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 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/RestrictedUser.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUser.java
new file mode 100644
index 0000000000..c66032e611
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUser.java
@@ -0,0 +1,199 @@
+/*
+ * 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.Restrictable;
+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.calendar.RestrictionType;
+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 RestrictedUser extends DelegatingUser implements Restrictable {
+
+ /**
+ * The remote address of the client from which the current user is logged in.
+ */
+ private final String remoteAddress;
+
+ /**
+ * true if the user logged in to Guacamole has administrative privileges
+ * for this user object, otherwise false.
+ */
+ private final boolean hasAdmin;
+
+ /**
+ * The name of the attribute that contains a list of weekdays and times (UTC)
+ * 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 (UTC)
+ * 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.
+ *
+ * @param remoteAddress
+ * The remote address of the client from which the current user is logged
+ * in.
+ */
+ public RestrictedUser(User user, String remoteAddress, boolean hasAdmin) {
+ super(user);
+ this.remoteAddress = remoteAddress;
+ this.hasAdmin = hasAdmin;
+ }
+
+ /**
+ * 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) {
+
+ /* If the user lacks admin access, don't set restriction attributes. */
+ if (!hasAdmin) {
+ attributes.remove(attribute);
+ continue;
+ }
+
+ /* Replace empty values with null values. */
+ String value = attributes.get(attribute);
+ if (value != null && value.isEmpty())
+ attributes.put(attribute, null);
+ }
+
+ super.setAttributes(attributes);
+
+ }
+
+ @Override
+ public RestrictionType getCurrentTimeRestriction() {
+ String allowedTimeString = getAttributes().get(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
+ String deniedTimeString = getAttributes().get(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
+ return RestrictionVerificationService.allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
+ }
+
+ @Override
+ public RestrictionType getCurrentHostRestriction() {
+ String allowedHostString = getAttributes().get(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
+ String deniedHostString = getAttributes().get(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
+ return RestrictionVerificationService.allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress);
+ }
+
+}
diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUserContext.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUserContext.java
new file mode 100644
index 0000000000..3aeed57629
--- /dev/null
+++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUserContext.java
@@ -0,0 +1,214 @@
+/*
+ * 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 java.util.Set;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.restrict.RestrictionVerificationService;
+import org.apache.guacamole.auth.restrict.connection.RestrictedConnection;
+import org.apache.guacamole.auth.restrict.connectiongroup.RestrictedConnectionGroup;
+import org.apache.guacamole.auth.restrict.usergroup.RestrictedUserGroup;
+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.Permissions;
+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.permission.ObjectPermission;
+import org.apache.guacamole.net.auth.permission.SystemPermission;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A UserContext implementation for additional login and connection restrictions
+ * which wraps the UserContext of some other extension.
+ */
+public class RestrictedUserContext extends DelegatingUserContext {
+
+ /**
+ * The logger for this class.
+ */
+ private static final Logger LOGGER = LoggerFactory.getLogger(RestrictedUserContext.class);
+
+ /**
+ * The remote address from which this user logged in.
+ */
+ private final String remoteAddress;
+
+ /**
+ * The identifiers effective groups of the user associated with this context.
+ */
+ private final Set effectiveUserGroups;
+
+ /**
+ * Creates a new RestrictedUserContext which wraps the given UserContext,
+ * providing additional control for user logins and connections.
+ *
+ * @param userContext
+ * The UserContext to wrap.
+ *
+ * @param remoteAddress
+ * The address the user is logging in from, if known.
+ *
+ * @param effectiveUserGroups
+ * The identifiers of the groups this user is associated with.
+ */
+ public RestrictedUserContext(UserContext userContext, String remoteAddress,
+ Set effectiveUserGroups) {
+ super(userContext);
+ this.remoteAddress = remoteAddress;
+ this.effectiveUserGroups = effectiveUserGroups;
+ }
+
+ @Override
+ public Directory getConnectionDirectory() throws GuacamoleException {
+ return new DecoratingDirectory(super.getConnectionDirectory()) {
+
+ @Override
+ protected Connection decorate(Connection object) throws GuacamoleException {
+ return new RestrictedConnection(object, remoteAddress);
+ }
+
+ @Override
+ protected Connection undecorate(Connection object) {
+ assert(object instanceof RestrictedConnection);
+ return ((RestrictedConnection) object).getUndecorated();
+ }
+
+ };
+ }
+
+ @Override
+ public Collection