Merge changes from patch branch back to main.

This commit is contained in:
Michael Jumper
2024-10-02 00:02:13 -07:00
40 changed files with 3638 additions and 5 deletions

View File

@@ -0,0 +1,3 @@
src/main/resources/generated/
target/
*~

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-restrict</artifactId>
<packaging>jar</packaging>
<version>1.6.0</version>
<name>guacamole-auth-restrict</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>extensions</artifactId>
<version>1.6.0</version>
<relativePath>../</relativePath>
</parent>
<build>
<plugins>
<!-- Pre-cache Angular templates with maven-angular-plugin -->
<plugin>
<groupId>com.keithbranton.mojo</groupId>
<artifactId>angular-maven-plugin</artifactId>
<version>0.3.4</version>
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>html2js</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceDir>${basedir}/src/main/resources</sourceDir>
<include>**/*.html</include>
<target>${basedir}/src/main/resources/generated/templates-main/templates.js</target>
<prefix>app/ext/restrict</prefix>
</configuration>
</plugin>
<!-- JS/CSS Minification Plugin -->
<plugin>
<groupId>com.github.buckelieg</groupId>
<artifactId>minify-maven-plugin</artifactId>
<executions>
<execution>
<id>default-cli</id>
<configuration>
<charset>UTF-8</charset>
<webappSourceDir>${basedir}/src/main/resources</webappSourceDir>
<webappTargetDir>${project.build.directory}/classes</webappTargetDir>
<cssSourceDir>/</cssSourceDir>
<cssTargetDir>/</cssTargetDir>
<cssFinalFile>restrict.css</cssFinalFile>
<cssSourceFiles>
<cssSourceFile>license.txt</cssSourceFile>
</cssSourceFiles>
<cssSourceIncludes>
<cssSourceInclude>**/*.css</cssSourceInclude>
</cssSourceIncludes>
<jsSourceDir>/</jsSourceDir>
<jsTargetDir>/</jsTargetDir>
<jsFinalFile>restrict.js</jsFinalFile>
<jsSourceFiles>
<jsSourceFile>license.txt</jsSourceFile>
</jsSourceFiles>
<jsSourceIncludes>
<jsSourceInclude>**/*.js</jsSourceInclude>
</jsSourceIncludes>
<!-- Do not minify and include tests -->
<jsSourceExcludes>
<jsSourceExclude>**/*.test.js</jsSourceExclude>
</jsSourceExcludes>
<jsEngine>CLOSURE</jsEngine>
<!-- Disable warnings for JSDoc annotations -->
<closureWarningLevels>
<misplacedTypeAnnotation>OFF</misplacedTypeAnnotation>
<nonStandardJsDocs>OFF</nonStandardJsDocs>
</closureWarningLevels>
</configuration>
<goals>
<goal>minify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.6.0</version>
<scope>provided</scope>
</dependency>
<!-- Guava - Utility Library -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- Guacamole depends on an implementation of JAX-WS -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<!-- Library for unified IPv4/6 parsing and validation -->
<dependency>
<groupId>com.github.seancfoley</groupId>
<artifactId>ipaddress</artifactId>
<version>5.5.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<!-- Output tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include licenses and extension .jar -->
<fileSets>
<!-- Include licenses -->
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,67 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict;
import org.apache.guacamole.calendar.RestrictionType;
import org.apache.guacamole.net.auth.Attributes;
/**
* An interface which defines methods that apply to items that can have
* restrictions applied to them.
*/
public interface Restrictable extends Attributes {
/**
* Return the restriction state for this restrictable object at the
* current date and time. By default returns an implicit denial.
*
* @return
* The restriction status for the current date and time.
*/
default public RestrictionType getCurrentTimeRestriction() {
return RestrictionType.IMPLICIT_DENY;
}
/**
* Return the restriction state for this restrictable object for the host
* from which the current user is logged in. By default returns an implicit
* denial.
*
* @return
* The restriction status for the host from which the current user is
* logged in.
*/
default public RestrictionType getCurrentHostRestriction() {
return RestrictionType.IMPLICIT_DENY;
}
/**
* Returns true if the current item is available based on the restrictions
* for the given implementation of this interface, or false if the item is
* not currently available. The default implementation checks current time
* and host restrictions, allowing if both those restrictions allow access.
*
* @return
* true if the item is available, otherwise false.
*/
default public boolean isAvailable() {
return (getCurrentTimeRestriction().isAllowed() && getCurrentHostRestriction().isAllowed());
}
}

View File

@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.restrict.user.RestrictedUserContext;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
/**
* AuthenticationProvider implementation which provides additional restrictions
* for users, groups of users, connections, and connection groups, allowing
* administrators to further control access to Guacamole resources.
*/
public class RestrictionAuthenticationProvider extends AbstractAuthenticationProvider {
@Override
public String getIdentifier() {
return "restrict";
}
@Override
public UserContext decorate(UserContext context,
AuthenticatedUser authenticatedUser, Credentials credentials)
throws GuacamoleException {
String remoteAddress = credentials.getRemoteAddress();
// Verify identity of user
RestrictionVerificationService.verifyLoginRestrictions(context,
authenticatedUser.getEffectiveUserGroups(), remoteAddress);
// User has been verified, and authentication should be allowed to
// continue
return new RestrictedUserContext(context, remoteAddress,
authenticatedUser.getEffectiveUserGroups());
}
}

View File

@@ -0,0 +1,538 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict;
import inet.ipaddr.HostName;
import inet.ipaddr.HostNameException;
import inet.ipaddr.IPAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.restrict.connection.RestrictedConnection;
import org.apache.guacamole.auth.restrict.user.RestrictedUser;
import org.apache.guacamole.auth.restrict.usergroup.RestrictedUserGroup;
import org.apache.guacamole.calendar.DailyRestriction;
import org.apache.guacamole.calendar.RestrictionType;
import org.apache.guacamole.calendar.TimeRestrictionParser;
import org.apache.guacamole.host.HostRestrictionParser;
import org.apache.guacamole.language.TranslatableGuacamoleSecurityException;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup;
import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for verifying additional user login restrictions against a given
* login attempt.
*/
public class RestrictionVerificationService {
/**
* Logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(RestrictionVerificationService.class);
/**
* Parse out the provided strings of allowed and denied times, verifying
* whether or not a login or connection should be allowed at the current
* day and time, and returning the appropriate restriction type.
*
* @param allowedTimeString
* The string containing the times that should be parsed to determine if
* the login or connection should be allowed at the current time, or
* null or an empty string if there are no specific allowed times defined.
*
* @param deniedTimeString
* The string containing the times that should be parsed to determine if
* the login or connection should be denied at the current time, or null
* or an empty string if there are no specific times during which a
* action should be denied.
*
* @return
* A RestrictionType based on the provided allowed and denied strings.
*/
public static RestrictionType allowedByTimeRestrictions(String allowedTimeString,
String deniedTimeString) {
// Check for denied entries, first, returning the explicit deny if the
// login or connection should not be allowed.
if (deniedTimeString != null && !deniedTimeString.isEmpty()) {
List<DailyRestriction> deniedTimes =
TimeRestrictionParser.parseString(deniedTimeString);
for (DailyRestriction restriction : deniedTimes) {
if (restriction.appliesNow())
return RestrictionType.EXPLICIT_DENY;
}
}
// If no allowed entries are present, return the implicit allow, allowing
// the login or connection to continue.
if (allowedTimeString == null || allowedTimeString.isEmpty())
return RestrictionType.IMPLICIT_ALLOW;
// Pull the list of allowed times.
List<DailyRestriction> allowedTimes =
TimeRestrictionParser.parseString(allowedTimeString);
// Allowed entries are present, loop through them and check for a valid time.
for (DailyRestriction restriction : allowedTimes) {
// If this time allows the login or connection return the explicit allow.
if (restriction.appliesNow())
return RestrictionType.EXPLICIT_ALLOW;
}
// We have allowed entries, but login hasn't matched, so implicitly deny it.
return RestrictionType.IMPLICIT_DENY;
}
/**
* Given the strings of allowed and denied hosts, verify that the login or
* connection should be allowed from the given remote address, returning
* the RestrictionType that matches the provided allowed and denied strings.
*
* @param allowedHostsString
* The string containing a semicolon-separated list of hosts from
* which the login or connection should be allowed, or null or an empty
* string if no specific set of allowed hosts is defined.
*
* @param deniedHostsString
* The string containing a semicolon-separated list of hosts from
* which the login or connection should be denied, or null or an empty
* string if no specific set of denied hosts is defined.
*
* @param remoteAddress
* The IP address from which the user is logging in or has logged in
* and is attempting to connect from, if it is known. If it is unknown
* and restrictions are defined, the login or connection will be denied.
*
* @return
* A RestrictionType that matches the provided allow and deny strings.
*/
public static RestrictionType allowedByHostRestrictions(String allowedHostsString,
String deniedHostsString, String remoteAddress) {
// Convert the string to a HostName
HostName remoteHostName = new HostName(remoteAddress);
// If attributes do not exist or are empty then the action is allowed.
if ((allowedHostsString == null || allowedHostsString.isEmpty())
&& (deniedHostsString == null || deniedHostsString.isEmpty()))
return RestrictionType.IMPLICIT_ALLOW;
// If the remote address cannot be determined, and restrictions are
// in effect, log an error and deny the action.
if (remoteAddress == null || remoteAddress.isEmpty()) {
LOGGER.warn("Host-based restrictions are present, but the remote "
+ "address is invalid or could not be resolved. "
+ "The action will not be allowed.");
return RestrictionType.IMPLICIT_DENY;
}
// Split denied hosts attribute and process each entry, checking them
// against the current remote address, and returning false if a match is
// found.
List<HostName> deniedHosts = HostRestrictionParser.parseHostList(deniedHostsString);
for (HostName hostName : deniedHosts) {
try {
if (hostName.isAddress() && hostName.toAddress().contains(remoteHostName.asAddress()))
return RestrictionType.EXPLICIT_DENY;
else
for (IPAddress currAddr : hostName.toAllAddresses())
if (currAddr.matches(remoteHostName.asAddressString()))
return RestrictionType.EXPLICIT_DENY;
}
catch (UnknownHostException | HostNameException e) {
LOGGER.warn("Unknown or invalid host in denied hosts list: \"{}\"", hostName);
LOGGER.debug("Exception while trying to resolve host: \"{}\"", hostName, e);
return RestrictionType.IMPLICIT_DENY;
}
}
// If denied hosts have been checked and allowed hosts are empty, we're
// good, and can allow the action.
if (allowedHostsString == null || allowedHostsString.isEmpty())
return RestrictionType.IMPLICIT_ALLOW;
// Run through allowed hosts, if there are any, and return, allowing the
// action if there are any matches.
List<HostName> allowedHosts = HostRestrictionParser.parseHostList(allowedHostsString);
for (HostName hostName : allowedHosts) {
try {
// If the entry is an IP or Subnet, check the remote address against it directly
if (hostName.isAddress() && hostName.toAddress().contains(remoteHostName.asAddress()))
return RestrictionType.EXPLICIT_ALLOW;
// Entry is a hostname, so resolve to IPs and check each one
for (IPAddress currAddr : hostName.toAllAddresses())
if (currAddr.matches(remoteHostName.asAddressString()))
return RestrictionType.EXPLICIT_ALLOW;
}
// If an entry cannot be resolved we will log a warning.
catch (UnknownHostException | HostNameException e) {
LOGGER.warn("Unknown host encountered in allowed host string: {}", hostName);
LOGGER.debug("Exception received trying to resolve host: {}", hostName, e);
}
}
// If we've made it here, the allowed hosts do not contain the remote
// address, and the action should not be allowed;
return RestrictionType.IMPLICIT_DENY;
}
/**
* Verify the host restrictions for the user associated with the given
* UserContext, throwing an exception if any of the restrictions result
* in the user not being allowed to be logged in to Guacamole from this
* host.
*
* @param context
* The UserContext associated with the user who is being verified.
*
* @param effectiveUserGroups
* The set of identifiers of groups of which the user who is being
* verified is a member.
*
* @param remoteAddress
* The remote address of the client from which the current user is
* logged in.
*
* @throws GuacamoleException
* If the restrictions on the user should prevent the user from
* logging in from the current client, or if an error occurs attempting
* to retrieve permissions.
*/
public static void verifyHostRestrictions(UserContext context,
Set<String> effectiveUserGroups, String remoteAddress)
throws GuacamoleException {
// Get the current user
User currentUser = context.self();
// Admins always have access.
if (currentUser.getEffectivePermissions().getSystemPermissions().hasPermission(SystemPermission.Type.ADMINISTER)) {
LOGGER.warn("User \"{}\" has System Administration permissions; additional restrictions will be bypassed.",
currentUser.getIdentifier());
return;
}
// Get user's attributes
Map<String, String> userAttributes = currentUser.getAttributes();
// Verify host-based restrictions specific to the user
String allowedHostString = userAttributes.get(RestrictedUser.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String deniedHostString = userAttributes.get(RestrictedUser.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
RestrictionType hostRestrictionResult = allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress);
switch (hostRestrictionResult) {
// User-level explicit deny overrides everything
case EXPLICIT_DENY:
throw new TranslatableInvalidHostLoginException("User \""
+ currentUser.getIdentifier()
+"\" is not allowed to log in from \""
+ remoteAddress + "\"",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"
);
// User-level explicit allow means the user is allowed.
case EXPLICIT_ALLOW:
return;
}
// Gather user's effective groups.
Collection<UserGroup> userGroups = context
.getPrivileged()
.getUserGroupDirectory()
.getAll(effectiveUserGroups);
// Loop user's effective groups and verify restrictions
for (UserGroup userGroup : userGroups) {
// Get group's attributes
Map<String, String> grpAttributes = userGroup.getAttributes();
// Pull host-based restrictions for this group and verify
String grpAllowedHostString = grpAttributes.get(RestrictedUserGroup.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String grpDeniedHostString = grpAttributes.get(RestrictedUserGroup.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
RestrictionType grpRestrictionResult = allowedByHostRestrictions(grpAllowedHostString, grpDeniedHostString, remoteAddress);
// Any explicit denials are thrown immediately
if (grpRestrictionResult == RestrictionType.EXPLICIT_DENY)
throw new TranslatableInvalidHostLoginException("User \""
+ currentUser.getIdentifier()
+ "\" is not allowed to log in from host \""
+ remoteAddress
+ "\" due to restrictions on group \""
+ userGroup.getIdentifier() + "\".",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"
);
// Compare the two, returning the highest-priority restriction so far.
hostRestrictionResult = RestrictionType.getHigherPriority(hostRestrictionResult, grpRestrictionResult);
}
// Check the result and log allowed
switch (hostRestrictionResult) {
// Explicit allow was the highest result, so we log it and return, allowing the user to be logged in.
case EXPLICIT_ALLOW:
return;
// Implicit allow was the highest result, so we log it and return, allowing the user to be logged in.
case IMPLICIT_ALLOW:
return;
}
// If we reach, here, we've reached an implict deny, so we throw an exception.
throw new TranslatableInvalidHostLoginException("User \""
+ currentUser.getIdentifier()
+ "\" is implicitly denied at this time.",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST"
);
}
/**
* Verify the host-based restrictions of the Connection, throwing an
* exception if the Connection should be allowed from the host from which
* the user is logged in.
*
* @param restrictable
* The Restrictable object that should be verified against host restrictions.
*
* @param remoteAddress
* The remote address of the client from which the current user is
* logged in.
*
* @throws GuacamoleException
* If the connection should not be allowed from the remote host from
* which the user is logged in.
*/
public static void verifyHostRestrictions(Restrictable restrictable,
String remoteAddress) throws GuacamoleException {
// Verify time-based restrictions specific to this connection.
String allowedHostsString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String deniedHostsString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
RestrictionType hostRestrictionResult = allowedByHostRestrictions(allowedHostsString, deniedHostsString, remoteAddress);
// If the host is not allowed
if (!hostRestrictionResult.isAllowed())
throw new TranslatableGuacamoleSecurityException(
"Use of this connection is not allowed from this remote host: \"" + remoteAddress + "\".",
"RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW"
);
}
/**
* Verifies the time restrictions for this extension and whether or not the
* account should be allowed to be logged in to Guacamole at the current
* day and time, throwing an exception if any of the restrictions result
* in a violation of the time constraints of the account.
*
* @param context
* The UserContext of the user whose access to Guacamole is being
* checked.
*
* @param effectiveUserGroups
* The set of identifiers of groups of which the user who is being
* verified is a member.
*
* @throws GuacamoleException
* If any of the time constraints configured for the user result in the
* user not being allowed to be logged in to Guacamole, or if errors
* occur trying to retrieve permissions or attributes.
*/
public static void verifyTimeRestrictions(UserContext context,
Set<String> effectiveUserGroups) throws GuacamoleException {
// Retrieve the current User object associated with the UserContext
User currentUser = context.self();
// Admins always have access.
if (currentUser.getEffectivePermissions().getSystemPermissions().hasPermission(SystemPermission.Type.ADMINISTER)) {
LOGGER.warn("User \"{}\" has System Administration permissions; additional restrictions will be bypassed.",
currentUser.getIdentifier());
return;
}
// Get user's attributes
Map<String, String> userAttributes = currentUser.getAttributes();
// Verify time-based restrictions specific to the user
String allowedTimeString = userAttributes.get(RestrictedUser.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String deniedTimeString = userAttributes.get(RestrictedUser.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
RestrictionType timeRestrictionResult = allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
// Check the time restriction for explicit results.
switch (timeRestrictionResult) {
// User-level explicit deny overrides everything
case EXPLICIT_DENY:
throw new TranslatableInvalidTimeLoginException("User \""
+ currentUser.getIdentifier()
+ "\" is not allowed to log in at this time.",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW"
);
// User-level explicit allow means the user is allowed.
case EXPLICIT_ALLOW:
return;
}
// Gather user's effective groups.
Collection<UserGroup> userGroups = context
.getPrivileged()
.getUserGroupDirectory()
.getAll(effectiveUserGroups);
// Loop user's effective groups and verify restrictions
for (UserGroup userGroup : userGroups) {
// Get group's attributes
Map<String, String> grpAttributes = userGroup.getAttributes();
// Pull time-based restrictions for this group and verify
String grpAllowedTimeString = grpAttributes.get(RestrictedUserGroup.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String grpDeniedTimeString = grpAttributes.get(RestrictedUserGroup.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
RestrictionType grpRestrictionResult = allowedByTimeRestrictions(grpAllowedTimeString, grpDeniedTimeString);
// An explicit deny results in immediate denial of the login.
if (grpRestrictionResult == RestrictionType.EXPLICIT_DENY)
throw new TranslatableInvalidTimeLoginException("User \""
+ currentUser.getIdentifier()
+"\" is not allowed to log in at this time due to restrictions on group \""
+ userGroup + "\".",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW"
);
// Compare the two, returning the highest-priority restriction so far.
timeRestrictionResult = RestrictionType.getHigherPriority(timeRestrictionResult, grpRestrictionResult);
}
switch (timeRestrictionResult) {
// Explicit allow was the highest result, so we log it and return, allowing the user to be logged in.
case EXPLICIT_ALLOW:
return;
// Implicit allow was the highest result, so we log it and return, allowing the user to be logged in.
case IMPLICIT_ALLOW:
return;
}
// If we reach, here, we've reached an implict deny, so we throw an exception.
throw new TranslatableInvalidTimeLoginException("User \""
+ currentUser.getIdentifier()
+ "\" is implicitly denied at this time.",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW"
);
}
/**
* Verify the time restrictions for the given Connection object, throwing
* an exception if the connection should not be allowed, or silently
* returning if the connection should be allowed.
*
* @param restrictable
* The item that supports restrictions that is to be verified against
* the current time.
*
* @throws GuacamoleException
* If the connection should not be allowed at the current time.
*/
public static void verifyTimeRestrictions(Restrictable restrictable) throws GuacamoleException {
// Verify time-based restrictions specific to this connection.
String allowedTimeString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String deniedTimeString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
RestrictionType timeRestriction = allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
if (!timeRestriction.isAllowed())
throw new TranslatableGuacamoleSecurityException(
"Use of this connection or connection group is not allowed at this time.",
"RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW"
);
}
/**
* Verifies the login restrictions supported by this extension for the user
* who is attempting to log in, throwing an exception if any of the
* restrictions result in the user not being allowed to log in.
*
* @param context
* The context of the user who is attempting to log in.
*
* @param effectiveUserGroups
* The identifiers of the UserGroups of which the user who is logging
* in is a member.
*
* @param remoteAddress
* The remote address of the client from which the current user is
* logged in.
*
* @throws GuacamoleException
* If any of the restrictions should prevent the user from logging in.
*/
public static void verifyLoginRestrictions(UserContext context,
Set<String> effectiveUserGroups, String remoteAddress)
throws GuacamoleException {
verifyTimeRestrictions(context, effectiveUserGroups);
verifyHostRestrictions(context, effectiveUserGroups, remoteAddress);
}
/**
* Verifies the connection restrictions supported by this extension for the
* connection the user is attempting to access, throwing an exception if
* any of the restrictions result in the connection being unavailable.
*
* @param restrictable
* The object that supports restrictions that is to be verified to be
* usable within the current restrictions.
*
* @param remoteAddress
* The remote address of the client from which the current user is
* logged in.
*
* @throws GuacamoleException
* If any of the restrictions should prevent the connection from being
* used by the user at the current time.
*/
public static void verifyConnectionRestrictions(Restrictable restrictable,
String remoteAddress) throws GuacamoleException {
verifyTimeRestrictions(restrictable);
verifyHostRestrictions(restrictable, remoteAddress);
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,200 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict.connection;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.restrict.Restrictable;
import org.apache.guacamole.auth.restrict.RestrictionVerificationService;
import org.apache.guacamole.auth.restrict.form.HostRestrictionField;
import org.apache.guacamole.auth.restrict.form.TimeRestrictionField;
import org.apache.guacamole.calendar.RestrictionType;
import org.apache.guacamole.form.Form;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.DelegatingConnection;
import org.apache.guacamole.protocol.GuacamoleClientInformation;
/**
* A Connection implementation that wraps another connection, providing additional
* ability to control access to the connection.
*/
public class RestrictedConnection extends DelegatingConnection implements Restrictable {
/**
* The remote address of the client from which the user logged in.
*/
private final String remoteAddress;
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that this connection can be accessed. The presence of values within this
* attribute will automatically restrict use of the connections at any
* times that are not specified.
*/
public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed";
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that this connection cannot be accessed. Denied times will always take
* precedence over allowed times. The presence of this attribute without
* guac-restrict-time-allowed will deny access only during the times listed
* in this attribute, allowing access at all other times. The presence of
* this attribute along with the guac-restrict-time-allowed attribute will
* deny access at any times that overlap with the allowed times.
*/
public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied";
/**
* The name of the attribute that contains a list of hosts from which a user
* may access this connection. The presence of this attribute will restrict
* access to only users accessing Guacamole from the list of hosts contained
* in the attribute, subject to further restriction by the
* guac-restrict-hosts-denied attribute.
*/
public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed";
/**
* The name of the attribute that contains a list of hosts from which
* a user may not access this connection. The presence of this attribute,
* absent the guac-restrict-hosts-allowed attribute, will allow access from
* all hosts except the ones listed in this attribute. The presence of this
* attribute coupled with the guac-restrict-hosts-allowed attribute will
* block access from any IPs in this list, overriding any that may be
* allowed.
*/
public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied";
/**
* The list of all connection attributes provided by this Connection implementation.
*/
public static final List<String> RESTRICT_CONNECTION_ATTRIBUTES = Arrays.asList(
RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME,
RESTRICT_TIME_DENIED_ATTRIBUTE_NAME,
RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME,
RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME
);
/**
* The form containing the list of fields for the attributes provided
* by this module.
*/
public static final Form RESTRICT_CONNECTION_FORM = new Form("restrict-login-form",
Arrays.asList(
new TimeRestrictionField(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME),
new TimeRestrictionField(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME),
new HostRestrictionField(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME),
new HostRestrictionField(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME)
)
);
/**
* Wraps the given Connection object, providing capability of further
* restricting connection access beyond the default access control provided
* by other modules.
*
* @param connection
* The Connection object to wrap.
*
* @param remoteAddress
* The remote address of the client from which the current user logged
* in.
*/
public RestrictedConnection(Connection connection, String remoteAddress) {
super(connection);
this.remoteAddress = remoteAddress;
}
/**
* Returns the original Connection object wrapped by this RestrictConnection.
*
* @return
* The wrapped Connection object.
*/
public Connection getUndecorated() {
return getDelegateConnection();
}
@Override
public Map<String, String> getAttributes() {
// Create independent, mutable copy of attributes
Map<String, String> 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<String, String> 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<String, String> tokens) throws GuacamoleException {
// Verify the restrictions for this connection.
RestrictionVerificationService.verifyConnectionRestrictions(this, remoteAddress);
// Connect
return super.connect(info, tokens);
}
@Override
public RestrictionType getCurrentTimeRestriction() {
String allowedTimeString = getAttributes().get(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String deniedTimeString = getAttributes().get(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
return RestrictionVerificationService.allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
}
@Override
public RestrictionType getCurrentHostRestriction() {
String allowedHostString = getAttributes().get(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String deniedHostString = getAttributes().get(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
return RestrictionVerificationService.allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress);
}
}

View File

@@ -0,0 +1,202 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict.connectiongroup;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.restrict.Restrictable;
import org.apache.guacamole.auth.restrict.RestrictionVerificationService;
import org.apache.guacamole.auth.restrict.form.HostRestrictionField;
import org.apache.guacamole.auth.restrict.form.TimeRestrictionField;
import org.apache.guacamole.calendar.RestrictionType;
import org.apache.guacamole.form.Form;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.DelegatingConnectionGroup;
import org.apache.guacamole.protocol.GuacamoleClientInformation;
/**
* A ConnectionGroup implementation that wraps an existing ConnectionGroup,
* providing additional ability to control access to the ConnectionGroup.
*/
public class RestrictedConnectionGroup extends DelegatingConnectionGroup implements Restrictable {
/**
* The remote address of the client from which the current user logged in.
*/
private final String remoteAddress;
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that this connection group can be accessed. The presence of values within
* this attribute will automatically restrict use of the connection group
* at any times that are not specified.
*/
public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed";
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that this connection group cannot be accessed. Denied times will always
* take precedence over allowed times. The presence of this attribute without
* guac-restrict-time-allowed will deny access only during the times listed
* in this attribute, allowing access at all other times. The presence of
* this attribute along with the guac-restrict-time-allowed attribute will
* deny access at any times that overlap with the allowed times.
*/
public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied";
/**
* The name of the attribute that contains a list of hosts from which a user
* may access this connection group. The presence of this attribute will
* restrict access to only users accessing Guacamole from the list of hosts
* contained in the attribute, subject to further restriction by the
* guac-restrict-hosts-denied attribute.
*/
public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed";
/**
* The name of the attribute that contains a list of hosts from which
* a user may not access this connection group. The presence of this
* attribute, absent the guac-restrict-hosts-allowed attribute, will allow
* access from all hosts except the ones listed in this attribute. The
* presence of this attribute coupled with the guac-restrict-hosts-allowed
* attribute will block access from any hosts in this list, overriding any
* that may be allowed.
*/
public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied";
/**
* The list of all connection group attributes provided by this
* ConnectionGroup implementation.
*/
public static final List<String> RESTRICT_CONNECTIONGROUP_ATTRIBUTES = Arrays.asList(
RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME,
RESTRICT_TIME_DENIED_ATTRIBUTE_NAME,
RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME,
RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME
);
/**
* The form containing the list of fields for the attributes provided
* by this ConnectionGroup implementation.
*/
public static final Form RESTRICT_CONNECTIONGROUP_FORM = new Form("restrict-login-form",
Arrays.asList(
new TimeRestrictionField(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME),
new TimeRestrictionField(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME),
new HostRestrictionField(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME),
new HostRestrictionField(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME)
)
);
/**
* Wraps the given ConnectionGroup object, providing capability of further
* restricting connection group access beyond the default access control
* provided by other modules.
*
* @param connectionGroup
* The ConnectionGroup object to wrap.
*
* @param remoteAddress
* The remote address of the client from which the current user logged
* in.
*/
public RestrictedConnectionGroup(ConnectionGroup connectionGroup, String remoteAddress) {
super(connectionGroup);
this.remoteAddress = remoteAddress;
}
/**
* Returns the original ConnectionGroup object wrapped by this
* RestrictConnectionGroup.
*
* @return
* The wrapped ConnectionGroup object.
*/
public ConnectionGroup getUndecorated() {
return getDelegateConnectionGroup();
}
@Override
public Map<String, String> getAttributes() {
// Create independent, mutable copy of attributes
Map<String, String> 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<String, String> 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<String, String> tokens) throws GuacamoleException {
// Verify restrictions for this connection group.
RestrictionVerificationService.verifyConnectionRestrictions(this, remoteAddress);
// Connect
return super.connect(info, tokens);
}
@Override
public RestrictionType getCurrentTimeRestriction() {
String allowedTimeString = getAttributes().get(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String deniedTimeString = getAttributes().get(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
return RestrictionVerificationService.allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
}
@Override
public RestrictionType getCurrentHostRestriction() {
String allowedHostString = getAttributes().get(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String deniedHostString = getAttributes().get(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
return RestrictionVerificationService.allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict.form;
import org.apache.guacamole.form.Field;
/**
* A field that parses a string containing time restrictions into its individual
* components for user-friendly display on the web interface.
*/
public class TimeRestrictionField extends Field {
/**
* The field type.
*/
public static final String FIELD_TYPE = "GUAC_TIME_RESTRICTION";
/**
* Create a new field that tracks time restrictions.
*
* @param name
* The name of the parameter that will be used to pass this field
* between the REST API and the web front-end.
*
*/
public TimeRestrictionField(String name) {
super(name, FIELD_TYPE);
}
}

View File

@@ -0,0 +1,199 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict.user;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.guacamole.auth.restrict.Restrictable;
import org.apache.guacamole.auth.restrict.RestrictionVerificationService;
import org.apache.guacamole.auth.restrict.form.HostRestrictionField;
import org.apache.guacamole.auth.restrict.form.TimeRestrictionField;
import org.apache.guacamole.calendar.RestrictionType;
import org.apache.guacamole.form.Form;
import org.apache.guacamole.net.auth.DelegatingUser;
import org.apache.guacamole.net.auth.User;
/**
* User implementation which wraps a User from another extension and enforces
* additional restrictions.
*/
public class RestrictedUser extends DelegatingUser implements Restrictable {
/**
* The remote address of the client from which the current user is logged in.
*/
private final String remoteAddress;
/**
* true if the user logged in to Guacamole has administrative privileges
* for this user object, otherwise false.
*/
private final boolean hasAdmin;
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that a user is allowed to log in. The presence of this attribute will
* restrict the user to logins only during the times that are contained
* within the attribute, subject to further restriction by the
* guac-restrict-time-denied attribute.
*/
public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed";
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that a user is not allowed to log in. Denied times will always take
* precedence over allowed times. The presence of this attribute without
* guac-restrict-time-allowed will deny logins only during the times listed
* in this attribute, allowing logins at all other times. The presence of
* this attribute along with the guac-restrict-time-allowed attribute will
* deny logins at any times that overlap with the allowed times.
*/
public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied";
/**
* The name of the attribute that contains a list of IP addresses from which
* a user is allowed to log in. The presence of this attribute will restrict
* users to only the list of IP addresses contained in the attribute, subject
* to further restriction by the guac-restrict-hosts-denied attribute.
*/
public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed";
/**
* The name of the attribute that contains a list of IP addresses from which
* a user is not allowed to log in. The presence of this attribute, absent
* the guac-restrict-hosts-allowed attribute, will allow logins from all
* hosts except the ones listed in this attribute. The presence of this
* attribute coupled with the guac-restrict-hosts-allowed attribute will
* block access from any IPs in this list, overriding any that may be
* allowed.
*/
public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied";
/**
* The list of all user attributes provided by this User implementation.
*/
public static final List<String> RESTRICT_USER_ATTRIBUTES = Arrays.asList(
RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME,
RESTRICT_TIME_DENIED_ATTRIBUTE_NAME,
RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME,
RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME
);
/**
* The form containing the list of fields for the attributes provided
* by this module.
*/
public static final Form RESTRICT_LOGIN_FORM = new Form("restrict-login-form",
Arrays.asList(
new TimeRestrictionField(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME),
new TimeRestrictionField(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME),
new HostRestrictionField(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME),
new HostRestrictionField(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME)
)
);
/**
* Wraps the given User object, providing capability of further restricting
* logins beyond the default restrictions provided by default modules.
*
* @param user
* The User object to wrap.
*
* @param remoteAddress
* The remote address of the client from which the current user is logged
* in.
*/
public RestrictedUser(User user, String remoteAddress, boolean hasAdmin) {
super(user);
this.remoteAddress = remoteAddress;
this.hasAdmin = hasAdmin;
}
/**
* Returns the User object wrapped by this RestrictUser.
*
* @return
* The wrapped User object.
*/
public User getUndecorated() {
return getDelegateUser();
}
@Override
public Map<String, String> getAttributes() {
// Create independent, mutable copy of attributes
Map<String, String> 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<String, String> attributes) {
// Create independent, mutable copy of attributes
attributes = new HashMap<>(attributes);
// Loop through extension-specific attributes, only sending ones
// that are non-null and non-empty to the underlying storage mechanism.
for (String attribute : RESTRICT_USER_ATTRIBUTES) {
/* If the user lacks admin access, don't set restriction attributes. */
if (!hasAdmin) {
attributes.remove(attribute);
continue;
}
/* Replace empty values with null values. */
String value = attributes.get(attribute);
if (value != null && value.isEmpty())
attributes.put(attribute, null);
}
super.setAttributes(attributes);
}
@Override
public RestrictionType getCurrentTimeRestriction() {
String allowedTimeString = getAttributes().get(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String deniedTimeString = getAttributes().get(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
return RestrictionVerificationService.allowedByTimeRestrictions(allowedTimeString, deniedTimeString);
}
@Override
public RestrictionType getCurrentHostRestriction() {
String allowedHostString = getAttributes().get(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String deniedHostString = getAttributes().get(RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
return RestrictionVerificationService.allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress);
}
}

View File

@@ -0,0 +1,214 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict.user;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.restrict.RestrictionVerificationService;
import org.apache.guacamole.auth.restrict.connection.RestrictedConnection;
import org.apache.guacamole.auth.restrict.connectiongroup.RestrictedConnectionGroup;
import org.apache.guacamole.auth.restrict.usergroup.RestrictedUserGroup;
import org.apache.guacamole.form.Form;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.DecoratingDirectory;
import org.apache.guacamole.net.auth.DelegatingUserContext;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.Permissions;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A UserContext implementation for additional login and connection restrictions
* which wraps the UserContext of some other extension.
*/
public class RestrictedUserContext extends DelegatingUserContext {
/**
* The logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(RestrictedUserContext.class);
/**
* The remote address from which this user logged in.
*/
private final String remoteAddress;
/**
* The identifiers effective groups of the user associated with this context.
*/
private final Set<String> effectiveUserGroups;
/**
* Creates a new RestrictedUserContext which wraps the given UserContext,
* providing additional control for user logins and connections.
*
* @param userContext
* The UserContext to wrap.
*
* @param remoteAddress
* The address the user is logging in from, if known.
*
* @param effectiveUserGroups
* The identifiers of the groups this user is associated with.
*/
public RestrictedUserContext(UserContext userContext, String remoteAddress,
Set<String> effectiveUserGroups) {
super(userContext);
this.remoteAddress = remoteAddress;
this.effectiveUserGroups = effectiveUserGroups;
}
@Override
public Directory<Connection> getConnectionDirectory() throws GuacamoleException {
return new DecoratingDirectory<Connection>(super.getConnectionDirectory()) {
@Override
protected Connection decorate(Connection object) throws GuacamoleException {
return new RestrictedConnection(object, remoteAddress);
}
@Override
protected Connection undecorate(Connection object) {
assert(object instanceof RestrictedConnection);
return ((RestrictedConnection) object).getUndecorated();
}
};
}
@Override
public Collection<Form> getConnectionAttributes() {
Collection<Form> connectionAttrs = new HashSet<>(super.getConnectionAttributes());
connectionAttrs.add(RestrictedConnection.RESTRICT_CONNECTION_FORM);
return Collections.unmodifiableCollection(connectionAttrs);
}
@Override
public Directory<ConnectionGroup> getConnectionGroupDirectory() throws GuacamoleException {
return new DecoratingDirectory<ConnectionGroup>(super.getConnectionGroupDirectory()) {
@Override
protected ConnectionGroup decorate(ConnectionGroup object) throws GuacamoleException {
return new RestrictedConnectionGroup(object, remoteAddress);
}
@Override
protected ConnectionGroup undecorate(ConnectionGroup object) {
assert(object instanceof RestrictedConnectionGroup);
return ((RestrictedConnectionGroup) object).getUndecorated();
}
};
}
@Override
public Collection<Form> getConnectionGroupAttributes() {
Collection<Form> connectionGroupAttrs = new HashSet<>(super.getConnectionGroupAttributes());
connectionGroupAttrs.add(RestrictedConnectionGroup.RESTRICT_CONNECTIONGROUP_FORM);
return Collections.unmodifiableCollection(connectionGroupAttrs);
}
@Override
public Directory<User> getUserDirectory() throws GuacamoleException {
// Pull permissions of the current logged-in user.
Permissions currentPermissions = self().getEffectivePermissions();
boolean isAdmin = currentPermissions.getSystemPermissions().hasPermission(
SystemPermission.Type.ADMINISTER
);
Collection<String> adminIdentifiers =
currentPermissions.getUserPermissions().getAccessibleObjects(
Collections.singletonList(ObjectPermission.Type.ADMINISTER), super.getUserDirectory().getIdentifiers());
return new DecoratingDirectory<User>(super.getUserDirectory()) {
@Override
protected User decorate(User object) throws GuacamoleException {
// Check and see if the logged in user has admin privileges -
// either system-level or for that particular object.
boolean hasAdmin = isAdmin || adminIdentifiers.contains(object.getIdentifier());
return new RestrictedUser(object, remoteAddress, hasAdmin);
}
@Override
protected User undecorate(User object) {
assert(object instanceof RestrictedUser);
return ((RestrictedUser) object).getUndecorated();
}
};
}
@Override
public Collection<Form> getUserAttributes() {
Collection<Form> userAttrs = new HashSet<>(super.getUserAttributes());
userAttrs.add(RestrictedUser.RESTRICT_LOGIN_FORM);
return Collections.unmodifiableCollection(userAttrs);
}
@Override
public Directory<UserGroup> getUserGroupDirectory() throws GuacamoleException {
return new DecoratingDirectory<UserGroup>(super.getUserGroupDirectory()) {
@Override
protected UserGroup decorate(UserGroup object) {
return new RestrictedUserGroup(object);
}
@Override
protected UserGroup undecorate(UserGroup object) {
assert(object instanceof RestrictedUserGroup);
return ((RestrictedUserGroup) object).getUndecorated();
}
};
}
@Override
public Collection<Form> getUserGroupAttributes() {
Collection<Form> userGroupAttrs = new HashSet<>(super.getUserGroupAttributes());
userGroupAttrs.add(RestrictedUserGroup.RESTRICT_LOGIN_FORM);
return Collections.unmodifiableCollection(userGroupAttrs);
}
@Override
public boolean isValid() {
try {
// Verify whether or not time restrictions still apply.
RestrictionVerificationService.verifyTimeRestrictions(this, effectiveUserGroups);
return true;
}
catch (GuacamoleException e) {
LOGGER.debug("User account is now restricted and is no longer valid", e);
return false;
}
}
}

View File

@@ -0,0 +1,160 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict.usergroup;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.guacamole.auth.restrict.form.HostRestrictionField;
import org.apache.guacamole.auth.restrict.form.TimeRestrictionField;
import org.apache.guacamole.form.Form;
import org.apache.guacamole.net.auth.DelegatingUserGroup;
import org.apache.guacamole.net.auth.UserGroup;
/**
* UserGroup implementation which wraps a UserGroup from another extension and
* enforces additional restrictions for members of that group.
*/
public class RestrictedUserGroup extends DelegatingUserGroup {
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that members of a group are allowed to log in. The presence of this
* attribute will restrict any users who are members of the group to logins
* only during the times that are contained within the attribute,
* subject to further restriction by the guac-restrict-time-denied attribute.
*/
public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed";
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that members of a group are not allowed to log in. Denied times will
* always take precedence over allowed times. The presence of this attribute
* without guac-restrict-time-allowed will deny logins only during the times
* listed in this attribute, allowing logins at all other times. The
* presence of this attribute along with the guac-restrict-time-allowed
* attribute will deny logins at any times that overlap with the allowed
* times.
*/
public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied";
/**
* The name of the attribute that contains a list of IP addresses from which
* members of a group are allowed to log in. The presence of this attribute
* will restrict users to only the list of IP addresses contained in the
* attribute, subject to further restriction by the
* guac-restrict-hosts-denied attribute.
*/
public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed";
/**
* The name of the attribute that contains a list of IP addresses from which
* members of a group are not allowed to log in. The presence of this
* attribute, absent the guac-restrict-hosts-allowed attribute, will allow
* logins from all hosts except the ones listed in this attribute. The
* presence of this attribute coupled with the guac-restrict-hosts-allowed
* attribute will block access from any IPs in this list, overriding any
* that may be allowed.
*/
public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied";
/**
* The list of all user attributes provided by this UserGroup implementation.
*/
public static final List<String> 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 RestrictedUserGroup(UserGroup userGroup) {
super(userGroup);
}
/**
* Returns the UserGroup object wrapped by this RestrictUserGroup.
*
* @return
* The wrapped UserGroup object.
*/
public UserGroup getUndecorated() {
return getDelegateUserGroupGroup();
}
@Override
public Map<String, String> getAttributes() {
// Create independent, mutable copy of attributes
Map<String, String> 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<String, String> 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);
}
}

View File

@@ -0,0 +1,140 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.calendar;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
/**
* A class that stores a daily time restriction that can be used to determine
* whether or not a user can log in on a certain day of the week and during
* a certain time window.
*/
public class DailyRestriction {
/**
* The days of the week that this restriction applies to.
*/
private final List<DayOfWeek> 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<DayOfWeek> 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<DayOfWeek> 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(ZoneId.of("UTC")).getDayOfWeek();
LocalTime currentTime = LocalTime.now(ZoneId.of("UTC"));
// If end time is less than the start time, we check the remainder of this
// day and the beginning of the next day.
if (endTime.isBefore(startTime)) {
if (weekDays.contains(currentDay) && currentTime.isAfter(startTime) && currentTime.isBefore(LocalTime.MAX))
return true;
return (weekDays.contains(currentDay.plus(1)) && currentTime.isAfter(LocalTime.MIDNIGHT) && currentTime.isBefore(endTime));
}
// Check that we are in the specified time restriction
return (weekDays.contains(currentDay) && currentTime.isAfter(startTime) && currentTime.isBefore(endTime));
}
}

View File

@@ -0,0 +1,109 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.calendar;
/**
* A data type that represents various values of what type of restriction applies
* at a given time.
*/
public enum RestrictionType {
/**
* Access is explicitly allowed.
*/
EXPLICIT_ALLOW(1, true),
/**
* Access is explicitly denied.
*/
EXPLICIT_DENY(0, false),
/**
* Access has not been explicitly allowed or denied, therefore it is
* implicitly allowed.
*/
IMPLICIT_ALLOW(3, true),
/**
* Access has not been explicitly allowed or denied, therefore it is
* implicitly denied.
*/
IMPLICIT_DENY(2, false);
/**
* The overall priority of the restriction, with zero being the highest
* priority and the priority decreasing as numbers increase from zero.
*/
final private int priority;
/**
* true if the restriction allows access, otherwise false.
*/
final private boolean allowed;
/**
* Create the new instance of this RestrictionType, with the given
* priority value for the instance.
*
* @param priority
* The priority of the restriction type, where zero is the highest
* priority.
*
* @param allowed
* true if the restriction allows access, otherwise false.
*/
RestrictionType(int priority, boolean allowed) {
this.priority = priority;
this.allowed = allowed;
}
/**
* Evaluates two restrictions, returning the higher priority of the two.
*
* @param restriction1
* The first restriction to compare.
*
* @param restriction2
* The second restriction to compare.
*
* @return
* Return which of the two restrictions is the higher-priority.
*/
public static RestrictionType getHigherPriority(RestrictionType restriction1, RestrictionType restriction2) {
// If the second is higher than the first, return the second.
if (restriction1.priority > restriction2.priority)
return restriction2;
// Return the first.
return restriction1;
}
/**
* Returns true if this restriction allows access, otherwise false.
*
* @return
* true if this restriction allows access, otherwise false.
*/
public boolean isAllowed() {
return this.allowed;
}
}

View File

@@ -0,0 +1,157 @@
/*
* 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.
*
* <p>Examples of valid restrictions are as follows:
* <ul>
* <li>1:0700-1700 - Monday from 07:00 to 17:00
* <li>7:0000-2359 - Sunday, all day (00:00 to 23:59)
* <li>*:0900-1700 - Every day, 09:00 to 17:00
* <li>6:0900-1600;7:1200-1300 - Saturday, 09:00 to 16:00, and Sunday,
* 12:00 - 13:00
* </ul>
*/
private static final Pattern RESTRICTION_REGEX =
Pattern.compile("(?:^|;)+([0-7*])(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\\-((?:[01][0-9]|2[0-3])[0-5][0-9]))+");
/**
* The RegEx group that contains the start day-of-week of the restriction.
*/
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 all days of the week.
*/
private static final List<DayOfWeek> 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<DailyRestriction> parseString(String restrictionString) {
List<DailyRestriction> 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;
// A specific day of the week.
default:
int dayInt = Integer.parseInt(dayString);
// While JavaScript sees Sunday as "0" and "7", DayOfWeek
// does not, so we'll convert it to "7" in order to process it.
if (dayInt == 0)
dayInt = 7;
restrictions.add(new DailyRestriction(DayOfWeek.of(dayInt), startTime, endTime));
}
}
// Return the list of restrictions
return restrictions;
}
}

View File

@@ -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<HostName> parseHostList(String hostString) {
List<HostName> 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;
}
}

View File

@@ -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'
});
}]);

View File

@@ -0,0 +1,170 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for host restriction fields, which are used to configure a
* hostname, IP address, or CIDR range, that this restriction applies to.
*/
angular.module('guacRestrict').controller('hostRestrictionFieldController', ['$scope', '$injector',
function hostRestrictionFieldController($scope, $injector) {
// Required types
const HostRestrictionEntry = $injector.get('HostRestrictionEntry');
/**
* Options which dictate the behavior of the input field model, as defined
* by https://docs.angularjs.org/api/ng/directive/ngModelOptions
*
* @type Object.<String, String>
*/
$scope.modelOptions = {
/**
* Space-delimited list of events on which the model will be updated.
*
* @type String
*/
updateOn : 'blur',
/**
* The time zone to use when reading/writing the Date object of the
* model.
*
* @type String
*/
timezone : 'UTC'
};
/**
* The restrictions, as objects, that are used by the HTML template to
* present the restrictions to the user via the web interface.
*
* @type HostRestrictionEntry[]
*/
$scope.restrictions = [];
/**
* Remove the current entry from the list.
*
* @param {HostRestrictionEntry} entry
* A restriction entry.
*/
$scope.removeEntry = function removeEntry(entry) {
if (entry === null || entry.$$hashKey === '') {
return;
}
for (let i = 0; i < $scope.restrictions.length; i++) {
if ($scope.restrictions[i].$$hashKey === entry.$$hashKey) {
$scope.restrictions.splice(i,1);
return;
}
}
};
/**
* Add an empty entry to the restriction list.
*/
$scope.addEntry = function addEntry() {
$scope.restrictions.push(new HostRestrictionEntry());
};
/**
* Parse the provided string into an array containing the objects that
* represent each of entries that can then be displayed as a more
* user-friendly field.
*
* @param {String} restrString
* The string that contains the restrictions, un-parsed and as stored
* in the underlying field.
*
* @returns {HostRestrictionEntry[]}
* An array of objects that represents each of the entries as parsed
* out of the string field, and which can be interpreted by the
* AngularJS field for display.
*/
const parseRestrictions = function parseRestrictions(restrString) {
var restrictions = [];
// If the string is null or empty, just return an empty array
if (restrString === null || restrString === "")
return restrictions;
// Set up the RegEx and split the string using the separator.
var restrArray = restrString.split(";");
// Loop through split string and process each item
for (let i = 0; i < restrArray.length; i++) {
var entry = new HostRestrictionEntry();
entry.host = restrArray[i];
restrictions.push(entry);
}
return restrictions;
};
/**
* Parse the restrictions in the field into a string that can be stored
* in an underlying module.
*
* @param {HostRestrictionEntry[]} restrictions
* The array of restrictions that will be converted to a string.
*
* @returns {String}
* The string containing the restriction data that can be stored in e.g.
* a database.
*/
const storeRestrictions = function storeRestrictions(restrictions) {
// If there are no members of the array, just return an empty string.
if (restrictions === null || restrictions.length < 1)
return '';
var restrString = '';
for (let i = 0; i < restrictions.length; i++) {
// If any of the properties are not defined, skip this one.
if (!Object.hasOwn(restrictions[i], 'host')
|| restrictions[i].host === null)
continue;
// If this is not the first item, then add a semi-colon separator
if (restrString.length > 0)
restrString += ';';
// Add the current host to the list
restrString += restrictions[i].host;
}
return restrString;
};
// Update the field when the model changes.
$scope.$watch('model', function modelChanged(model) {
$scope.restrictions = parseRestrictions(model);
});
// Update string value in model when web form is changed
$scope.$watch('restrictions', function restrictionsChanged(restrictions) {
$scope.model = storeRestrictions(restrictions);
}, true);
}]);

View File

@@ -0,0 +1,317 @@
/*
* 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.<String, String>
*/
$scope.modelOptions = {
/**
* Space-delimited list of events on which the model will be updated.
*
* @type String
*/
updateOn : 'blur'
};
/**
* The restrictions, as objects, that are used by the HTML template to
* present the restrictions to the user via the web interface.
*
* @type TimeRestrictionEntry[]
*/
$scope.restrictions = [];
/**
* Map of weekday identifier to display name. Note that Sunday occurs
* twice - once for the 0-index and once for the 7 index.
*/
$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' }
];
/**
* Remove the current entry from the list.
*
* @param {TimeRestrictionEntry} entry
* A restriction entry.
*/
$scope.removeEntry = function removeEntry(entry) {
if (entry === null || entry.$$hashKey === '') {
return;
}
for (let i = 0; i < $scope.restrictions.length; i++) {
if ($scope.restrictions[i].$$hashKey === entry.$$hashKey) {
$scope.restrictions.splice(i,1);
return;
}
}
};
/**
* Add an empty entry to the restriction list.
*/
$scope.addEntry = function addEntry() {
$scope.restrictions.push(new TimeRestrictionEntry());
};
/**
* Parse the provided string into an array containing the objects that
* represent each of entries that can then be displayed as a more
* user-friendly field.
*
* @param {String} restrString
* The string that contains the restrictions, un-parsed and as stored
* in the underlying field.
*
* @returns {TimeRestrictionEntry[]}
* An array of objects that represents each of the entries as parsed
* out of the string field, and which can be interpreted by the
* AngularJS field for display.
*/
const parseRestrictions = function parseRestrictions(restrString) {
// Array to store the restrictions
var restrictions = [];
// Grab the current date so that we can accurately parse DST later
var templateDate = new Date();
// If the string is null or empty, just return an empty array
if (restrString === null || restrString === "")
return restrictions;
// Set up the RegEx and split the string using the separator.
const restrictionRegex = new RegExp('^([0-7*])(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\-((?:[01][0-9]|2[0-3])[0-5][0-9]))$');
var restrArray = restrString.split(";");
// Loop through split string and process each item
for (let i = 0; i < restrArray.length; i++) {
// Test if our regex matches
if (restrictionRegex.test(restrArray[i])) {
var currArray = restrArray[i].match(restrictionRegex);
let entry = new TimeRestrictionEntry();
entry.startTime = new Date(Date.UTC(templateDate.getFullYear(),
templateDate.getMonth(),
templateDate.getDate(),
parseInt(currArray[2].slice(0,2)),
parseInt(currArray[2].slice(2))));
entry.endTime = new Date(Date.UTC(templateDate.getFullYear(),
templateDate.getMonth(),
templateDate.getDate(),
parseInt(currArray[3].slice(0,2)),
parseInt(currArray[3].slice(2))));
var origDay = currArray[1];
if (currArray[1] === '*')
entry.weekDay = '' + currArray[1];
else {
// If UTC day is greater than local day, we subtract a day,
// wrapping as required.
if (entry.startTime.getDay() < entry.startTime.getUTCDay()) {
if (origDay <= 0)
entry.weekDay = '' + 6;
else
entry.weekDay = '' + (--origDay);
}
// If UTC day is less than local day, we add a day,
// wrapping as required.
else if (entry.startTime.getDay() > entry.startTime.getUTCDay()) {
if (origDay >= 6)
entry.weekDay = '' + 0;
else
entry.weekDay = '' + (++origDay);
}
// Local day and UTC day are the same, adjust the display day
else
entry.weekDay = '' + origDay;
}
restrictions.push(entry);
}
}
return restrictions;
};
/**
* Since new Time fields in HTML get a default year of 1970, we need to
* merge the hours and minutes from the time field into the current Date,
* primarily so that Daylight Savings Time offsets are correct.
*
* @param {Date} justTime
* The Date object produced by an HTML field that contains the hours
* and minutes we need.
*
* @returns {Date}
* The Date object that merges the current calendar date with the
* hours and minutes from the HTML field.
*/
const timeToCurrentDate = function timeToCurrentDate(justTime) {
let dateAndTime = new Date();
dateAndTime.setHours(justTime.getHours());
dateAndTime.setMinutes(justTime.getMinutes());
return dateAndTime;
};
/**
* Parse the restrictions in the field into a string that can be stored
* in an underlying module.
*
* @param {TimeRestrictionEntry[]} restrictions
* The array of restrictions that will be converted to a string.
*
* @returns {String}
* The string containing the restriction data that can be stored in e.g.
* a database.
*/
const storeRestrictions = function storeRestrictions(restrictions) {
// If there are no members of the array, just return an empty string.
if (restrictions === null || restrictions.length < 1)
return '';
let restrString = '';
for (let i = 0; i < restrictions.length; i++) {
// If any of the properties are not defined, skip this one.
if (!Object.hasOwn(restrictions[i], 'weekDay')
|| restrictions[i].weekDay === null
|| restrictions[i].weekDay === ''
|| !Object.hasOwn(restrictions[i], 'startTime')
|| restrictions[i].startTime === null
|| !(restrictions[i].startTime instanceof Date)
|| !Object.hasOwn(restrictions[i], 'endTime')
|| restrictions[i].endTime === null
|| !(restrictions[i].endTime instanceof Date))
continue;
// If this is not the first item, then add a semi-colon separator
if (restrString.length > 0)
restrString += ';';
// When these fields first gets a value, the default year is 1970
// In order to avoid issues with Daylight Savings Time, we have to
// work around this.
if (restrictions[i].startTime instanceof Date && restrictions[i].startTime.getFullYear() === 1970)
restrictions[i].startTime = timeToCurrentDate(restrictions[i].startTime);
if (restrictions[i].endTime instanceof Date && restrictions[i].endTime.getFullYear() === 1970)
restrictions[i].endTime = timeToCurrentDate(restrictions[i].endTime);
// Process the start day, factoring in wrapping for local time to
// UTC adjustments.
let weekDay = restrictions[i].weekDay;
const startDay = restrictions[i].startTime.getDay();
const utcStartDay = restrictions[i].startTime.getUTCDay();
// Local day is less than UTC day, so we add a day for storing,
// wrapping around as required.
if (weekDay !== '*' && startDay < utcStartDay) {
if (weekDay >= 6)
weekDay = 0;
else
weekDay++;
}
else if (weekDay !== '*' && startDay > utcStartDay) {
if (weekDay <= 0)
weekDay = 6;
else
weekDay--;
}
let currString = '' + weekDay.toString();
currString += ':';
// Retrieve startTime hours component and add it, adding leading zero if required.
let startHours = restrictions[i].startTime.getUTCHours();
if (startHours !== null && startHours < 10)
currString += '0';
currString += startHours.toString();
// Retrieve startTime minutes component and add it, adding leading zero if required.
let startMins = restrictions[i].startTime.getUTCMinutes();
if (startMins !== null && startMins < 10)
currString += '0';
currString += startMins.toString();
currString += '-';
// Retrieve endTime hours component and add it, adding leading zero if required.
let endHours = restrictions[i].endTime.getUTCHours();
if (endHours !== null && endHours < 10)
currString += '0';
currString += endHours.toString();
// Retrieve endTime minutes component and add it, adding leading zero if required.
let endMins = restrictions[i].endTime.getUTCMinutes();
if (endMins !== null && endMins < 10)
currString += '0';
currString += endMins.toString();
// Add the newly-created string to the overall restriction string.
restrString += currString;
}
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);
}]);

View File

@@ -0,0 +1,29 @@
{
"guacamoleVersion" : "1.6.0",
"name" : "Restriction Authentication Backend",
"namespace" : "restrict",
"authProviders" : [
"org.apache.guacamole.auth.restrict.RestrictionAuthenticationProvider"
],
"translations" : [
"translations/en.json"
],
"js" : [
"restrict.min.js"
],
"css" : [
"restrict.min.css"
],
"resources" : {
"templates/hostRestrictionField.html" : "text/html",
"templates/timeRestrictionField.html" : "text/html"
}
}

View File

@@ -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.
*/

View File

@@ -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');

View File

@@ -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;
}

View File

@@ -0,0 +1,23 @@
<div class="hostField">
<table class="restrictionList" ng-show="restrictions !== null && restrictions.length > 0">
<tr>
<th>{{ 'RESTRICT.TABLE_HEADER_HOST' | translate }}</th>
<th>&nbsp;</th>
</tr>
<tr class="restrictionListItem" ng-repeat="entry in restrictions">
<td>
<input type="text" ng-model="entry.host" ng-model-options="modelOptions">
</td>
<td>
<img class="restrictionListItem"
src="images/x-red.svg"
alt="Remove entry"
ng-click="removeEntry(entry)">
</td>
</tr>
</table>
<button ng-click="addEntry()"
class="restrictionListButton">
{{ 'RESTRICT.ACTION_ADD_ENTRY' | translate }}
</button>
</div>

View File

@@ -0,0 +1,38 @@
<div class="timeRestrictionField">
<table class="restrictionList" ng-show="restrictions !== null && restrictions.length > 0">
<tr>
<th>{{ 'RESTRICT.TABLE_HEADER_DAY' | translate }}</th>
<th>{{ 'RESTRICT.TABLE_HEADER_START_TIME' | translate }}</th>
<th>{{ 'RESTRICT.TABLE_HEADER_END_TIME' | translate }}</th>
<th>&nbsp;</th>
</tr>
<tr class="restrictionListItem" ng-repeat="entry in restrictions">
<td>
<select ng-model="entry.weekDay"
ng-options="weekDay.id as weekDay.day for weekDay in weekDays"
ng-model-options="modelOptions">
</select>
</td>
<td>
<input type="time"
ng-model="entry.startTime"
ng-model-options="modelOptions">
</td>
<td>
<input type="time"
ng-model="entry.endTime"
ng-model-options="modelOptions">
</td>
<td>
<img class="restrictionListItem"
src="images/x-red.svg"
alt="Remove entry"
ng-click="removeEntry(entry)">
</td>
</tr>
</table>
<button ng-click="addEntry()"
class="restrictionListButton">
{{ 'RESTRICT.ACTION_ADD_ENTRY' | translate }}
</button>
</div>

View File

@@ -0,0 +1,67 @@
{
"DATA_SOURCE_LOGIN_RESTRICTIONS" : {
"NAME" : "Additional Restrictions"
},
"CONNECTION_ATTRIBUTES" : {
"FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which connection may be accessed:",
"FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which connection may not be accessed:",
"FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times connection is allowed to be used:",
"FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times connection may not be used:",
"SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Connection Restrictions"
},
"CONNECTION_GROUP_ATTRIBUTES" : {
"FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which connection group may be accessed:",
"FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which connection group may not be accessed:",
"FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times connection group is allowed to be used:",
"FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times connection group may not be used:",
"SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Connection Restrictions"
},
"RESTRICT" : {
"ACTION_ADD_ENTRY" : "Add Entry",
"ERROR_CONNECTION_NOT_ALLOWED_NOW" : "The connection is not available at this time.",
"ERROR_CONNECTION_NOT_ALLOWED_FROM_HOST" : "The connection is not allowed from this host.",
"ERROR_USER_LOGIN_NOT_ALLOWED_NOW" : "The login for this user is not allowed at this time.",
"ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST" : "The login for this user is not allowed from this host.",
"TABLE_HEADER_DAY" : "Day",
"TABLE_HEADER_END_TIME" : "End Time",
"TABLE_HEADER_HOST" : "Host",
"TABLE_HEADER_START_TIME" : "Start Time"
},
"USER_ATTRIBUTES" : {
"FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which user can log in:",
"FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which user may not log in:",
"FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times user is allowed to log in:",
"FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times user is denied from log in:",
"SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Login Restrictions"
},
"USER_GROUP_ATTRIBUTES" : {
"FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which members may log in:",
"FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which members may not log in:",
"FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times members are allowed to log in:",
"FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times members are denied from log in:",
"SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Login Restrictions"
}
}

View File

@@ -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;
}]);

View File

@@ -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 {string}
*/
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;
}]);

View File

@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<version>1.6.0</version>
<name>guacamole-auth-totp</name>
<url>http://guacamole.incubator.apache.org/</url>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>

View File

@@ -47,6 +47,7 @@
<module>guacamole-auth-json</module>
<module>guacamole-auth-ldap</module>
<module>guacamole-auth-quickconnect</module>
<module>guacamole-auth-restrict</module>
<module>guacamole-auth-sso</module>
<module>guacamole-auth-totp</module>

View File

@@ -18,7 +18,7 @@
#
##
## @fn 010-build-and-install-guacamole.sh
## @fn 000-build-and-install-guacamole.sh
##
## Builds the Guacamole web application and all main extensions, installing the
## resulting binaries to standard locations within the Docker image. After the

View File

@@ -18,7 +18,7 @@
#
##
## @fn 020-map-guacamole-extensions.sh
## @fn 010-map-guacamole-extensions.sh
##
## Maps all installed Guacamole extensions (built in a previous step) to their
## corresponding environment variable prefixes, adding symbolic links so that
@@ -106,6 +106,7 @@ map_extensions <<'EOF'
guacamole-auth-ldap.........................LDAP_
guacamole-auth-quickconnect.................QUICKCONNECT_
guacamole-auth-radius.......................RADIUS_
guacamole-auth-restrict.....................RESTRICT_
guacamole-auth-sso/cas......................CAS_
guacamole-auth-sso/openid...................OPENID_
guacamole-auth-sso/saml.....................SAML_

View File

@@ -18,7 +18,7 @@
#
##
## @fn 030-download-drivers.sh
## @fn 020-download-drivers.sh
##
## Downloads all JDBC drivers required by the various supported databases. Each
## downloaded driver is stored beneath /opt/guacamole/drivers, with symbolic

View File

@@ -19,7 +19,7 @@
#
##
## @fn 800-configure-features.sh
## @fn 700-configure-features.sh
##
## Automatically checks all environment variables currently set and performs
## configuration tasks related to those variabels, including installing any