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