GUACAMOLE-1020: Implement extension with enhanced login and connection restrictions.

This commit is contained in:
Virtually Nick
2023-03-26 17:03:58 -04:00
parent 2168b44be0
commit 1088f60a49
34 changed files with 3058 additions and 1 deletions

View File

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

View File

@@ -0,0 +1,333 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict;
import inet.ipaddr.HostName;
import inet.ipaddr.HostNameException;
import inet.ipaddr.IPAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.restrict.connection.RestrictConnection;
import org.apache.guacamole.auth.restrict.user.RestrictUser;
import org.apache.guacamole.auth.restrict.usergroup.RestrictUserGroup;
import org.apache.guacamole.calendar.DailyRestriction;
import org.apache.guacamole.calendar.TimeRestrictionParser;
import org.apache.guacamole.host.HostRestrictionParser;
import org.apache.guacamole.language.TranslatableGuacamoleSecurityException;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup;
import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for verifying additional user login restrictions against a given
* login attempt.
*/
public class RestrictionVerificationService {
/**
* Logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(RestrictionVerificationService.class);
/**
* Parse out the provided strings of allowed and denied times, verifying
* whether or not a login or connection should be allowed at the current
* day and time. A boolean true will be returned if the action should be
* allowed, otherwise false will be returned.
*
* @param allowedTimeString
* The string containing the times that should be parsed to determine if
* the login or connection should be allowed at the current time, or
* null or an empty string if there are no specific allowed times defined.
*
* @param deniedTimeString
* The string containing the times that should be parsed to determine if
* the login or connection should be denied at the current time, or null
* or an empty string if there are no specific times during which a
* action should be denied.
*
* @return
* True if the login or connection should be allowed, otherwise false.
*/
private static boolean allowedByTimeRestrictions(String allowedTimeString,
String deniedTimeString) {
// Check for denied entries, first, returning false if the login or
// connection should not be allowed.
if (deniedTimeString != null && !deniedTimeString.isEmpty()) {
List<DailyRestriction> deniedTimes =
TimeRestrictionParser.parseString(deniedTimeString);
for (DailyRestriction restriction : deniedTimes) {
if (restriction.appliesNow())
return false;
}
}
// If no allowed entries are present, return true, allowing the login
// or connection to continue.
if (allowedTimeString == null || allowedTimeString.isEmpty())
return true;
List<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 true.
if (restriction.appliesNow())
return true;
}
// We have allowed entries, but login hasn't matched, so deny it.
return false;
}
/**
* Given the strings of allowed and denied hosts, verify that the login or
* connection should be allowed from the given remote address. If the action
* should not be allowed, return false - otherwise, return true.
*
* @param allowedHostsString
* The string containing a semicolon-separated list of hosts from
* which the login or connection should be allowed, or null or an empty
* string if no specific set of allowed hosts is defined.
*
* @param deniedHostsString
* The string containing a semicolon-separated list of hosts from
* which the login or connection should be denied, or null or an empty
* string if no specific set of denied hosts is defined.
*
* @param remoteAddress
* The IP address from which the user is logging in or has logged in
* and is attempting to connect from, if it is known. If it is unknown
* and restrictions are defined, the login or connection will be denied.
*
* @return
* True if the login or connection should be allowed by the host-based
* restrictions, otherwise false.
*/
private static boolean allowedByHostRestrictions(String allowedHostsString,
String deniedHostsString, String remoteAddress) {
HostName remoteHostName = new HostName(remoteAddress);
// If attributes do not exist or are empty then the action is allowed.
if ((allowedHostsString == null || allowedHostsString.isEmpty())
&& (deniedHostsString == null || deniedHostsString.isEmpty()))
return true;
// If the remote address cannot be determined, and restrictions are
// in effect, log an error and deny the action.
if (remoteAddress == null || remoteAddress.isEmpty()) {
LOGGER.warn("Host-based restrictions are present, but the remote "
+ "address is invalid or could not be resolved. "
+ "The action will not be allowed.");
return false;
}
// Split denied hosts attribute and process each entry, checking them
// against the current remote address, and returning false if a match is
// found.
List<HostName> deniedHosts = HostRestrictionParser.parseHostList(deniedHostsString);
for (HostName hostName : deniedHosts) {
try {
if (hostName.isAddress() && hostName.toAddress().contains(remoteHostName.asAddress()))
return false;
else
for (IPAddress currAddr : hostName.toAllAddresses())
if (currAddr.matches(remoteHostName.asAddressString()))
return false;
}
catch (UnknownHostException | HostNameException e) {
LOGGER.warn("Unknown or invalid host in denied hosts list: \"{}\"", hostName);
LOGGER.debug("Exception while trying to resolve host: \"{}\"", hostName, e);
return false;
}
}
// If denied hosts have been checked and allowed hosts are empty, we're
// good, and can allow the action.
if (allowedHostsString == null || allowedHostsString.isEmpty())
return true;
// Run through allowed hosts, if there are any, and return, allowing the
// action if there are any matches.
List<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 true;
// Entry is a hostname, so resolve to IPs and check each one
for (IPAddress currAddr : hostName.toAllAddresses())
if (currAddr.matches(remoteHostName.asAddressString()))
return true;
}
// If an entry cannot be resolved we will log a warning.
catch (UnknownHostException | HostNameException e) {
LOGGER.warn("Unknown host encountered in allowed host string: {}", hostName);
LOGGER.debug("Exception received trying to resolve host: {}", hostName, e);
}
}
// If we've made it here, the allowed hosts do not contain the remote
// address, and the action should not be allowed;
return false;
}
/**
* Verifies the login restrictions supported by this extension for the user
* who is attempting to log in, throwing an exception if any of the
* restrictions result in the user not being allowed to log in.
*
* @param context
* The context of the user who is attempting to log in.
*
* @param authenticatedUser
* The AuthenticatedUser object associated with the user who is
* attempting to log in.
*
* @throws GuacamoleException
* If any of the restrictions should prevent the user from logging in.
*/
public static void verifyLoginRestrictions(UserContext context,
AuthenticatedUser authenticatedUser) throws GuacamoleException {
// Get user's attributes
Map<String, String> userAttributes = context.self().getAttributes();
String remoteAddress = authenticatedUser.getCredentials().getRemoteAddress();
if (context.self().getEffectivePermissions().getSystemPermissions().hasPermission(SystemPermission.Type.ADMINISTER)) {
LOGGER.warn("User \"{}\" has System Administration permissions; additional restrictions will be bypassed.",
authenticatedUser.getIdentifier());
return;
}
// Verify time-based restrictions specific to the user
String allowedTimeString = userAttributes.get(RestrictUser.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String deniedTimeString = userAttributes.get(RestrictUser.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
if (!allowedByTimeRestrictions(allowedTimeString, deniedTimeString))
throw new TranslatableInvalidTimeLoginException("User \""
+ authenticatedUser.getIdentifier()
+ "\" is not allowed to log in at this time.",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW");
// Verify host-based restrictions specific to the user
String allowedHostString = userAttributes.get(RestrictUser.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String deniedHostString = userAttributes.get(RestrictUser.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
if (!allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress))
throw new TranslatableInvalidHostLoginException("User \""
+ authenticatedUser.getIdentifier()
+"\" is not allowed to log in from \""
+ remoteAddress + "\"",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST");
// Gather user's effective groups.
Set<String> userGroups = authenticatedUser.getEffectiveUserGroups();
Directory<UserGroup> directoryGroups = context.getPrivileged().getUserGroupDirectory();
// Loop user's effective groups and verify restrictions
for (String userGroup : userGroups) {
UserGroup thisGroup = directoryGroups.get(userGroup);
if (thisGroup == null) {
continue;
}
// Get group's attributes
Map<String, String> grpAttributes = thisGroup.getAttributes();
// Pull time-based restrictions for this group and verify
String grpAllowedTimeString = grpAttributes.get(RestrictUserGroup.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String grpDeniedTimeString = grpAttributes.get(RestrictUserGroup.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
if (!allowedByTimeRestrictions(grpAllowedTimeString, grpDeniedTimeString))
throw new TranslatableInvalidTimeLoginException("User \""
+ authenticatedUser.getIdentifier()
+"\" is not allowed to log in at this time due to restrictions on group \""
+ userGroup + "\".",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_NOW");
// Pull host-based restrictions for this group and verify
String grpAllowedHostString = grpAttributes.get(RestrictUserGroup.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String grpDeniedHostString = grpAttributes.get(RestrictUserGroup.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
if (!allowedByHostRestrictions(grpAllowedHostString, grpDeniedHostString, remoteAddress))
throw new TranslatableInvalidHostLoginException("User \""
+ authenticatedUser.getIdentifier()
+ "\" is not allowed to log in from this host due to restrictions on group \""
+ userGroup + "\".",
"RESTRICT.ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST");
}
}
/**
* Verifies the connection restrictions supported by this extension for the
* connection the user is attempting to access, throwing an exception if
* any of the restrictions result in the connection being unavailable.
*
* @param connectionAttributes
* The attributes of the connection that may contain any additional
* restrictions on use of the connection.
*
* @param remoteAddress
* The remote IP address of the user trying to access the connection.
*
* @throws GuacamoleException
* If any of the restrictions should prevent the connection from being
* used by the user at the current time.
*/
public static void verifyConnectionRestrictions(
Map<String, String> connectionAttributes, String remoteAddress)
throws GuacamoleException {
// Verify time-based restrictions specific to this connection.
String allowedTimeString = connectionAttributes.get(RestrictConnection.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME);
String deniedTimeString = connectionAttributes.get(RestrictConnection.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME);
if (!allowedByTimeRestrictions(allowedTimeString, deniedTimeString))
throw new TranslatableGuacamoleSecurityException(
"Use of this connection is not allowed at this time.",
"RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW"
);
// Verify host-based restrictions specific to this connection.
String allowedHostString = connectionAttributes.get(RestrictConnection.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME);
String deniedHostString = connectionAttributes.get(RestrictConnection.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME);
if (!allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress))
throw new TranslatableGuacamoleSecurityException(
"Use of this connection is not allowed from this remote host: \"" + remoteAddress + "\".",
"RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW"
);
}
}

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

View File

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

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

View File

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

View File

@@ -0,0 +1,165 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.restrict.user;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.restrict.connection.RestrictConnection;
import org.apache.guacamole.auth.restrict.connectiongroup.RestrictConnectionGroup;
import org.apache.guacamole.auth.restrict.usergroup.RestrictUserGroup;
import org.apache.guacamole.form.Form;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.DecoratingDirectory;
import org.apache.guacamole.net.auth.DelegatingUserContext;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup;
/**
* A UserContext implementation for additional login and connection restrictions
* which wraps the UserContext of some other extension.
*/
public class RestrictUserContext extends DelegatingUserContext {
/**
* The remote address from which this user logged in.
*/
private final String remoteAddress;
/**
* Creates a new RestrictUserContext which wraps the given UserContext,
* providing additional control for user logins and connections.
*
* @param userContext
* The UserContext to wrap.
*
* @param remoteAddress
* The address the user is logging in from, if known.
*/
public RestrictUserContext(UserContext userContext, String remoteAddress) {
super(userContext);
this.remoteAddress = remoteAddress;
}
@Override
public Directory<Connection> getConnectionDirectory() throws GuacamoleException {
return new DecoratingDirectory<Connection>(super.getConnectionDirectory()) {
@Override
protected Connection decorate(Connection object) {
return new RestrictConnection(object, remoteAddress);
}
@Override
protected Connection undecorate(Connection object) {
assert(object instanceof RestrictConnection);
return ((RestrictConnection) object).getUndecorated();
}
};
}
@Override
public Collection<Form> getConnectionAttributes() {
Collection<Form> connectionAttrs = new HashSet<>(super.getConnectionAttributes());
connectionAttrs.add(RestrictConnection.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) {
return new RestrictConnectionGroup(object, remoteAddress);
}
@Override
protected ConnectionGroup undecorate(ConnectionGroup object) {
assert(object instanceof RestrictConnectionGroup);
return ((RestrictConnectionGroup) object).getUndecorated();
}
};
}
@Override
public Collection<Form> getConnectionGroupAttributes() {
Collection<Form> connectionGroupAttrs = new HashSet<>(super.getConnectionGroupAttributes());
connectionGroupAttrs.add(RestrictConnectionGroup.RESTRICT_CONNECTIONGROUP_FORM);
return Collections.unmodifiableCollection(connectionGroupAttrs);
}
@Override
public Directory<User> getUserDirectory() throws GuacamoleException {
return new DecoratingDirectory<User>(super.getUserDirectory()) {
@Override
protected User decorate(User object) {
return new RestrictUser(object);
}
@Override
protected User undecorate(User object) {
assert(object instanceof RestrictUser);
return ((RestrictUser) object).getUndecorated();
}
};
}
@Override
public Collection<Form> getUserAttributes() {
Collection<Form> userAttrs = new HashSet<>(super.getUserAttributes());
userAttrs.add(RestrictUser.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 RestrictUserGroup(object);
}
@Override
protected UserGroup undecorate(UserGroup object) {
assert(object instanceof RestrictUserGroup);
return ((RestrictUserGroup) object).getUndecorated();
}
};
}
@Override
public Collection<Form> getUserGroupAttributes() {
Collection<Form> userGroupAttrs = new HashSet<>(super.getUserGroupAttributes());
userGroupAttrs.add(RestrictUserGroup.RESTRICT_LOGIN_FORM);
return Collections.unmodifiableCollection(userGroupAttrs);
}
}

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 RestrictUserGroup extends DelegatingUserGroup {
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that members of a group are allowed to log in. The presence of this
* attribute will restrict any users who are members of the group to logins
* only during the times that are contained within the attribute,
* subject to further restriction by the guac-restrict-time-denied attribute.
*/
public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed";
/**
* The name of the attribute that contains a list of weekdays and times (UTC)
* that members of a group are not allowed to log in. Denied times will
* always take precedence over allowed times. The presence of this attribute
* without guac-restrict-time-allowed will deny logins only during the times
* listed in this attribute, allowing logins at all other times. The
* presence of this attribute along with the guac-restrict-time-allowed
* attribute will deny logins at any times that overlap with the allowed
* times.
*/
public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied";
/**
* The name of the attribute that contains a list of IP addresses from which
* members of a group are allowed to log in. The presence of this attribute
* will restrict users to only the list of IP addresses contained in the
* attribute, subject to further restriction by the
* guac-restrict-hosts-denied attribute.
*/
public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed";
/**
* The name of the attribute that contains a list of IP addresses from which
* members of a group are not allowed to log in. The presence of this
* attribute, absent the guac-restrict-hosts-allowed attribute, will allow
* logins from all hosts except the ones listed in this attribute. The
* presence of this attribute coupled with the guac-restrict-hosts-allowed
* attribute will block access from any IPs in this list, overriding any
* that may be allowed.
*/
public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied";
/**
* The list of all user attributes provided by this UserGroup implementation.
*/
public static final List<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 RestrictUserGroup(UserGroup userGroup) {
super(userGroup);
}
/**
* Returns the UserGroup object wrapped by this RestrictUserGroup.
*
* @return
* The wrapped UserGroup object.
*/
public UserGroup getUndecorated() {
return getDelegateUserGroupGroup();
}
@Override
public Map<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,132 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.calendar;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
/**
* A class that stores a daily time restriction that can be used to determine
* whether or not a user can log in on a certain day of the week and during
* a certain time window.
*/
public class DailyRestriction {
/**
* The days of the week that this restriction applies to.
*/
private final List<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().getDayOfWeek();
LocalTime currentTime = LocalTime.now(ZoneId.of("UTC"));
// Check that we are in the specified time restriction
return (weekDays.contains(currentDay) && currentTime.isAfter(startTime) && currentTime.isBefore(endTime));
}
}

View File

@@ -0,0 +1,180 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.calendar;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A class for parsing time-based restrictions stored in a String into other
* formats that can be used by Guacamole.
*/
public class TimeRestrictionParser {
/**
* The compiled regular expression that matches one or more instances of
* a restriction string, which specifies at least one day and time range
* that the restriction applies to.
*
* <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>wd:0900-1700 - Monday through Friday, 09:00 to 17:00
* <li>we:0900-1700 - Saturday and Sunday, 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("(?:^|;)+([1-7*]|(?:[w][ed]))(?::((?:[01][0-9]|2[0-3])[0-5][0-9])\\-((?:[01][0-9]|2[0-3])[0-5][0-9]))+");
/**
* The RegEx group that contains the start day-of-week of the restriction.
*/
private static final int RESTRICTION_DAY_GROUP = 1;
/**
* The RegEx group that contains the start time of the restriction.
*/
private static final int RESTRICTION_TIME_START_GROUP = 2;
/**
* The RegEx group that contains the end time of the restriction.
*/
private static final int RESTRICTION_TIME_END_GROUP = 3;
/**
* A list of DayOfWeek items that make up weekdays.
*/
private static final List<DayOfWeek> RESTRICTION_WEEKDAYS = Arrays.asList(
DayOfWeek.MONDAY,
DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY,
DayOfWeek.THURSDAY,
DayOfWeek.FRIDAY
);
/**
* A list of DayOfWeek items that make up weekends.
*/
private static final List<DayOfWeek> RESTRICTION_WEEKEND = Arrays.asList(
DayOfWeek.SATURDAY,
DayOfWeek.SUNDAY
);
/**
* A list of DayOfWeek items that make up all days of the week.
*/
private static final List<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;
// Weekdays only.
case "wd":
restrictions.add(new DailyRestriction(RESTRICTION_WEEKDAYS, startTime, endTime));
break;
// Weekend days only.
case "we":
restrictions.add(new DailyRestriction(RESTRICTION_WEEKEND, startTime, endTime));
break;
// A specific day of the week.
default:
restrictions.add(new DailyRestriction(DayOfWeek.of(Integer.parseInt(dayString)), startTime, endTime));
}
}
// Return the list of restrictions
return restrictions;
}
}

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