From 1088f60a4922f1a674f27a678c2a94e133c1652d Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Sun, 26 Mar 2023 17:03:58 -0400 Subject: [PATCH 01/11] GUACAMOLE-1020: Implement extension with enhanced login and connection restrictions. --- extensions/guacamole-auth-restrict/.gitignore | 3 + extensions/guacamole-auth-restrict/.ratignore | 0 extensions/guacamole-auth-restrict/pom.xml | 179 ++++++++++ .../src/main/assembly/dist.xml | 53 +++ .../RestrictionAuthenticationProvider.java | 62 ++++ .../RestrictionVerificationService.java | 333 ++++++++++++++++++ ...latableInvalidHostConnectionException.java | 73 ++++ ...TranslatableInvalidHostLoginException.java | 72 ++++ ...latableInvalidTimeConnectionException.java | 73 ++++ ...TranslatableInvalidTimeLoginException.java | 73 ++++ .../connection/RestrictConnection.java | 185 ++++++++++ .../RestrictConnectionGroup.java | 187 ++++++++++ .../restrict/form/HostRestrictionField.java | 47 +++ .../restrict/form/TimeRestrictionField.java | 49 +++ .../auth/restrict/user/RestrictUser.java | 158 +++++++++ .../restrict/user/RestrictUserContext.java | 165 +++++++++ .../restrict/usergroup/RestrictUserGroup.java | 160 +++++++++ .../guacamole/calendar/DailyRestriction.java | 132 +++++++ .../calendar/TimeRestrictionParser.java | 180 ++++++++++ .../guacamole/host/HostRestrictionParser.java | 77 ++++ .../main/resources/config/restrictConfig.js | 40 +++ .../hostRestrictionFieldController.js | 170 +++++++++ .../timeRestrictionFieldController.js | 223 ++++++++++++ .../src/main/resources/guac-manifest.json | 29 ++ .../src/main/resources/license.txt | 18 + .../src/main/resources/restrictModule.js | 29 ++ .../src/main/resources/styles/restrict.css | 36 ++ .../templates/hostRestrictionField.html | 23 ++ .../templates/timeRestrictionField.html | 38 ++ .../src/main/resources/translations/en.json | 67 ++++ .../resources/types/HostRestrictionEntry.js | 53 +++ .../resources/types/TimeRestrictionEntry.js | 69 ++++ extensions/guacamole-auth-totp/pom.xml | 2 +- extensions/pom.xml | 1 + 34 files changed, 3058 insertions(+), 1 deletion(-) create mode 100644 extensions/guacamole-auth-restrict/.gitignore create mode 100644 extensions/guacamole-auth-restrict/.ratignore create mode 100644 extensions/guacamole-auth-restrict/pom.xml create mode 100644 extensions/guacamole-auth-restrict/src/main/assembly/dist.xml create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionAuthenticationProvider.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidHostConnectionException.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidHostLoginException.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidTimeConnectionException.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/TranslatableInvalidTimeLoginException.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictConnection.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictConnectionGroup.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/HostRestrictionField.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/TimeRestrictionField.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUser.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUserContext.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictUserGroup.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/host/HostRestrictionParser.java create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/config/restrictConfig.js create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/controllers/hostRestrictionFieldController.js create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/guac-manifest.json create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/license.txt create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/restrictModule.js create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/styles/restrict.css create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/templates/hostRestrictionField.html create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/templates/timeRestrictionField.html create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/translations/en.json create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/types/HostRestrictionEntry.js create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/types/TimeRestrictionEntry.js diff --git a/extensions/guacamole-auth-restrict/.gitignore b/extensions/guacamole-auth-restrict/.gitignore new file mode 100644 index 000000000..1de9633ae --- /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 000000000..e69de29bb diff --git a/extensions/guacamole-auth-restrict/pom.xml b/extensions/guacamole-auth-restrict/pom.xml new file mode 100644 index 000000000..b011568d6 --- /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 000000000..0b16a7147 --- /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 000000000..f6f0d3699 --- /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 000000000..2b09b3023 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java @@ -0,0 +1,333 @@ +/* + * 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.warn("Host-based restrictions are present, but the remote " + + "address is invalid or could not be resolved. " + + "The action will not be allowed."); + 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.warn("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 000000000..285e769c7 --- /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 000000000..227710948 --- /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 000000000..f9158b8ee --- /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 000000000..7533a859b --- /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 000000000..534c617ba --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictConnection.java @@ -0,0 +1,185 @@ +/* + * 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 control access to the connection. + */ +public class RestrictConnection extends DelegatingConnection { + + /** + * 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) + ) + ); + + /** + * 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 000000000..48997c173 --- /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 an existing ConnectionGroup, + * providing additional ability to control access to the ConnectionGroup. + */ +public class RestrictConnectionGroup extends DelegatingConnectionGroup { + + /** + * 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) + ) + ); + + /** + * The remote address from which the user accessing this connection group + * logged in. + */ + private final String remoteAddress; + + /** + * 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 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 000000000..f89c82d6e --- /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 000000000..a3ace4f2c --- /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 000000000..1e4b558b8 --- /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 (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. + */ + 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 000000000..dd06f4e88 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUserContext.java @@ -0,0 +1,165 @@ +/* + * 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 and connection restrictions + * which wraps the UserContext of some other extension. + */ +public class RestrictUserContext extends DelegatingUserContext { + + /** + * The remote address from which this user logged in. + */ + private final String remoteAddress; + + /** + * Creates a new RestrictUserContext 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. + */ + 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
getConnectionAttributes() { + Collection connectionAttrs = new HashSet<>(super.getConnectionAttributes()); + connectionAttrs.add(RestrictConnection.RESTRICT_CONNECTION_FORM); + return Collections.unmodifiableCollection(connectionAttrs); + } + + @Override + public Directory getConnectionGroupDirectory() throws GuacamoleException { + return new DecoratingDirectory(super.getConnectionGroupDirectory()) { + + @Override + protected ConnectionGroup decorate(ConnectionGroup object) { + return new RestrictConnectionGroup(object, remoteAddress); + } + + @Override + protected ConnectionGroup undecorate(ConnectionGroup object) { + assert(object instanceof RestrictConnectionGroup); + return ((RestrictConnectionGroup) object).getUndecorated(); + } + + }; + } + + @Override + public Collection getConnectionGroupAttributes() { + Collection connectionGroupAttrs = new HashSet<>(super.getConnectionGroupAttributes()); + connectionGroupAttrs.add(RestrictConnectionGroup.RESTRICT_CONNECTIONGROUP_FORM); + return Collections.unmodifiableCollection(connectionGroupAttrs); + } + + @Override + public Directory getUserDirectory() throws GuacamoleException { + return new DecoratingDirectory(super.getUserDirectory()) { + + @Override + protected User decorate(User object) { + return new RestrictUser(object); + } + + @Override + protected User undecorate(User object) { + assert(object instanceof RestrictUser); + return ((RestrictUser) object).getUndecorated(); + } + + }; + } + + @Override + public Collection getUserAttributes() { + Collection userAttrs = new HashSet<>(super.getUserAttributes()); + userAttrs.add(RestrictUser.RESTRICT_LOGIN_FORM); + return Collections.unmodifiableCollection(userAttrs); + } + + @Override + public Directory getUserGroupDirectory() throws GuacamoleException { + return new DecoratingDirectory(super.getUserGroupDirectory()) { + + @Override + protected UserGroup decorate(UserGroup object) { + return new RestrictUserGroup(object); + } + + @Override + protected UserGroup undecorate(UserGroup object) { + assert(object instanceof RestrictUserGroup); + return ((RestrictUserGroup) object).getUndecorated(); + } + + }; + } + + @Override + public Collection getUserGroupAttributes() { + Collection userGroupAttrs = new HashSet<>(super.getUserGroupAttributes()); + userGroupAttrs.add(RestrictUserGroup.RESTRICT_LOGIN_FORM); + return Collections.unmodifiableCollection(userGroupAttrs); + } + +} diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictUserGroup.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictUserGroup.java new file mode 100644 index 000000000..9a1f026fd --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictUserGroup.java @@ -0,0 +1,160 @@ +/* + * 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.usergroup; + +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.DelegatingUserGroup; +import org.apache.guacamole.net.auth.UserGroup; + +/** + * UserGroup implementation which wraps a UserGroup from another extension and + * enforces additional restrictions for members of that group. + */ +public class RestrictUserGroup extends DelegatingUserGroup { + + /** + * The name of the attribute that contains a list of weekdays and times (UTC) + * that members of a group are allowed to log in. The presence of this + * attribute will restrict any users who are members of the group 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 members of a group are 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 + * members of a group are 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 + * members of a group are 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 UserGroup implementation. + */ + public static final List RESTRICT_USERGROUP_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 UserGroup object, providing capability of further restricting + * logins beyond the default restrictions provided by default modules. + * + * @param userGroup + * The UserGroup object to wrap. + */ + public RestrictUserGroup(UserGroup userGroup) { + super(userGroup); + } + + /** + * Returns the UserGroup object wrapped by this RestrictUserGroup. + * + * @return + * The wrapped UserGroup object. + */ + public UserGroup getUndecorated() { + return getDelegateUserGroupGroup(); + } + + @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_USERGROUP_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_USERGROUP_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/calendar/DailyRestriction.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java new file mode 100644 index 000000000..8060962d0 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java @@ -0,0 +1,132 @@ +/* + * 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.calendar; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.Collections; +import java.util.List; + +/** + * A class that stores a daily time restriction that can be used to determine + * whether or not a user can log in on a certain day of the week and during + * a certain time window. + */ +public class DailyRestriction { + + /** + * The days of the week that this restriction applies to. + */ + private final List weekDays; + + /** + * The time that the restriction starts. + */ + private final LocalTime startTime; + + /** + * The time that the restriction ends. + */ + private final LocalTime endTime; + + /** + * Create a new daily restriction with the specified day of the week, start + * time, and end time. + * + * @param weekDay + * The day of the week that this restriction should apply to. + * + * @param startTime + * The start time of the restriction. + * + * @param endTime + * The end time of the restriction. + */ + public DailyRestriction(DayOfWeek weekDay, + LocalTime startTime, LocalTime endTime) { + this.weekDays = Collections.singletonList(weekDay); + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Create a new daily restriction with the specified days of the week, start + * time, and end time. + * + * @param weekDays + * The days of the week that this restriction should apply to. + * + * @param startTime + * The start time of the restriction. + * + * @param endTime + * The end time of the restriction. + */ + public DailyRestriction(List weekDays, + LocalTime startTime, LocalTime endTime) { + this.weekDays = weekDays; + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Create a new daily restriction for an entire day, settings the start + * time at midnight and the end time at the end of the day (235959). + * + * @param weekDay + * The day of the week that this restriction should apply to. + */ + public DailyRestriction(DayOfWeek weekDay) { + this.weekDays = Collections.singletonList(weekDay); + this.startTime = LocalTime.of(0, 0, 0); + this.endTime = LocalTime.of(23, 59, 59); + } + + /** + * Create a new daily restriction for entire days, settings the start + * time at midnight and the end time at the end of the day (235959). + * + * @param weekDays + * The days of the week that this restriction should apply to. + */ + public DailyRestriction(List weekDays) { + this.weekDays = weekDays; + this.startTime = LocalTime.of(0, 0, 0); + this.endTime = LocalTime.of(23, 59, 59); + } + + /** + * Returns true if this restriction applies now, otherwise false. + * + * @return + * true if the current time of day falls within this restriction, + * otherwise false. + */ + public boolean appliesNow() { + DayOfWeek currentDay = LocalDate.now().getDayOfWeek(); + LocalTime currentTime = LocalTime.now(ZoneId.of("UTC")); + + // Check that we are in the specified time restriction + return (weekDays.contains(currentDay) && currentTime.isAfter(startTime) && currentTime.isBefore(endTime)); + } + +} diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java new file mode 100644 index 000000000..d308272d3 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java @@ -0,0 +1,180 @@ +/* + * 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.calendar; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A class for parsing time-based restrictions stored in a String into other + * formats that can be used by Guacamole. + */ +public class TimeRestrictionParser { + + /** + * The compiled regular expression that matches one or more instances of + * a restriction string, which specifies at least one day and time range + * that the restriction applies to. + * + *

Examples of valid restrictions are as follows: + *

    + *
  • 1:0700-1700 - Monday from 07:00 to 17:00 + *
  • 7:0000-2359 - Sunday, all day (00:00 to 23:59) + *
  • wd:0900-1700 - Monday through Friday, 09:00 to 17:00 + *
  • we:0900-1700 - Saturday and Sunday, 09:00 to 17:00 + *
  • 6:0900-1600;7:1200-1300 - Saturday, 09:00 to 16:00, and Sunday, + * 12:00 - 13:00 + *
+ */ + private static final Pattern RESTRICTION_REGEX = + Pattern.compile("(?:^|;)+([1-7*]|(?:[w][ed]))(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\\-((?:[01][0-9]|2[0-3])[0-5][0-9]))+"); + + /** + * The RegEx group that contains the start day-of-week of the restriction. + */ + private static final int RESTRICTION_DAY_GROUP = 1; + + /** + * The RegEx group that contains the start time of the restriction. + */ + private static final int RESTRICTION_TIME_START_GROUP = 2; + + /** + * The RegEx group that contains the end time of the restriction. + */ + private static final int RESTRICTION_TIME_END_GROUP = 3; + + /** + * A list of DayOfWeek items that make up weekdays. + */ + private static final List RESTRICTION_WEEKDAYS = Arrays.asList( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY + ); + + /** + * A list of DayOfWeek items that make up weekends. + */ + private static final List RESTRICTION_WEEKEND = Arrays.asList( + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY + ); + + /** + * A list of DayOfWeek items that make up all days of the week. + */ + private static final List RESTRICTION_ALL_DAYS = Arrays.asList( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY + ); + + /** + * Parse the provided string containing one or more restrictions into + * a list of objects. + * + * @param restrictionString + * The string that should contain one or more semicolon-separated + * restriction periods. + * + * @return + * A list of objects parsed from the string. + */ + public static List parseString(String restrictionString) { + + List restrictions = new ArrayList<>(); + Matcher restrictionMatcher = RESTRICTION_REGEX.matcher(restrictionString); + + // Loop through RegEx matches + while (restrictionMatcher.find()) { + + // Pull the day string, start time, and end time + String dayString = restrictionMatcher.group(RESTRICTION_DAY_GROUP); + String startTimeString = restrictionMatcher.group(RESTRICTION_TIME_START_GROUP); + String endTimeString = restrictionMatcher.group(RESTRICTION_TIME_END_GROUP); + LocalTime startTime, endTime; + + // We must always have a value for the day. + if (dayString == null || dayString.isEmpty()) + continue; + + // Convert the start and end time strings to LocalTime values. + DateTimeFormatter hourFormat = DateTimeFormatter.ofPattern("HHmm"); + + // If start time is empty, assume the start of the day. + if (startTimeString == null || startTimeString.isEmpty()) + startTime = LocalTime.of(0, 0, 0); + + // Otherwise, parse out the start time. + else + startTime = LocalTime.parse(startTimeString, hourFormat); + + // If end time is empty, assume the end of the day. + if (endTimeString == null || endTimeString.isEmpty()) + endTime = LocalTime.of(23, 59, 59); + + // Otherwise, parse out the end time. + else + endTime = LocalTime.parse(endTimeString, hourFormat); + + // Based on value of day string, add the appropriate entry. + switch(dayString) { + // All days of the week. + case "*": + restrictions.add(new DailyRestriction(RESTRICTION_ALL_DAYS, startTime, endTime)); + break; + + // Weekdays only. + case "wd": + restrictions.add(new DailyRestriction(RESTRICTION_WEEKDAYS, startTime, endTime)); + break; + + // Weekend days only. + case "we": + restrictions.add(new DailyRestriction(RESTRICTION_WEEKEND, startTime, endTime)); + break; + + // A specific day of the week. + default: + restrictions.add(new DailyRestriction(DayOfWeek.of(Integer.parseInt(dayString)), startTime, endTime)); + + } + + } + + // Return the list of restrictions + return restrictions; + + } + +} diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/host/HostRestrictionParser.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/host/HostRestrictionParser.java new file mode 100644 index 000000000..d368c807a --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/host/HostRestrictionParser.java @@ -0,0 +1,77 @@ +/* + * 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.host; + +import inet.ipaddr.HostName; +import inet.ipaddr.HostNameException; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A utility class that parses a string for a set of IPv4 or IPv6 addresses, + * or hostnames, splitting the string into a list of components. + */ +public class HostRestrictionParser { + + /** + * The logger for this class. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(HostRestrictionParser.class); + + /** + * Parse the provided string into a List of HostName objects, validating + * that each item is an IP address, subnet, and/or DNS name. + * + * @param hostString + * The string that contains a semi-colon-separated list of items to + * parse. + * + * @return + * A List of HostName objects parsed from the provided string. + */ + public static List parseHostList(String hostString) { + + List addressList = new ArrayList<>(); + + if (hostString == null || hostString.isEmpty()) + return addressList; + + // First split the string by semicolons and process each entry + for (String host : hostString.split(";")) { + + HostName hostName = new HostName(host); + try { + hostName.validate(); + addressList.add(hostName); + } + catch (HostNameException e) { + LOGGER.warn("Invalid host name or IP: {}", host); + LOGGER.debug("HostNameException.", e.getMessage()); + } + + } + + return addressList; + + } + +} diff --git a/extensions/guacamole-auth-restrict/src/main/resources/config/restrictConfig.js b/extensions/guacamole-auth-restrict/src/main/resources/config/restrictConfig.js new file mode 100644 index 000000000..4c63e8a2d --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/config/restrictConfig.js @@ -0,0 +1,40 @@ +/* + * 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. + */ + +/** + * Config block which registers restrict-specific field types. + */ +angular.module('guacRestrict').config(['formServiceProvider', + function guacRestrictConfig(formServiceProvider) { + + // Define the time restriction field + formServiceProvider.registerFieldType('GUAC_TIME_RESTRICTION', { + module : 'guacRestrict', + controller : 'timeRestrictionFieldController', + templateUrl : 'app/ext/restrict/templates/timeRestrictionField.html' + }); + + // Define the host restriction field + formServiceProvider.registerFieldType('GUAC_HOST_RESTRICTION', { + module : 'guacRestrict', + controller : 'hostRestrictionFieldController', + templateUrl : 'app/ext/restrict/templates/hostRestrictionField.html' + }); + +}]); diff --git a/extensions/guacamole-auth-restrict/src/main/resources/controllers/hostRestrictionFieldController.js b/extensions/guacamole-auth-restrict/src/main/resources/controllers/hostRestrictionFieldController.js new file mode 100644 index 000000000..94bb39207 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/controllers/hostRestrictionFieldController.js @@ -0,0 +1,170 @@ +/* + * 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. + */ + + +/** + * Controller for host restriction fields, which are used to configure a + * hostname, IP address, or CIDR range, that this restriction applies to. + */ +angular.module('guacRestrict').controller('hostRestrictionFieldController', ['$scope', '$injector', + function hostRestrictionFieldController($scope, $injector) { + + // Required types + const HostRestrictionEntry = $injector.get('HostRestrictionEntry'); + + /** + * Options which dictate the behavior of the input field model, as defined + * by https://docs.angularjs.org/api/ng/directive/ngModelOptions + * + * @type Object. + */ + $scope.modelOptions = { + + /** + * Space-delimited list of events on which the model will be updated. + * + * @type String + */ + updateOn : 'blur', + + /** + * The time zone to use when reading/writing the Date object of the + * model. + * + * @type String + */ + timezone : 'UTC' + + }; + + /** + * The restrictions, as objects, that are used by the HTML template to + * present the restrictions to the user via the web interface. + * + * @type HostRestrictionEntry[] + */ + $scope.restrictions = []; + + /** + * Remove the current entry from the list. + * + * @param {HostRestrictionEntry} entry + * A restriction entry. + */ + $scope.removeEntry = function removeEntry(entry) { + if (entry === null || entry.$$hashKey === '') { + return; + } + for (let i = 0; i < $scope.restrictions.length; i++) { + if ($scope.restrictions[i].$$hashKey === entry.$$hashKey) { + $scope.restrictions.splice(i,1); + return; + } + } + }; + + /** + * Add an empty entry to the restriction list. + */ + $scope.addEntry = function addEntry() { + $scope.restrictions.push(new HostRestrictionEntry()); + }; + + /** + * Parse the provided string into an array containing the objects that + * represent each of entries that can then be displayed as a more + * user-friendly field. + * + * @param {String} restrString + * The string that contains the restrictions, un-parsed and as stored + * in the underlying field. + * + * @returns {HostRestrictionEntry[]} + * An array of objects that represents each of the entries as parsed + * out of the string field, and which can be interpreted by the + * AngularJS field for display. + */ + const parseRestrictions = function parseRestrictions(restrString) { + + var restrictions = []; + + // If the string is null or empty, just return an empty array + if (restrString === null || restrString === "") + return restrictions; + + // Set up the RegEx and split the string using the separator. + var restrArray = restrString.split(";"); + + // Loop through split string and process each item + for (let i = 0; i < restrArray.length; i++) { + var entry = new HostRestrictionEntry(); + entry.host = restrArray[i]; + restrictions.push(entry); + } + + return restrictions; + + }; + + /** + * Parse the restrictions in the field into a string that can be stored + * in an underlying module. + * + * @param {HostRestrictionEntry[]} restrictions + * The array of restrictions that will be converted to a string. + * + * @returns {String} + * The string containing the restriction data that can be stored in e.g. + * a database. + */ + const storeRestrictions = function storeRestrictions(restrictions) { + // If there are no members of the array, just return an empty string. + if (restrictions === null || restrictions.length < 1) + return ''; + + var restrString = ''; + for (let i = 0; i < restrictions.length; i++) { + // If any of the properties are not defined, skip this one. + if (!Object.hasOwn(restrictions[i], 'host') + || restrictions[i].host === null) + continue; + + // If this is not the first item, then add a semi-colon separator + if (restrString.length > 0) + restrString += ';'; + + // Add the current host to the list + restrString += restrictions[i].host; + } + + return restrString; + + }; + + // Update the field when the model changes. + $scope.$watch('model', function modelChanged(model) { + $scope.restrictions = parseRestrictions(model); + }); + + // Update string value in model when web form is changed + $scope.$watch('restrictions', function restrictionsChanged(restrictions) { + $scope.model = storeRestrictions(restrictions); + }, true); + +}]); \ No newline at end of file diff --git a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js new file mode 100644 index 000000000..a238e61e9 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js @@ -0,0 +1,223 @@ +/* + * 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. + */ + + +/** + * Controller for time restriction fields, which are used to select weekday and + * time restrictions that apply to user logins and connections. + */ +angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$scope', '$injector', + function timeRestrictionFieldController($scope, $injector) { + + // Required types + const TimeRestrictionEntry = $injector.get('TimeRestrictionEntry'); + + /** + * Options which dictate the behavior of the input field model, as defined + * by https://docs.angularjs.org/api/ng/directive/ngModelOptions + * + * @type Object. + */ + $scope.modelOptions = { + + /** + * Space-delimited list of events on which the model will be updated. + * + * @type String + */ + updateOn : 'blur' + + }; + + /** + * The restrictions, as objects, that are used by the HTML template to + * present the restrictions to the user via the web interface. + * + * @type TimeRestrictionEntry[] + */ + $scope.restrictions = []; + + /** + * Map of weekday identifier to display name. + */ + $scope.weekDays = [ + { id : '1', day : 'Monday' }, + { id : '2', day : 'Tuesday' }, + { id : '3', day : 'Wednesday' }, + { id : '4', day : 'Thursday' }, + { id : '5', day : 'Friday' }, + { id : '6', day : 'Saturday' }, + { id : '7', day : 'Sunday' }, + { id : '*', day : 'All days' }, + { id : 'wd', day: 'Week days' }, + { id : 'we', day: 'Week end' } + ]; + + /** + * Remove the current entry from the list. + * + * @param {TimeRestrictionEntry} entry + * A restriction entry. + */ + $scope.removeEntry = function removeEntry(entry) { + if (entry === null || entry.$$hashKey === '') { + return; + } + for (let i = 0; i < $scope.restrictions.length; i++) { + if ($scope.restrictions[i].$$hashKey === entry.$$hashKey) { + $scope.restrictions.splice(i,1); + return; + } + } + }; + + /** + * Add an empty entry to the restriction list. + */ + $scope.addEntry = function addEntry() { + $scope.restrictions.push(new TimeRestrictionEntry()); + }; + + + /** + * Parse the provided string into an array containing the objects that + * represent each of entries that can then be displayed as a more + * user-friendly field. + * + * @param {String} restrString + * The string that contains the restrictions, un-parsed and as stored + * in the underlying field. + * + * @returns {TimeRestrictionEntry[]} + * An array of objects that represents each of the entries as parsed + * out of the string field, and which can be interpreted by the + * AngularJS field for display. + */ + const parseRestrictions = function parseRestrictions(restrString) { + + var restrictions = []; + + // If the string is null or empty, just return an empty array + if (restrString === null || restrString === "") + return restrictions; + + // Set up the RegEx and split the string using the separator. + const restrictionRegex = new RegExp('^([1-7*]|(?:[w][ed]))(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\-((?:[01][0-9]|2[0-3])[0-5][0-9]))$'); + var restrArray = restrString.split(";"); + + // Loop through split string and process each item + for (let i = 0; i < restrArray.length; i++) { + + // Test if our regex matches + if (restrictionRegex.test(restrArray[i])) { + var currArray = restrArray[i].match(restrictionRegex); + var entry = new TimeRestrictionEntry(); + entry.weekDay = '' + currArray[1]; + entry.startTime = new Date(Date.UTC(1970, 1, 1, parseInt(currArray[2].slice(0,2)), parseInt(currArray[2].slice(2)), 0, 0)); + entry.endTime = new Date(Date.UTC(1970, 1, 1, parseInt(currArray[3].slice(0,2)), parseInt(currArray[3].slice(2)), 0, 0)); + restrictions.push(entry); + } + } + + return restrictions; + + }; + + /** + * Parse the restrictions in the field into a string that can be stored + * in an underlying module. + * + * @param {TimeRestrictionEntry[]} restrictions + * The array of restrictions that will be converted to a string. + * + * @returns {String} + * The string containing the restriction data that can be stored in e.g. + * a database. + */ + const storeRestrictions = function storeRestrictions(restrictions) { + // If there are no members of the array, just return an empty string. + if (restrictions === null || restrictions.length < 1) + return ''; + + var restrString = ''; + for (let i = 0; i < restrictions.length; i++) { + // If any of the properties are not defined, skip this one. + if (!Object.hasOwn(restrictions[i], 'weekDay') + || restrictions[i].weekDay === null + || !Object.hasOwn(restrictions[i], 'startTime') + || restrictions[i].startTime === null + || !(restrictions[i].startTime instanceof Date) + || !Object.hasOwn(restrictions[i], 'endTime') + || restrictions[i].endTime === null + || !(restrictions[i].endTime instanceof Date)) + continue; + + // If this is not the first item, then add a semi-colon separator + if (restrString.length > 0) + restrString += ';'; + + // Add the weekday component of the restriction, insuring it is a string. + var currString = '' + restrictions[i].weekDay; + currString += ':'; + + // Retrieve startTime hours component and add it, adding leading zero if required. + startHours = restrictions[i].startTime.getUTCHours(); + if (startHours !== null && startHours < 10) + startHours = '0' + startHours; + currString += startHours; + + // Retrieve startTime minutes component and add it, adding leading zero if required. + startMins = restrictions[i].startTime.getUTCMinutes(); + if (startMins !== null && startMins < 10) + startMins = '0' + startMins; + currString += startMins; + + currString += '-'; + + // Retrieve endTime hours component and add it, adding leading zero if required. + endHours = restrictions[i].endTime.getUTCHours(); + if (endHours !== null && endHours < 10) + endHours = '0' + endHours; + currString += endHours; + + // Retrieve endTime minutes component and add it, adding leading zero if required. + endMins = restrictions[i].endTime.getUTCMinutes(); + if (endMins < 10) + endMins = '0' + endMins; + currString += endMins; + + // Add the newly-created string to the overall restriction string. + restrString += currString; + } + + return restrString; + + }; + + // Update the field when the model changes. + $scope.$watch('model', function modelChanged(model) { + $scope.restrictions = parseRestrictions(model); + }); + + // Update string value in model when web form is changed + $scope.$watch('restrictions', function restrictionsChanged(restrictions) { + $scope.model = storeRestrictions(restrictions); + }, true); + +}]); \ No newline at end of file diff --git a/extensions/guacamole-auth-restrict/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-restrict/src/main/resources/guac-manifest.json new file mode 100644 index 000000000..e0b692848 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/guac-manifest.json @@ -0,0 +1,29 @@ +{ + + "guacamoleVersion" : "1.6.0", + + "name" : "Restriction Authentication Backend", + "namespace" : "restrict", + + "authProviders" : [ + "org.apache.guacamole.auth.restrict.RestrictionAuthenticationProvider" + ], + + "translations" : [ + "translations/en.json" + ], + + "js" : [ + "restrict.min.js" + ], + + "css" : [ + "restrict.min.css" + ], + + "resources" : { + "templates/hostRestrictionField.html" : "text/html", + "templates/timeRestrictionField.html" : "text/html" + } + +} diff --git a/extensions/guacamole-auth-restrict/src/main/resources/license.txt b/extensions/guacamole-auth-restrict/src/main/resources/license.txt new file mode 100644 index 000000000..042f3ce1f --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/license.txt @@ -0,0 +1,18 @@ +/* + * 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. + */ diff --git a/extensions/guacamole-auth-restrict/src/main/resources/restrictModule.js b/extensions/guacamole-auth-restrict/src/main/resources/restrictModule.js new file mode 100644 index 000000000..7d508183b --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/restrictModule.js @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** + * Module which provides handling for additional login and connection + * restrictions. + */ +angular.module('guacRestrict', [ + 'form' +]); + +// Ensure the guacRestrict module is loaded along with the rest of the app +angular.module('index').requires.push('guacRestrict'); diff --git a/extensions/guacamole-auth-restrict/src/main/resources/styles/restrict.css b/extensions/guacamole-auth-restrict/src/main/resources/styles/restrict.css new file mode 100644 index 000000000..b6a91eb56 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/styles/restrict.css @@ -0,0 +1,36 @@ +/* + * 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. + */ + +.restrictionList { + border: 0; +} + +button.restrictionListButton { + font-size: 0.75em; +} + +img.restrictionListHeader { + width: 0.75em; + height: 0.75em; +} + +img.restrictionListItem { + width: 1em; + height: 1em; +} \ No newline at end of file diff --git a/extensions/guacamole-auth-restrict/src/main/resources/templates/hostRestrictionField.html b/extensions/guacamole-auth-restrict/src/main/resources/templates/hostRestrictionField.html new file mode 100644 index 000000000..3febbf007 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/templates/hostRestrictionField.html @@ -0,0 +1,23 @@ +
+ + + + + + + + + +
{{ 'RESTRICT.TABLE_HEADER_HOST' | translate }} 
+ + + Remove entry +
+ +
\ No newline at end of file diff --git a/extensions/guacamole-auth-restrict/src/main/resources/templates/timeRestrictionField.html b/extensions/guacamole-auth-restrict/src/main/resources/templates/timeRestrictionField.html new file mode 100644 index 000000000..dfc95ee41 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/templates/timeRestrictionField.html @@ -0,0 +1,38 @@ +
+ + + + + + + + + + + + + +
{{ 'RESTRICT.TABLE_HEADER_DAY' | translate }}{{ 'RESTRICT.TABLE_HEADER_START_TIME' | translate }}{{ 'RESTRICT.TABLE_HEADER_END_TIME' | translate }} 
+ + + + + + + Remove entry +
+ +
\ No newline at end of file diff --git a/extensions/guacamole-auth-restrict/src/main/resources/translations/en.json b/extensions/guacamole-auth-restrict/src/main/resources/translations/en.json new file mode 100644 index 000000000..3eb3d0460 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/translations/en.json @@ -0,0 +1,67 @@ +{ + + "DATA_SOURCE_LOGIN_RESTRICTIONS" : { + "NAME" : "Additional Restrictions" + }, + + "CONNECTION_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which connection may be accessed:", + "FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which connection may not be accessed:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times connection is allowed to be used:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times connection may not be used:", + + "SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Connection Restrictions" + + }, + + "CONNECTION_GROUP_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which connection group may be accessed:", + "FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which connection group may not be accessed:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times connection group is allowed to be used:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times connection group may not be used:", + + "SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Connection Restrictions" + + }, + + "RESTRICT" : { + + "ACTION_ADD_ENTRY" : "Add Entry", + + "ERROR_CONNECTION_NOT_ALLOWED_NOW" : "The connection is not available at this time.", + "ERROR_CONNECTION_NOT_ALLOWED_FROM_HOST" : "The connection is not allowed from this host.", + "ERROR_USER_LOGIN_NOT_ALLOWED_NOW" : "The login for this user is not allowed at this time.", + "ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST" : "The login for this user is not allowed from this host.", + + "TABLE_HEADER_DAY" : "Day", + "TABLE_HEADER_END_TIME" : "End Time", + "TABLE_HEADER_HOST" : "Host", + "TABLE_HEADER_START_TIME" : "Start Time" + + }, + + "USER_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which user can log in:", + "FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which user may not log in:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times user is allowed to log in:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times user is denied from log in:", + + "SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Login Restrictions" + + }, + + "USER_GROUP_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which members may log in:", + "FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which members may not log in:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times members are allowed to log in:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times members are denied from log in:", + + "SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Login Restrictions" + + } + +} diff --git a/extensions/guacamole-auth-restrict/src/main/resources/types/HostRestrictionEntry.js b/extensions/guacamole-auth-restrict/src/main/resources/types/HostRestrictionEntry.js new file mode 100644 index 000000000..e4246f54d --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/types/HostRestrictionEntry.js @@ -0,0 +1,53 @@ +/* + * 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. + */ + +/** + * Provides the HostRestrictionEntry class definition. + */ +angular.module('guacRestrict').factory('HostRestrictionEntry', [ + function defineHostRestrictionEntry() { + + /** + * Creates a new HostRestrictionEntry, initializing the properties of that + * HostRestrictionEntry with the corresponding properties of the given + * template. + * + * @constructor + * @param {HostRestrictionEntry|Object} [template={}] + * The object whose properties should be copied within the new + * HostRestrictionEntry. + */ + var HostRestrictionEntry = function HostRestrictionEntry(template) { + + // Use empty object by default + template = template || {}; + + /** + * The IP address, CIDR notation range, or DNS hostname of the host(s) + * specified by this restriction. + * + * @type String + */ + this.host = template.host || ''; + + }; + + return HostRestrictionEntry; + +}]); diff --git a/extensions/guacamole-auth-restrict/src/main/resources/types/TimeRestrictionEntry.js b/extensions/guacamole-auth-restrict/src/main/resources/types/TimeRestrictionEntry.js new file mode 100644 index 000000000..fe7d8170b --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/types/TimeRestrictionEntry.js @@ -0,0 +1,69 @@ +/* + * 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. + */ + +/** + * Provides the TimeRestrictionEntry class definition. + */ +angular.module('guacRestrict').factory('TimeRestrictionEntry', [ + function defineTimeRestrictionEntry() { + + /** + * Creates a new TimeRestrictionEntry, initializing the properties of that + * TimeRestrictionEntry with the corresponding properties of the given + * template. + * + * @constructor + * @param {TimeRestrictionEntry|Object} [template={}] + * The object whose properties should be copied within the new + * TimeRestrictionEntry. + */ + var TimeRestrictionEntry = function TimeRestrictionEntry(template) { + + // Use empty object by default + template = template || {}; + + /** + * The numerical representation of the day of the week this restriction + * applies to. + * + * @type Number + */ + this.weekDay = template.weekDay; + + /** + * The hour and minute that this restriction starts, in 24-hour time, + * and with no separator between the hour and minute. + * + * @type Date + */ + this.startTime = template.startTime; + + /** + * The hour and minute that this restriction ends, in 24-hour time, and + * with no separator between the hour and minute. + * + * @type Date + */ + this.endTime = template.endTime; + + }; + + return TimeRestrictionEntry; + +}]); diff --git a/extensions/guacamole-auth-totp/pom.xml b/extensions/guacamole-auth-totp/pom.xml index 765667ba1..a637362e2 100644 --- a/extensions/guacamole-auth-totp/pom.xml +++ b/extensions/guacamole-auth-totp/pom.xml @@ -28,7 +28,7 @@ jar 1.6.0 guacamole-auth-totp - http://guacamole.incubator.apache.org/ + http://guacamole.apache.org/ org.apache.guacamole diff --git a/extensions/pom.xml b/extensions/pom.xml index 760c845b0..53512ad67 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -47,6 +47,7 @@ guacamole-auth-json guacamole-auth-ldap guacamole-auth-quickconnect + guacamole-auth-restrict guacamole-auth-sso guacamole-auth-totp From e7279f0a8dd3453310f344c979612bed6f72af4c Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Sat, 10 Jun 2023 17:34:43 -0400 Subject: [PATCH 02/11] GUACAMOLE-1020: Remove Weekend and Weekday definitions to avoid locale issues. --- .../calendar/TimeRestrictionParser.java | 34 ++----------------- .../timeRestrictionFieldController.js | 17 ++++++---- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java index d308272d3..3c7099f45 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java @@ -43,14 +43,13 @@ public class TimeRestrictionParser { *
    *
  • 1:0700-1700 - Monday from 07:00 to 17:00 *
  • 7:0000-2359 - Sunday, all day (00:00 to 23:59) - *
  • wd:0900-1700 - Monday through Friday, 09:00 to 17:00 - *
  • we:0900-1700 - Saturday and Sunday, 09:00 to 17:00 + *
  • *:0900-1700 - Every day, 09:00 to 17:00 *
  • 6:0900-1600;7:1200-1300 - Saturday, 09:00 to 16:00, and Sunday, * 12:00 - 13:00 *
*/ private static final Pattern RESTRICTION_REGEX = - Pattern.compile("(?:^|;)+([1-7*]|(?:[w][ed]))(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\\-((?:[01][0-9]|2[0-3])[0-5][0-9]))+"); + Pattern.compile("(?:^|;)+([1-7*])(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\\-((?:[01][0-9]|2[0-3])[0-5][0-9]))+"); /** * The RegEx group that contains the start day-of-week of the restriction. @@ -67,25 +66,6 @@ public class TimeRestrictionParser { */ private static final int RESTRICTION_TIME_END_GROUP = 3; - /** - * A list of DayOfWeek items that make up weekdays. - */ - private static final List RESTRICTION_WEEKDAYS = Arrays.asList( - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY - ); - - /** - * A list of DayOfWeek items that make up weekends. - */ - private static final List RESTRICTION_WEEKEND = Arrays.asList( - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY - ); - /** * A list of DayOfWeek items that make up all days of the week. */ @@ -154,16 +134,6 @@ public class TimeRestrictionParser { restrictions.add(new DailyRestriction(RESTRICTION_ALL_DAYS, startTime, endTime)); break; - // Weekdays only. - case "wd": - restrictions.add(new DailyRestriction(RESTRICTION_WEEKDAYS, startTime, endTime)); - break; - - // Weekend days only. - case "we": - restrictions.add(new DailyRestriction(RESTRICTION_WEEKEND, startTime, endTime)); - break; - // A specific day of the week. default: restrictions.add(new DailyRestriction(DayOfWeek.of(Integer.parseInt(dayString)), startTime, endTime)); diff --git a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js index a238e61e9..d65ed8df2 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js +++ b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js @@ -28,6 +28,9 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s // Required types const TimeRestrictionEntry = $injector.get('TimeRestrictionEntry'); + // Required services + const $log = $injector.get('$log'); + /** * Options which dictate the behavior of the input field model, as defined * by https://docs.angularjs.org/api/ng/directive/ngModelOptions @@ -64,9 +67,7 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s { id : '5', day : 'Friday' }, { id : '6', day : 'Saturday' }, { id : '7', day : 'Sunday' }, - { id : '*', day : 'All days' }, - { id : 'wd', day: 'Week days' }, - { id : 'we', day: 'Week end' } + { id : '*', day : 'All days' } ]; /** @@ -111,14 +112,18 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s */ const parseRestrictions = function parseRestrictions(restrString) { + // Array to store the restrictions var restrictions = []; + // Grab the current date so that we can accurately parse DST later + var templateDate = new Date(); + // If the string is null or empty, just return an empty array if (restrString === null || restrString === "") return restrictions; // Set up the RegEx and split the string using the separator. - const restrictionRegex = new RegExp('^([1-7*]|(?:[w][ed]))(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\-((?:[01][0-9]|2[0-3])[0-5][0-9]))$'); + const restrictionRegex = new RegExp('^([1-7*])(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\-((?:[01][0-9]|2[0-3])[0-5][0-9]))$'); var restrArray = restrString.split(";"); // Loop through split string and process each item @@ -129,8 +134,8 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s var currArray = restrArray[i].match(restrictionRegex); var entry = new TimeRestrictionEntry(); entry.weekDay = '' + currArray[1]; - entry.startTime = new Date(Date.UTC(1970, 1, 1, parseInt(currArray[2].slice(0,2)), parseInt(currArray[2].slice(2)), 0, 0)); - entry.endTime = new Date(Date.UTC(1970, 1, 1, parseInt(currArray[3].slice(0,2)), parseInt(currArray[3].slice(2)), 0, 0)); + entry.startTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[2].slice(0,2)), parseInt(currArray[2].slice(2)))); + entry.endTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[3].slice(0,2)), parseInt(currArray[3].slice(2)))) restrictions.push(entry); } } From 1b7c35e189318cb984ccf6f2c499544ab6ebf6ca Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Sat, 4 Nov 2023 21:31:54 -0400 Subject: [PATCH 03/11] GUACAMOLE-1020: Correct issues with DST not being interpreted correctly. --- .../guacamole/calendar/DailyRestriction.java | 2 +- .../timeRestrictionFieldController.js | 56 +++++++++++++------ .../resources/types/TimeRestrictionEntry.js | 4 +- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java index 8060962d0..0186bda86 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java @@ -122,7 +122,7 @@ public class DailyRestriction { * otherwise false. */ public boolean appliesNow() { - DayOfWeek currentDay = LocalDate.now().getDayOfWeek(); + DayOfWeek currentDay = LocalDate.now(ZoneId.of("UTC")).getDayOfWeek(); LocalTime currentTime = LocalTime.now(ZoneId.of("UTC")); // Check that we are in the specified time restriction diff --git a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js index d65ed8df2..bf88513b0 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js +++ b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js @@ -135,7 +135,7 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s var entry = new TimeRestrictionEntry(); entry.weekDay = '' + currArray[1]; entry.startTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[2].slice(0,2)), parseInt(currArray[2].slice(2)))); - entry.endTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[3].slice(0,2)), parseInt(currArray[3].slice(2)))) + entry.endTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[3].slice(0,2)), parseInt(currArray[3].slice(2)))); restrictions.push(entry); } } @@ -156,15 +156,17 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s * a database. */ const storeRestrictions = function storeRestrictions(restrictions) { + // If there are no members of the array, just return an empty string. if (restrictions === null || restrictions.length < 1) return ''; - var restrString = ''; + let restrString = ''; for (let i = 0; i < restrictions.length; i++) { // If any of the properties are not defined, skip this one. if (!Object.hasOwn(restrictions[i], 'weekDay') || restrictions[i].weekDay === null + || restrictions[i].weekDay === '' || !Object.hasOwn(restrictions[i], 'startTime') || restrictions[i].startTime === null || !(restrictions[i].startTime instanceof Date) @@ -178,34 +180,56 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s restrString += ';'; // Add the weekday component of the restriction, insuring it is a string. - var currString = '' + restrictions[i].weekDay; + let currString = '' + restrictions[i].weekDay.toString(); currString += ':'; + + // When the field first gets a value, it defaults to a year of 1970 + // In order to avoid issues with Daylight Savings Time, we have to + // work around this. + if (restrictions[i].startTime instanceof Date && restrictions[i].startTime.getFullYear() === 1970) { + let startHour = restrictions[i].startTime.getHours(); + let startMin = restrictions[i].startTime.getMinutes(); + restrictions[i].startTime = new Date(); + restrictions[i].startTime.setHours(startHour); + restrictions[i].startTime.setMinutes(startMin); + } // Retrieve startTime hours component and add it, adding leading zero if required. - startHours = restrictions[i].startTime.getUTCHours(); + let startHours = restrictions[i].startTime.getUTCHours(); if (startHours !== null && startHours < 10) - startHours = '0' + startHours; - currString += startHours; + currString += '0'; + currString += startHours.toString(); // Retrieve startTime minutes component and add it, adding leading zero if required. - startMins = restrictions[i].startTime.getUTCMinutes(); + let startMins = restrictions[i].startTime.getUTCMinutes(); if (startMins !== null && startMins < 10) - startMins = '0' + startMins; - currString += startMins; + currString += '0'; + currString += startMins.toString(); currString += '-'; + // When the field first gets a value, it defaults to a year of 1970 + // In order to avoid issues with Daylight Savings Time, we have to + // work around this. + if (restrictions[i].endTime instanceof Date && restrictions[i].endTime.getFullYear() === 1970) { + let endHour = restrictions[i].endTime.getHours(); + let endMin = restrictions[i].endTime.getMinutes(); + restrictions[i].endTime = new Date(); + restrictions[i].endTime.setHours(endHour); + restrictions[i].endTime.setMinutes(endMin); + } + // Retrieve endTime hours component and add it, adding leading zero if required. - endHours = restrictions[i].endTime.getUTCHours(); + let endHours = restrictions[i].endTime.getUTCHours(); if (endHours !== null && endHours < 10) - endHours = '0' + endHours; - currString += endHours; + currString += '0'; + currString += endHours.toString(); // Retrieve endTime minutes component and add it, adding leading zero if required. - endMins = restrictions[i].endTime.getUTCMinutes(); - if (endMins < 10) - endMins = '0' + endMins; - currString += endMins; + let endMins = restrictions[i].endTime.getUTCMinutes(); + if (endMins !== null && endMins < 10) + currString += '0'; + currString += endMins.toString(); // Add the newly-created string to the overall restriction string. restrString += currString; diff --git a/extensions/guacamole-auth-restrict/src/main/resources/types/TimeRestrictionEntry.js b/extensions/guacamole-auth-restrict/src/main/resources/types/TimeRestrictionEntry.js index fe7d8170b..00debb224 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/types/TimeRestrictionEntry.js +++ b/extensions/guacamole-auth-restrict/src/main/resources/types/TimeRestrictionEntry.js @@ -42,9 +42,9 @@ angular.module('guacRestrict').factory('TimeRestrictionEntry', [ * The numerical representation of the day of the week this restriction * applies to. * - * @type Number + * @type {string} */ - this.weekDay = template.weekDay; + this.weekDay = template.weekDay || ''; /** * The hour and minute that this restriction starts, in 24-hour time, From 422db894f3fbbf15b4f79e089a4d2f2b23812288 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Tue, 7 Nov 2023 19:44:30 -0500 Subject: [PATCH 04/11] GUACAMOLE-1020: Fix issues with Timezone offset and next day calculations. --- .../guacamole/calendar/DailyRestriction.java | 8 ++ .../timeRestrictionFieldController.js | 92 ++++++++++++++----- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java index 0186bda86..0169141e8 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/DailyRestriction.java @@ -125,6 +125,14 @@ public class DailyRestriction { DayOfWeek currentDay = LocalDate.now(ZoneId.of("UTC")).getDayOfWeek(); LocalTime currentTime = LocalTime.now(ZoneId.of("UTC")); + // If end time is less than the start time, we check the remainder of this + // day and the beginning of the next day. + if (endTime.isBefore(startTime)) { + if (weekDays.contains(currentDay) && currentTime.isAfter(startTime) && currentTime.isBefore(LocalTime.MAX)) + return true; + return (weekDays.contains(currentDay.plus(1)) && currentTime.isAfter(LocalTime.MIDNIGHT) && currentTime.isBefore(endTime)); + } + // Check that we are in the specified time restriction return (weekDays.contains(currentDay) && currentTime.isAfter(startTime) && currentTime.isBefore(endTime)); } diff --git a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js index bf88513b0..31ef38525 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js +++ b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js @@ -57,9 +57,11 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s $scope.restrictions = []; /** - * Map of weekday identifier to display name. + * Map of weekday identifier to display name. Note that Sunday occurs + * twice - once for the 0-index and once for the 7 index. */ $scope.weekDays = [ + { id : '0', day : 'Sunday' }, { id : '1', day : 'Monday' }, { id : '2', day : 'Tuesday' }, { id : '3', day : 'Wednesday' }, @@ -123,7 +125,7 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s return restrictions; // Set up the RegEx and split the string using the separator. - const restrictionRegex = new RegExp('^([1-7*])(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\-((?:[01][0-9]|2[0-3])[0-5][0-9]))$'); + const restrictionRegex = new RegExp('^([0-7*])(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\-((?:[01][0-9]|2[0-3])[0-5][0-9]))$'); var restrArray = restrString.split(";"); // Loop through split string and process each item @@ -132,10 +134,40 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s // Test if our regex matches if (restrictionRegex.test(restrArray[i])) { var currArray = restrArray[i].match(restrictionRegex); - var entry = new TimeRestrictionEntry(); - entry.weekDay = '' + currArray[1]; + let entry = new TimeRestrictionEntry(); entry.startTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[2].slice(0,2)), parseInt(currArray[2].slice(2)))); entry.endTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[3].slice(0,2)), parseInt(currArray[3].slice(2)))); + var origDay = currArray[1]; + + if (currArray[1] === '*') + entry.weekDay = '' + currArray[1]; + + else { + // If UTC day is greater than local day, we subtract a day, + // wrapping as required. + if (entry.startTime.getDay() < entry.startTime.getUTCDay()) { + if (origDay <= 0) + entry.weekDay = '' + 6; + else + entry.weekDay = '' + (--origDay); + + } + + // If UTC day is less than local day, we add a day, + // wrapping as required. + else if (entry.startTime.getDay() > entry.startTime.getUTCDay()) { + if (origDay >= 6) + entry.weekDay = '' + 0; + else + entry.weekDay = '' + (++origDay); + } + + // Local day and UTC day are the same, adjust the display day + else + entry.weekDay = '' + origDay; + + } + restrictions.push(entry); } } @@ -179,12 +211,7 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s if (restrString.length > 0) restrString += ';'; - // Add the weekday component of the restriction, insuring it is a string. - let currString = '' + restrictions[i].weekDay.toString(); - currString += ':'; - - - // When the field first gets a value, it defaults to a year of 1970 + // When these fields first gets a value, the default year is 1970 // In order to avoid issues with Daylight Savings Time, we have to // work around this. if (restrictions[i].startTime instanceof Date && restrictions[i].startTime.getFullYear() === 1970) { @@ -194,6 +221,40 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s restrictions[i].startTime.setHours(startHour); restrictions[i].startTime.setMinutes(startMin); } + + if (restrictions[i].endTime instanceof Date && restrictions[i].endTime.getFullYear() === 1970) { + let endHour = restrictions[i].endTime.getHours(); + let endMin = restrictions[i].endTime.getMinutes(); + restrictions[i].endTime = new Date(); + restrictions[i].endTime.setHours(endHour); + restrictions[i].endTime.setMinutes(endMin); + } + + // Process the start day, factoring in wrapping for local time to + // UTC adjustments. + let weekDay = restrictions[i].weekDay; + const startDay = restrictions[i].startTime.getDay(); + const utcStartDay = restrictions[i].startTime.getUTCDay(); + + // Local day is less than UTC day, so we add a day for storing, + // wrapping around as required. + if (weekDay !== '*' && startDay < utcStartDay) { + if (weekDay >= 6) + weekDay = 0; + else + weekDay++; + } + + else if (weekDay !== '*' && startDay > utcStartDay) { + if (weekDay <= 0) + weekDay = 6; + else + weekDay--; + } + + let currString = '' + weekDay.toString(); + currString += ':'; + // Retrieve startTime hours component and add it, adding leading zero if required. let startHours = restrictions[i].startTime.getUTCHours(); if (startHours !== null && startHours < 10) @@ -207,17 +268,6 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s currString += startMins.toString(); currString += '-'; - - // When the field first gets a value, it defaults to a year of 1970 - // In order to avoid issues with Daylight Savings Time, we have to - // work around this. - if (restrictions[i].endTime instanceof Date && restrictions[i].endTime.getFullYear() === 1970) { - let endHour = restrictions[i].endTime.getHours(); - let endMin = restrictions[i].endTime.getMinutes(); - restrictions[i].endTime = new Date(); - restrictions[i].endTime.setHours(endHour); - restrictions[i].endTime.setMinutes(endMin); - } // Retrieve endTime hours component and add it, adding leading zero if required. let endHours = restrictions[i].endTime.getUTCHours(); From 8ad254e89ff6ff9ae0f4a995e091f7f970d384b5 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Fri, 14 Jun 2024 16:51:56 -0400 Subject: [PATCH 05/11] GUACAMOLE-1020: Add Docker mapping for the restrict extension module. --- guacamole-docker/build.d/000-build-and-install-guacamole.sh | 2 +- guacamole-docker/build.d/010-map-guacamole-extensions.sh | 3 ++- guacamole-docker/build.d/020-download-drivers.sh | 2 +- guacamole-docker/entrypoint.d/700-configure-features.sh | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/guacamole-docker/build.d/000-build-and-install-guacamole.sh b/guacamole-docker/build.d/000-build-and-install-guacamole.sh index 8c9c77203..e20d87b2d 100644 --- a/guacamole-docker/build.d/000-build-and-install-guacamole.sh +++ b/guacamole-docker/build.d/000-build-and-install-guacamole.sh @@ -18,7 +18,7 @@ # ## -## @fn 010-build-and-install-guacamole.sh +## @fn 000-build-and-install-guacamole.sh ## ## Builds the Guacamole web application and all main extensions, installing the ## resulting binaries to standard locations within the Docker image. After the diff --git a/guacamole-docker/build.d/010-map-guacamole-extensions.sh b/guacamole-docker/build.d/010-map-guacamole-extensions.sh index de12262ae..45a344c13 100644 --- a/guacamole-docker/build.d/010-map-guacamole-extensions.sh +++ b/guacamole-docker/build.d/010-map-guacamole-extensions.sh @@ -18,7 +18,7 @@ # ## -## @fn 020-map-guacamole-extensions.sh +## @fn 010-map-guacamole-extensions.sh ## ## Maps all installed Guacamole extensions (built in a previous step) to their ## corresponding environment variable prefixes, adding symbolic links so that @@ -106,6 +106,7 @@ map_extensions <<'EOF' guacamole-auth-ldap.........................LDAP_ guacamole-auth-quickconnect.................QUICKCONNECT_ guacamole-auth-radius.......................RADIUS_ + guacamole-auth-restrict.....................RESTRICT_ guacamole-auth-sso/cas......................CAS_ guacamole-auth-sso/openid...................OPENID_ guacamole-auth-sso/saml.....................SAML_ diff --git a/guacamole-docker/build.d/020-download-drivers.sh b/guacamole-docker/build.d/020-download-drivers.sh index 6613dc3a2..a5fe6c778 100644 --- a/guacamole-docker/build.d/020-download-drivers.sh +++ b/guacamole-docker/build.d/020-download-drivers.sh @@ -18,7 +18,7 @@ # ## -## @fn 030-download-drivers.sh +## @fn 020-download-drivers.sh ## ## Downloads all JDBC drivers required by the various supported databases. Each ## downloaded driver is stored beneath /opt/guacamole/drivers, with symbolic diff --git a/guacamole-docker/entrypoint.d/700-configure-features.sh b/guacamole-docker/entrypoint.d/700-configure-features.sh index f1a2b1a43..cc7ad0e24 100644 --- a/guacamole-docker/entrypoint.d/700-configure-features.sh +++ b/guacamole-docker/entrypoint.d/700-configure-features.sh @@ -19,7 +19,7 @@ # ## -## @fn 800-configure-features.sh +## @fn 700-configure-features.sh ## ## Automatically checks all environment variables currently set and performs ## configuration tasks related to those variabels, including installing any From 042b99bede68beb0941925dc511d4bbc23c1a6b1 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 29 Jul 2024 09:07:29 -0400 Subject: [PATCH 06/11] GUACAMOLE-1020: Implement a new Restrictable interface and make slight name changes to classes. --- .../guacamole/auth/restrict/Restrictable.java | 67 +++ .../RestrictionAuthenticationProvider.java | 15 +- .../RestrictionVerificationService.java | 436 +++++++++++++----- ...nection.java => RestrictedConnection.java} | 44 +- ...up.java => RestrictedConnectionGroup.java} | 44 +- ...{RestrictUser.java => RestrictedUser.java} | 34 +- ...ontext.java => RestrictedUserContext.java} | 69 ++- ...serGroup.java => RestrictedUserGroup.java} | 6 +- .../guacamole/calendar/RestrictionType.java | 109 +++++ 9 files changed, 641 insertions(+), 183 deletions(-) create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/Restrictable.java rename extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/{RestrictConnection.java => RestrictedConnection.java} (82%) rename extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/{RestrictConnectionGroup.java => RestrictedConnectionGroup.java} (82%) rename extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/{RestrictUser.java => RestrictedUser.java} (81%) rename extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/{RestrictUserContext.java => RestrictedUserContext.java} (66%) rename extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/{RestrictUserGroup.java => RestrictedUserGroup.java} (98%) create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/RestrictionType.java 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 000000000..ff1acf745 --- /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 index f6f0d3699..a2dfcdf0d 100644 --- 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 @@ -19,8 +19,10 @@ package org.apache.guacamole.auth.restrict; +import com.google.inject.Guice; +import com.google.inject.Injector; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.restrict.user.RestrictUserContext; +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; @@ -32,7 +34,7 @@ import org.apache.guacamole.net.auth.UserContext; * administrators to further control access to Guacamole resources. */ public class RestrictionAuthenticationProvider extends AbstractAuthenticationProvider { - + @Override public String getIdentifier() { return "restrict"; @@ -43,12 +45,15 @@ public class RestrictionAuthenticationProvider extends AbstractAuthenticationPro AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { + String remoteAddress = credentials.getRemoteAddress(); + + // Verify identity of user - RestrictionVerificationService.verifyLoginRestrictions(context, authenticatedUser); + RestrictionVerificationService.verifyLoginRestrictions(context, remoteAddress); // User has been verified, and authentication should be allowed to // continue - return new RestrictUserContext(context, credentials.getRemoteAddress()); + return new RestrictedUserContext(context, remoteAddress); } @@ -56,7 +61,7 @@ public class RestrictionAuthenticationProvider extends AbstractAuthenticationPro public UserContext redecorate(UserContext decorated, UserContext context, AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { - return new RestrictUserContext(context, credentials.getRemoteAddress()); + return new RestrictedUserContext(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 index 2b09b3023..eb05e2b35 100644 --- 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 @@ -23,19 +23,19 @@ 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.RestrictConnection; -import org.apache.guacamole.auth.restrict.user.RestrictUser; -import org.apache.guacamole.auth.restrict.usergroup.RestrictUserGroup; +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.AuthenticatedUser; -import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.permission.SystemPermission; @@ -56,8 +56,7 @@ public class RestrictionVerificationService { /** * 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. + * day and time, and returning the appropriate restriction type. * * @param allowedTimeString * The string containing the times that should be parsed to determine if @@ -71,47 +70,48 @@ public class RestrictionVerificationService { * action should be denied. * * @return - * True if the login or connection should be allowed, otherwise false. + * A RestrictionType based on the provided allowed and denied strings. */ - private static boolean allowedByTimeRestrictions(String allowedTimeString, + public static RestrictionType allowedByTimeRestrictions(String allowedTimeString, String deniedTimeString) { - // Check for denied entries, first, returning false if the login or - // connection should not be allowed. + // 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 false; + return RestrictionType.EXPLICIT_DENY; } } - // If no allowed entries are present, return true, allowing the login - // or connection to continue. + // If no allowed entries are present, return the implicit allow, allowing + // the login or connection to continue. if (allowedTimeString == null || allowedTimeString.isEmpty()) - return true; + 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 true. + // If this time allows the login or connection return the explicit allow. if (restriction.appliesNow()) - return true; + return RestrictionType.EXPLICIT_ALLOW; } - // We have allowed entries, but login hasn't matched, so deny it. - return false; + // 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. If the action - * should not be allowed, return false - otherwise, return true. + * 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 @@ -129,18 +129,18 @@ public class RestrictionVerificationService { * 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. + * A RestrictionType that matches the provided allow and deny strings. */ - private static boolean allowedByHostRestrictions(String allowedHostsString, + 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 true; + return RestrictionType.IMPLICIT_ALLOW; // If the remote address cannot be determined, and restrictions are // in effect, log an error and deny the action. @@ -148,7 +148,7 @@ public class RestrictionVerificationService { 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 false; + return RestrictionType.IMPLICIT_DENY; } // Split denied hosts attribute and process each entry, checking them @@ -158,24 +158,24 @@ public class RestrictionVerificationService { for (HostName hostName : deniedHosts) { try { if (hostName.isAddress() && hostName.toAddress().contains(remoteHostName.asAddress())) - return false; + return RestrictionType.EXPLICIT_DENY; else for (IPAddress currAddr : hostName.toAllAddresses()) if (currAddr.matches(remoteHostName.asAddressString())) - return false; + 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 false; + 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 true; + return RestrictionType.IMPLICIT_ALLOW; // Run through allowed hosts, if there are any, and return, allowing the // action if there are any matches. @@ -184,12 +184,12 @@ public class RestrictionVerificationService { 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; + 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 true; + return RestrictionType.EXPLICIT_ALLOW; } // If an entry cannot be resolved we will log a warning. @@ -201,7 +201,270 @@ public class RestrictionVerificationService { // If we've made it here, the allowed hosts do not contain the remote // address, and the action should not be allowed; - return false; + 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 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, 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(currentUser.getUserGroups().getObjects()); + + // 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: + LOGGER.debug("User \"{}\" is explicitly allowed from host \"{}\".", + currentUser.getIdentifier(), remoteAddress); + return; + + // Implicit allow was the highest result, so we log it and return, allowing the user to be logged in. + case IMPLICIT_ALLOW: + LOGGER.debug("User \"{}\" is implicitly allowed from host \"{}\".", + currentUser.getIdentifier(), remoteAddress); + 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 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. + * + * @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) 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(currentUser.getUserGroups().getObjects()); + + // 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: + LOGGER.debug("User \"{}\" is explicitly allowed at this time.", currentUser.getIdentifier()); + return; + + // Implicit allow was the highest result, so we log it and return, allowing the user to be logged in. + case IMPLICIT_ALLOW: + LOGGER.debug("User \"{}\" is implicitly allowed at this time.", currentUser.getIdentifier()); + return; + } + + // If we reach, here, we've reached an implict deny, so we throw an exception. + throw new TranslatableInvalidTimeLoginException("User \"{}\" is implicitly denied at this time.", currentUser.getIdentifier()); + + } + + /** + * 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" + ); } @@ -213,80 +476,17 @@ public class RestrictionVerificationService { * @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. + * @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, - AuthenticatedUser authenticatedUser) throws GuacamoleException { + public static void verifyLoginRestrictions(UserContext context, String remoteAddress) 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"); - - } + verifyTimeRestrictions(context); + verifyHostRestrictions(context, remoteAddress); } @@ -295,38 +495,24 @@ public class RestrictionVerificationService { * 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 restrictable + * The object that supports restrictions that is to be verified to be + * usable within the current restrictions. * * @param remoteAddress - * The remote IP address of the user trying to access the connection. + * 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( - Map connectionAttributes, String remoteAddress) + public void verifyConnectionRestrictions(Restrictable restrictable, 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" - ); + verifyTimeRestrictions(restrictable); + verifyHostRestrictions(restrictable, remoteAddress); - // 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/connection/RestrictConnection.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictedConnection.java similarity index 82% rename from extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictConnection.java rename to extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictedConnection.java index 534c617ba..31f7c33a4 100644 --- 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/RestrictedConnection.java @@ -19,14 +19,17 @@ package org.apache.guacamole.auth.restrict.connection; +import com.google.inject.Inject; 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; @@ -37,7 +40,18 @@ import org.apache.guacamole.protocol.GuacamoleClientInformation; * A Connection implementation that wraps another connection, providing additional * ability to control access to the connection. */ -public class RestrictConnection extends DelegatingConnection { +public class RestrictedConnection extends DelegatingConnection implements Restrictable { + + /** + * The remote address of the client from which the user logged in. + */ + private final String remoteAddress; + + /** + * The restriction verification service. + */ + @Inject + private RestrictionVerificationService verificationService; /** * The name of the attribute that contains a list of weekdays and times (UTC) @@ -101,12 +115,6 @@ public class RestrictConnection extends DelegatingConnection { ) ); - /** - * 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 @@ -116,10 +124,10 @@ public class RestrictConnection extends DelegatingConnection { * The Connection object to wrap. * * @param remoteAddress - * The remote address from which the user attempting to access this - * connection logged in. + * The remote address of the client from which the current user logged + * in. */ - public RestrictConnection(Connection connection, String remoteAddress) { + public RestrictedConnection(Connection connection, String remoteAddress) { super(connection); this.remoteAddress = remoteAddress; } @@ -175,11 +183,25 @@ public class RestrictConnection extends DelegatingConnection { Map tokens) throws GuacamoleException { // Verify the restrictions for this connection. - RestrictionVerificationService.verifyConnectionRestrictions(getAttributes(), remoteAddress); + verificationService.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/RestrictConnectionGroup.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java similarity index 82% rename from extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictConnectionGroup.java rename to extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java index 48997c173..b877bb51f 100644 --- 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/RestrictedConnectionGroup.java @@ -19,14 +19,17 @@ package org.apache.guacamole.auth.restrict.connectiongroup; +import com.google.inject.Inject; 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; @@ -37,7 +40,18 @@ 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 RestrictConnectionGroup extends DelegatingConnectionGroup { +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 verification service. + */ + @Inject + private RestrictionVerificationService verificationService; /** * The name of the attribute that contains a list of weekdays and times (UTC) @@ -102,12 +116,6 @@ public class RestrictConnectionGroup extends DelegatingConnectionGroup { ) ); - /** - * The remote address from which the user accessing this connection group - * logged in. - */ - private final String remoteAddress; - /** * Wraps the given ConnectionGroup object, providing capability of further * restricting connection group access beyond the default access control @@ -117,10 +125,10 @@ public class RestrictConnectionGroup extends DelegatingConnectionGroup { * The ConnectionGroup object to wrap. * * @param remoteAddress - * The remote address from which the user accessing this connection - * logged in. + * The remote address of the client from which the current user logged + * in. */ - public RestrictConnectionGroup(ConnectionGroup connectionGroup, String remoteAddress) { + public RestrictedConnectionGroup(ConnectionGroup connectionGroup, String remoteAddress) { super(connectionGroup); this.remoteAddress = remoteAddress; } @@ -177,11 +185,25 @@ public class RestrictConnectionGroup extends DelegatingConnectionGroup { Map tokens) throws GuacamoleException { // Verify restrictions for this connection group. - RestrictionVerificationService.verifyConnectionRestrictions(getAttributes(), remoteAddress); + verificationService.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/user/RestrictUser.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUser.java similarity index 81% rename from extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUser.java rename to extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUser.java index 1e4b558b8..84e2bac50 100644 --- 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/RestrictedUser.java @@ -23,8 +23,11 @@ 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; @@ -33,8 +36,13 @@ 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 { - +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; + /** * 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 @@ -97,16 +105,20 @@ public class RestrictUser extends DelegatingUser { ) ); - /** * 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 RestrictUser(User user) { + public RestrictedUser(User user, String remoteAddress) { super(user); + this.remoteAddress = remoteAddress; } /** @@ -154,5 +166,19 @@ public class RestrictUser extends DelegatingUser { 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/RestrictUserContext.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUserContext.java similarity index 66% rename from extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictUserContext.java rename to extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUserContext.java index dd06f4e88..53ec773e7 100644 --- 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/RestrictedUserContext.java @@ -23,9 +23,10 @@ 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.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; @@ -35,20 +36,27 @@ import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserGroup; +import org.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 RestrictUserContext extends DelegatingUserContext { +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; /** - * Creates a new RestrictUserContext which wraps the given UserContext, + * Creates a new RestrictedUserContext which wraps the given UserContext, * providing additional control for user logins and connections. * * @param userContext @@ -57,7 +65,7 @@ public class RestrictUserContext extends DelegatingUserContext { * @param remoteAddress * The address the user is logging in from, if known. */ - public RestrictUserContext(UserContext userContext, String remoteAddress) { + public RestrictedUserContext(UserContext userContext, String remoteAddress) { super(userContext); this.remoteAddress = remoteAddress; } @@ -67,14 +75,14 @@ public class RestrictUserContext extends DelegatingUserContext { return new DecoratingDirectory(super.getConnectionDirectory()) { @Override - protected Connection decorate(Connection object) { - return new RestrictConnection(object, remoteAddress); + protected Connection decorate(Connection object) throws GuacamoleException { + return new RestrictedConnection(object, remoteAddress); } @Override protected Connection undecorate(Connection object) { - assert(object instanceof RestrictConnection); - return ((RestrictConnection) object).getUndecorated(); + assert(object instanceof RestrictedConnection); + return ((RestrictedConnection) object).getUndecorated(); } }; @@ -83,7 +91,7 @@ public class RestrictUserContext extends DelegatingUserContext { @Override public Collection getConnectionAttributes() { Collection connectionAttrs = new HashSet<>(super.getConnectionAttributes()); - connectionAttrs.add(RestrictConnection.RESTRICT_CONNECTION_FORM); + connectionAttrs.add(RestrictedConnection.RESTRICT_CONNECTION_FORM); return Collections.unmodifiableCollection(connectionAttrs); } @@ -92,14 +100,14 @@ public class RestrictUserContext extends DelegatingUserContext { return new DecoratingDirectory(super.getConnectionGroupDirectory()) { @Override - protected ConnectionGroup decorate(ConnectionGroup object) { - return new RestrictConnectionGroup(object, remoteAddress); + protected ConnectionGroup decorate(ConnectionGroup object) throws GuacamoleException { + return new RestrictedConnectionGroup(object, remoteAddress); } @Override protected ConnectionGroup undecorate(ConnectionGroup object) { - assert(object instanceof RestrictConnectionGroup); - return ((RestrictConnectionGroup) object).getUndecorated(); + assert(object instanceof RestrictedConnectionGroup); + return ((RestrictedConnectionGroup) object).getUndecorated(); } }; @@ -108,7 +116,7 @@ public class RestrictUserContext extends DelegatingUserContext { @Override public Collection getConnectionGroupAttributes() { Collection connectionGroupAttrs = new HashSet<>(super.getConnectionGroupAttributes()); - connectionGroupAttrs.add(RestrictConnectionGroup.RESTRICT_CONNECTIONGROUP_FORM); + connectionGroupAttrs.add(RestrictedConnectionGroup.RESTRICT_CONNECTIONGROUP_FORM); return Collections.unmodifiableCollection(connectionGroupAttrs); } @@ -118,13 +126,13 @@ public class RestrictUserContext extends DelegatingUserContext { @Override protected User decorate(User object) { - return new RestrictUser(object); + return new RestrictedUser(object, remoteAddress); } @Override protected User undecorate(User object) { - assert(object instanceof RestrictUser); - return ((RestrictUser) object).getUndecorated(); + assert(object instanceof RestrictedUser); + return ((RestrictedUser) object).getUndecorated(); } }; @@ -133,7 +141,7 @@ public class RestrictUserContext extends DelegatingUserContext { @Override public Collection getUserAttributes() { Collection userAttrs = new HashSet<>(super.getUserAttributes()); - userAttrs.add(RestrictUser.RESTRICT_LOGIN_FORM); + userAttrs.add(RestrictedUser.RESTRICT_LOGIN_FORM); return Collections.unmodifiableCollection(userAttrs); } @@ -143,13 +151,13 @@ public class RestrictUserContext extends DelegatingUserContext { @Override protected UserGroup decorate(UserGroup object) { - return new RestrictUserGroup(object); + return new RestrictedUserGroup(object); } @Override protected UserGroup undecorate(UserGroup object) { - assert(object instanceof RestrictUserGroup); - return ((RestrictUserGroup) object).getUndecorated(); + assert(object instanceof RestrictedUserGroup); + return ((RestrictedUserGroup) object).getUndecorated(); } }; @@ -158,8 +166,21 @@ public class RestrictUserContext extends DelegatingUserContext { @Override public Collection getUserGroupAttributes() { Collection userGroupAttrs = new HashSet<>(super.getUserGroupAttributes()); - userGroupAttrs.add(RestrictUserGroup.RESTRICT_LOGIN_FORM); + userGroupAttrs.add(RestrictedUserGroup.RESTRICT_LOGIN_FORM); return Collections.unmodifiableCollection(userGroupAttrs); } + + @Override + public boolean isValid() { + try { + // Verify whether or not time restrictions still apply. + RestrictionVerificationService.verifyTimeRestrictions(this); + return true; + } + catch (GuacamoleException e) { + LOGGER.debug("User account is now restricted and is no longer valid", e); + return false; + } + } } diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictUserGroup.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictedUserGroup.java similarity index 98% rename from extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictUserGroup.java rename to extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictedUserGroup.java index 9a1f026fd..2e637872b 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictUserGroup.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictedUserGroup.java @@ -33,8 +33,8 @@ import org.apache.guacamole.net.auth.UserGroup; * UserGroup implementation which wraps a UserGroup from another extension and * enforces additional restrictions for members of that group. */ -public class RestrictUserGroup extends DelegatingUserGroup { - +public class RestrictedUserGroup extends DelegatingUserGroup { + /** * The name of the attribute that contains a list of weekdays and times (UTC) * that members of a group are allowed to log in. The presence of this @@ -107,7 +107,7 @@ public class RestrictUserGroup extends DelegatingUserGroup { * @param userGroup * The UserGroup object to wrap. */ - public RestrictUserGroup(UserGroup userGroup) { + public RestrictedUserGroup(UserGroup userGroup) { super(userGroup); } diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/RestrictionType.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/RestrictionType.java new file mode 100644 index 000000000..bc62eb998 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/RestrictionType.java @@ -0,0 +1,109 @@ +/* + * 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.calendar; + +/** + * A data type that represents various values of what type of restriction applies + * at a given time. + */ +public enum RestrictionType { + + /** + * Access is explicitly allowed. + */ + EXPLICIT_ALLOW(1, true), + + /** + * Access is explicitly denied. + */ + EXPLICIT_DENY(0, false), + + /** + * Access has not been explicitly allowed or denied, therefore it is + * implicitly allowed. + */ + IMPLICIT_ALLOW(3, true), + + /** + * Access has not been explicitly allowed or denied, therefore it is + * implicitly denied. + */ + IMPLICIT_DENY(2, false); + + /** + * The overall priority of the restriction, with zero being the highest + * priority and the priority decreasing as numbers increase from zero. + */ + final private int priority; + + /** + * true if the restriction allows access, otherwise false. + */ + final private boolean allowed; + + /** + * Create the new instance of this RestrictionType, with the given + * priority value for the instance. + * + * @param priority + * The priority of the restriction type, where zero is the highest + * priority. + * + * @param allowed + * true if the restriction allows access, otherwise false. + */ + RestrictionType(int priority, boolean allowed) { + this.priority = priority; + this.allowed = allowed; + } + + /** + * Evaluates two restrictions, returning the higher priority of the two. + * + * @param restriction1 + * The first restriction to compare. + * + * @param restriction2 + * The second restriction to compare. + * + * @return + * Return which of the two restrictions is the higher-priority. + */ + public static RestrictionType getHigherPriority(RestrictionType restriction1, RestrictionType restriction2) { + + // If the second is higher than the first, return the second. + if (restriction1.priority > restriction2.priority) + return restriction2; + + // Return the first. + return restriction1; + + } + + /** + * Returns true if this restriction allows access, otherwise false. + * + * @return + * true if this restriction allows access, otherwise false. + */ + public boolean isAllowed() { + return this.allowed; + } + +} From 7cfe979ed1c396315a3aac534ec3c184bb2317f5 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Sun, 8 Sep 2024 16:46:46 -0400 Subject: [PATCH 07/11] GUACAMOLE-1020: Move time conversion to shared function. --- .../timeRestrictionFieldController.js | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js index 31ef38525..86312aae6 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js +++ b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js @@ -28,9 +28,6 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s // Required types const TimeRestrictionEntry = $injector.get('TimeRestrictionEntry'); - // Required services - const $log = $injector.get('$log'); - /** * Options which dictate the behavior of the input field model, as defined * by https://docs.angularjs.org/api/ng/directive/ngModelOptions @@ -176,6 +173,27 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s }; + /** + * Since new Time fields in HTML get a default year of 1970, we need to + * merge the hours and minutes from the time field into the current Date, + * primarily so that Daylight Savings Time offsets are correct. + * + * @param {Date} justTime + * The Date object produced by an HTML field that contains the hours + * and minutes we need. + * + * @returns {Date} + * The Date object that merges the current calendar date with the + * hours and minutes from the HTML field. + */ + const timeToCurrentDate = function timeToCurrentDate(justTime) { + let dateAndTime = new Date(); + dateAndTime.setHours(justTime.getHours()); + dateAndTime.setMinutes(justTime.getMinutes()); + + return dateAndTime; + }; + /** * Parse the restrictions in the field into a string that can be stored * in an underlying module. @@ -214,21 +232,11 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s // When these fields first gets a value, the default year is 1970 // In order to avoid issues with Daylight Savings Time, we have to // work around this. - if (restrictions[i].startTime instanceof Date && restrictions[i].startTime.getFullYear() === 1970) { - let startHour = restrictions[i].startTime.getHours(); - let startMin = restrictions[i].startTime.getMinutes(); - restrictions[i].startTime = new Date(); - restrictions[i].startTime.setHours(startHour); - restrictions[i].startTime.setMinutes(startMin); - } + if (restrictions[i].startTime instanceof Date && restrictions[i].startTime.getFullYear() === 1970) + restrictions[i].startTime = timeToCurrentDate(restrictions[i].startTime); - if (restrictions[i].endTime instanceof Date && restrictions[i].endTime.getFullYear() === 1970) { - let endHour = restrictions[i].endTime.getHours(); - let endMin = restrictions[i].endTime.getMinutes(); - restrictions[i].endTime = new Date(); - restrictions[i].endTime.setHours(endHour); - restrictions[i].endTime.setMinutes(endMin); - } + if (restrictions[i].endTime instanceof Date && restrictions[i].endTime.getFullYear() === 1970) + restrictions[i].endTime = timeToCurrentDate(restrictions[i].endTime); // Process the start day, factoring in wrapping for local time to // UTC adjustments. From d97cde0e6fa140c2e2f421d69950ab0a9aae3595 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 9 Sep 2024 11:14:49 -0400 Subject: [PATCH 08/11] GUACAMOLE-1020: Clean up unnecessary Java imports. --- .../restrict/connection/RestrictedConnection.java | 13 +++---------- .../connectiongroup/RestrictedConnectionGroup.java | 9 +-------- .../auth/restrict/form/TimeRestrictionField.java | 2 -- 3 files changed, 4 insertions(+), 20 deletions(-) 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 index 31f7c33a4..bdbce0bcc 100644 --- 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 @@ -19,7 +19,6 @@ package org.apache.guacamole.auth.restrict.connection; -import com.google.inject.Inject; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -41,18 +40,12 @@ import org.apache.guacamole.protocol.GuacamoleClientInformation; * 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 restriction verification service. - */ - @Inject - private RestrictionVerificationService verificationService; - /** * 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 @@ -181,9 +174,9 @@ public class RestrictedConnection extends DelegatingConnection implements Restri @Override public GuacamoleTunnel connect(GuacamoleClientInformation info, Map tokens) throws GuacamoleException { - + // Verify the restrictions for this connection. - verificationService.verifyConnectionRestrictions(this, remoteAddress); + RestrictionVerificationService.verifyConnectionRestrictions(this, remoteAddress); // Connect return super.connect(info, tokens); 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 index b877bb51f..b6c18144e 100644 --- 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 @@ -19,7 +19,6 @@ package org.apache.guacamole.auth.restrict.connectiongroup; -import com.google.inject.Inject; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -47,12 +46,6 @@ public class RestrictedConnectionGroup extends DelegatingConnectionGroup impleme */ private final String remoteAddress; - /** - * The verification service. - */ - @Inject - private RestrictionVerificationService verificationService; - /** * 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 @@ -185,7 +178,7 @@ public class RestrictedConnectionGroup extends DelegatingConnectionGroup impleme Map tokens) throws GuacamoleException { // Verify restrictions for this connection group. - verificationService.verifyConnectionRestrictions(this, remoteAddress); + RestrictionVerificationService.verifyConnectionRestrictions(this, remoteAddress); // Connect return super.connect(info, tokens); 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 index a3ace4f2c..e02395302 100644 --- 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 @@ -20,8 +20,6 @@ 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 From cc14281f01793e4d317eb5c26a288d4c4213c0c0 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 9 Sep 2024 11:15:13 -0400 Subject: [PATCH 09/11] GUACAMOLE-1020: Handle differences in Sunday processing for Java and JavaScript. --- .../guacamole/calendar/TimeRestrictionParser.java | 11 +++++++++-- .../controllers/timeRestrictionFieldController.js | 13 ++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java index 3c7099f45..91b195422 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/calendar/TimeRestrictionParser.java @@ -49,7 +49,7 @@ public class TimeRestrictionParser { * */ private static final Pattern RESTRICTION_REGEX = - Pattern.compile("(?:^|;)+([1-7*])(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\\-((?:[01][0-9]|2[0-3])[0-5][0-9]))+"); + Pattern.compile("(?:^|;)+([0-7*])(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\\-((?:[01][0-9]|2[0-3])[0-5][0-9]))+"); /** * The RegEx group that contains the start day-of-week of the restriction. @@ -136,7 +136,14 @@ public class TimeRestrictionParser { // A specific day of the week. default: - restrictions.add(new DailyRestriction(DayOfWeek.of(Integer.parseInt(dayString)), startTime, endTime)); + int dayInt = Integer.parseInt(dayString); + + // While JavaScript sees Sunday as "0" and "7", DayOfWeek + // does not, so we'll convert it to "7" in order to process it. + if (dayInt == 0) + dayInt = 7; + + restrictions.add(new DailyRestriction(DayOfWeek.of(dayInt), startTime, endTime)); } diff --git a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js index 86312aae6..de83b6a93 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js +++ b/extensions/guacamole-auth-restrict/src/main/resources/controllers/timeRestrictionFieldController.js @@ -58,7 +58,6 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s * twice - once for the 0-index and once for the 7 index. */ $scope.weekDays = [ - { id : '0', day : 'Sunday' }, { id : '1', day : 'Monday' }, { id : '2', day : 'Tuesday' }, { id : '3', day : 'Wednesday' }, @@ -132,8 +131,16 @@ angular.module('guacRestrict').controller('timeRestrictionFieldController', ['$s if (restrictionRegex.test(restrArray[i])) { var currArray = restrArray[i].match(restrictionRegex); let entry = new TimeRestrictionEntry(); - entry.startTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[2].slice(0,2)), parseInt(currArray[2].slice(2)))); - entry.endTime = new Date(Date.UTC(templateDate.getFullYear(), templateDate.getMonth(), templateDate.getDate(), parseInt(currArray[3].slice(0,2)), parseInt(currArray[3].slice(2)))); + entry.startTime = new Date(Date.UTC(templateDate.getFullYear(), + templateDate.getMonth(), + templateDate.getDate(), + parseInt(currArray[2].slice(0,2)), + parseInt(currArray[2].slice(2)))); + entry.endTime = new Date(Date.UTC(templateDate.getFullYear(), + templateDate.getMonth(), + templateDate.getDate(), + parseInt(currArray[3].slice(0,2)), + parseInt(currArray[3].slice(2)))); var origDay = currArray[1]; if (currArray[1] === '*') From 1e04d6d3664cb336391369dcbcc018921d26ca9e Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 9 Sep 2024 11:15:59 -0400 Subject: [PATCH 10/11] GUACAMOLE-1020: Pull effective group membership from the AuthenticatedUser object. --- .../RestrictionAuthenticationProvider.java | 16 ++--- .../RestrictionVerificationService.java | 71 ++++++++++++------- .../restrict/user/RestrictedUserContext.java | 15 +++- 3 files changed, 62 insertions(+), 40 deletions(-) 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 index a2dfcdf0d..ec6236432 100644 --- 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 @@ -19,8 +19,6 @@ package org.apache.guacamole.auth.restrict; -import com.google.inject.Guice; -import com.google.inject.Injector; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.restrict.user.RestrictedUserContext; import org.apache.guacamole.net.auth.AbstractAuthenticationProvider; @@ -47,21 +45,15 @@ public class RestrictionAuthenticationProvider extends AbstractAuthenticationPro String remoteAddress = credentials.getRemoteAddress(); - // Verify identity of user - RestrictionVerificationService.verifyLoginRestrictions(context, remoteAddress); + RestrictionVerificationService.verifyLoginRestrictions(context, + authenticatedUser.getEffectiveUserGroups(), remoteAddress); // User has been verified, and authentication should be allowed to // continue - return new RestrictedUserContext(context, remoteAddress); + return new RestrictedUserContext(context, remoteAddress, + authenticatedUser.getEffectiveUserGroups()); } - @Override - public UserContext redecorate(UserContext decorated, UserContext context, - AuthenticatedUser authenticatedUser, Credentials credentials) - throws GuacamoleException { - return new RestrictedUserContext(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 index eb05e2b35..c0666cd7d 100644 --- 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 @@ -26,6 +26,7 @@ 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; @@ -214,6 +215,10 @@ public class RestrictionVerificationService { * @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. @@ -223,7 +228,9 @@ public class RestrictionVerificationService { * logging in from the current client, or if an error occurs attempting * to retrieve permissions. */ - public static void verifyHostRestrictions(UserContext context, String remoteAddress) throws GuacamoleException { + public static void verifyHostRestrictions(UserContext context, + Set effectiveUserGroups, String remoteAddress) + throws GuacamoleException { // Get the current user User currentUser = context.self(); @@ -250,7 +257,8 @@ public class RestrictionVerificationService { + currentUser.getIdentifier() +"\" is not allowed to log in from \"" + remoteAddress + "\"", - "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"); + "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST" + ); // User-level explicit allow means the user is allowed. case EXPLICIT_ALLOW: @@ -262,7 +270,7 @@ public class RestrictionVerificationService { Collection userGroups = context .getPrivileged() .getUserGroupDirectory() - .getAll(currentUser.getUserGroups().getObjects()); + .getAll(effectiveUserGroups); // Loop user's effective groups and verify restrictions for (UserGroup userGroup : userGroups) { @@ -283,7 +291,8 @@ public class RestrictionVerificationService { + remoteAddress + "\" due to restrictions on group \"" + userGroup.getIdentifier() + "\".", - "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"); + "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST" + ); // Compare the two, returning the highest-priority restriction so far. hostRestrictionResult = RestrictionType.getHigherPriority(hostRestrictionResult, grpRestrictionResult); @@ -294,14 +303,10 @@ public class RestrictionVerificationService { switch (hostRestrictionResult) { // Explicit allow was the highest result, so we log it and return, allowing the user to be logged in. case EXPLICIT_ALLOW: - LOGGER.debug("User \"{}\" is explicitly allowed from host \"{}\".", - currentUser.getIdentifier(), remoteAddress); return; // Implicit allow was the highest result, so we log it and return, allowing the user to be logged in. case IMPLICIT_ALLOW: - LOGGER.debug("User \"{}\" is implicitly allowed from host \"{}\".", - currentUser.getIdentifier(), remoteAddress); return; } @@ -309,7 +314,8 @@ public class RestrictionVerificationService { throw new TranslatableInvalidHostLoginException("User \"" + currentUser.getIdentifier() + "\" is implicitly denied at this time.", - "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"); + "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST" + ); } @@ -329,7 +335,8 @@ public class RestrictionVerificationService { * If the connection should not be allowed from the remote host from * which the user is logged in. */ - public void verifyHostRestrictions(Restrictable restrictable, String remoteAddress) throws GuacamoleException { + 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); @@ -355,12 +362,17 @@ public class RestrictionVerificationService { * 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) throws GuacamoleException { + public static void verifyTimeRestrictions(UserContext context, + Set effectiveUserGroups) throws GuacamoleException { // Retrieve the current User object associated with the UserContext User currentUser = context.self(); @@ -387,7 +399,8 @@ public class RestrictionVerificationService { throw new TranslatableInvalidTimeLoginException("User \"" + currentUser.getIdentifier() + "\" is not allowed to log in at this time.", - "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW"); + "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW" + ); // User-level explicit allow means the user is allowed. case EXPLICIT_ALLOW: @@ -399,11 +412,11 @@ public class RestrictionVerificationService { Collection userGroups = context .getPrivileged() .getUserGroupDirectory() - .getAll(currentUser.getUserGroups().getObjects()); + .getAll(effectiveUserGroups); // Loop user's effective groups and verify restrictions for (UserGroup userGroup : userGroups) { - + // Get group's attributes Map grpAttributes = userGroup.getAttributes(); @@ -418,7 +431,8 @@ public class RestrictionVerificationService { + currentUser.getIdentifier() +"\" is not allowed to log in at this time due to restrictions on group \"" + userGroup + "\".", - "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW"); + "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW" + ); // Compare the two, returning the highest-priority restriction so far. timeRestrictionResult = RestrictionType.getHigherPriority(timeRestrictionResult, grpRestrictionResult); @@ -428,17 +442,19 @@ public class RestrictionVerificationService { switch (timeRestrictionResult) { // Explicit allow was the highest result, so we log it and return, allowing the user to be logged in. case EXPLICIT_ALLOW: - LOGGER.debug("User \"{}\" is explicitly allowed at this time.", currentUser.getIdentifier()); return; // Implicit allow was the highest result, so we log it and return, allowing the user to be logged in. case IMPLICIT_ALLOW: - LOGGER.debug("User \"{}\" is implicitly allowed at this time.", currentUser.getIdentifier()); return; } // If we reach, here, we've reached an implict deny, so we throw an exception. - throw new TranslatableInvalidTimeLoginException("User \"{}\" is implicitly denied at this time.", currentUser.getIdentifier()); + throw new TranslatableInvalidTimeLoginException("User \"" + + currentUser.getIdentifier() + + "\" is implicitly denied at this time.", + "RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW" + ); } @@ -476,6 +492,10 @@ public class RestrictionVerificationService { * @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. @@ -483,10 +503,12 @@ public class RestrictionVerificationService { * @throws GuacamoleException * If any of the restrictions should prevent the user from logging in. */ - public static void verifyLoginRestrictions(UserContext context, String remoteAddress) throws GuacamoleException { + public static void verifyLoginRestrictions(UserContext context, + Set effectiveUserGroups, String remoteAddress) + throws GuacamoleException { - verifyTimeRestrictions(context); - verifyHostRestrictions(context, remoteAddress); + verifyTimeRestrictions(context, effectiveUserGroups); + verifyHostRestrictions(context, effectiveUserGroups, remoteAddress); } @@ -507,13 +529,10 @@ public class RestrictionVerificationService { * If any of the restrictions should prevent the connection from being * used by the user at the current time. */ - public void verifyConnectionRestrictions(Restrictable restrictable, String remoteAddress) - throws GuacamoleException { - + 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/user/RestrictedUserContext.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/user/RestrictedUserContext.java index 53ec773e7..943e08b07 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -55,6 +56,11 @@ public class RestrictedUserContext extends DelegatingUserContext { */ 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. @@ -64,10 +70,15 @@ public class RestrictedUserContext extends DelegatingUserContext { * * @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) { + public RestrictedUserContext(UserContext userContext, String remoteAddress, + Set effectiveUserGroups) { super(userContext); this.remoteAddress = remoteAddress; + this.effectiveUserGroups = effectiveUserGroups; } @Override @@ -174,7 +185,7 @@ public class RestrictedUserContext extends DelegatingUserContext { public boolean isValid() { try { // Verify whether or not time restrictions still apply. - RestrictionVerificationService.verifyTimeRestrictions(this); + RestrictionVerificationService.verifyTimeRestrictions(this, effectiveUserGroups); return true; } catch (GuacamoleException e) { From 95cd3867912396f1867175fae8a9531b948f7b20 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Sat, 28 Sep 2024 19:39:59 -0400 Subject: [PATCH 11/11] GUACAMOLE-1020: Make sure only admin users can modify restrictions. --- .../auth/restrict/user/RestrictedUser.java | 21 ++++++++++++++--- .../restrict/user/RestrictedUserContext.java | 23 ++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) 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 index 84e2bac50..c66032e61 100644 --- 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 @@ -43,6 +43,12 @@ public class RestrictedUser extends DelegatingUser implements Restrictable { */ 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 @@ -116,9 +122,10 @@ public class RestrictedUser extends DelegatingUser implements Restrictable { * The remote address of the client from which the current user is logged * in. */ - public RestrictedUser(User user, String remoteAddress) { + public RestrictedUser(User user, String remoteAddress, boolean hasAdmin) { super(user); this.remoteAddress = remoteAddress; + this.hasAdmin = hasAdmin; } /** @@ -133,7 +140,7 @@ public class RestrictedUser extends DelegatingUser implements Restrictable { @Override public Map getAttributes() { - + // Create independent, mutable copy of attributes Map attributes = new HashMap<>(super.getAttributes()); @@ -154,10 +161,18 @@ public class RestrictedUser extends DelegatingUser implements Restrictable { // 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); 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 index 943e08b07..3aeed5762 100644 --- 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 @@ -34,9 +34,12 @@ 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; @@ -60,7 +63,7 @@ public class RestrictedUserContext extends DelegatingUserContext { * 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. @@ -133,11 +136,25 @@ public class RestrictedUserContext extends DelegatingUserContext { @Override public Directory getUserDirectory() throws GuacamoleException { + + // Pull permissions of the current logged-in user. + Permissions currentPermissions = self().getEffectivePermissions(); + boolean isAdmin = currentPermissions.getSystemPermissions().hasPermission( + SystemPermission.Type.ADMINISTER + ); + Collection adminIdentifiers = + currentPermissions.getUserPermissions().getAccessibleObjects( + Collections.singletonList(ObjectPermission.Type.ADMINISTER), super.getUserDirectory().getIdentifiers()); + return new DecoratingDirectory(super.getUserDirectory()) { @Override - protected User decorate(User object) { - return new RestrictedUser(object, remoteAddress); + protected User decorate(User object) throws GuacamoleException { + + // Check and see if the logged in user has admin privileges - + // either system-level or for that particular object. + boolean hasAdmin = isAdmin || adminIdentifiers.contains(object.getIdentifier()); + return new RestrictedUser(object, remoteAddress, hasAdmin); } @Override