From 6bf2ff3e2fedfa60eec9d8ee845ef92071c4045a Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 30 Oct 2014 23:54:58 -0700 Subject: [PATCH] GUAC-919: Copy REST and Guice changes over from GUAC-546 branch. --- .gitignore | 1 + .../SNAPSHOT/javax.ws.rs-api-2.0-SNAPSHOT.jar | Bin 0 -> 109582 bytes guacamole/pom.xml | 49 ++ .../net/basic/AuthenticatingFilter.java | 452 ------------------ .../basic/BasicGuacamoleTunnelServlet.java | 13 +- ....java => BasicServletContextListener.java} | 44 +- .../guacamole/net/basic/GuacamoleSession.java | 164 +++++++ .../guacamole/net/basic/RestrictedFilter.java | 93 ---- .../net/basic/RestrictedHttpServlet.java | 154 ------ .../guacamole/net/basic/SessionKeepAlive.java | 32 +- ...etSupportLoader.java => TunnelModule.java} | 113 ++--- ...Utility.java => TunnelRequestService.java} | 35 +- .../ConnectionGroupUtility.java | 71 --- .../basic/crud/connectiongroups/Create.java | 75 --- .../basic/crud/connectiongroups/Delete.java | 59 --- .../DummyConnectionGroup.java | 60 --- .../net/basic/crud/connectiongroups/List.java | 218 --------- .../net/basic/crud/connectiongroups/Move.java | 66 --- .../basic/crud/connectiongroups/Update.java | 70 --- .../crud/connections/ConnectionUtility.java | 72 --- .../net/basic/crud/connections/Create.java | 97 ---- .../crud/connections/DummyConnection.java | 54 --- .../net/basic/crud/connections/List.java | 342 ------------- .../net/basic/crud/connections/Move.java | 66 --- .../net/basic/crud/connections/Update.java | 92 ---- .../net/basic/crud/permissions/List.java | 224 --------- .../net/basic/crud/users/Create.java | 65 --- .../net/basic/crud/users/DummyUser.java | 67 --- .../guacamole/net/basic/crud/users/List.java | 106 ---- .../net/basic/crud/users/Update.java | 311 ------------ ...LogbackInitializer.java => LogModule.java} | 16 +- .../net/basic/rest/APIConstants.java | 37 ++ .../guacamole/net/basic/rest/APIError.java | 52 ++ .../guacamole/net/basic/rest/APIPatch.java | 104 ++++ .../AuthProviderRESTExceptionWrapper.java | 65 +++ .../basic/rest/AuthProviderRESTExposure.java | 38 ++ .../net/basic/rest/HTTPException.java | 58 +++ .../guacamole/net/basic/rest/PATCH.java | 40 ++ .../guacamole/net/basic/rest/RESTModule.java | 89 ++++ .../net/basic/rest/RESTServletModule.java | 61 +++ .../auth/APIAuthToken.java} | 62 ++- .../basic/rest/auth/AuthTokenGenerator.java | 39 ++ .../rest/auth/AuthenticationService.java | 81 ++++ .../basic/rest/auth/BasicTokenSessionMap.java | 143 ++++++ .../net/basic/rest/auth/LoginRESTService.java | 124 +++++ .../auth/SecureRandomAuthTokenGenerator.java | 48 ++ .../net/basic/rest/auth/TokenSessionMap.java | 54 +++ .../auth}/package-info.java | 6 +- .../clipboard/ClipboardRESTService.java} | 53 +- .../basic/rest/connection/APIConnection.java | 194 ++++++++ .../rest/connection/APIConnectionWrapper.java | 116 +++++ .../connection/ConnectionRESTService.java | 302 ++++++++++++ .../connection/ConnectionService.java} | 52 +- .../connection}/package-info.java | 6 +- .../connectiongroup/APIConnectionGroup.java | 147 ++++++ .../APIConnectionGroupWrapper.java | 109 +++++ .../ConnectionGroupRESTService.java | 321 +++++++++++++ .../ConnectionGroupService.java | 59 +++ .../rest/connectiongroup/package-info.java | 28 ++ .../{crud/users => rest}/package-info.java | 6 +- .../basic/rest/permission/APIPermission.java | 228 +++++++++ .../permission/PermissionRESTService.java | 217 +++++++++ .../rest/permission/PermissionService.java | 74 +++ .../basic/rest/permission/package-info.java | 27 ++ .../rest/protocol/ProtocolRESTService.java | 73 +++ .../protocol/ProtocolRetrievalService.java} | 173 +------ .../protocol}/package-info.java | 6 +- .../net/basic/rest/user/APIUser.java | 94 ++++ .../net/basic/rest/user/APIUserWrapper.java | 108 +++++ .../net/basic/rest/user/UserRESTService.java | 222 +++++++++ .../user/UserService.java} | 53 +- .../user}/package-info.java | 6 +- ...BasicGuacamoleWebSocketTunnelEndpoint.java | 31 +- .../BasicGuacamoleWebSocketTunnelServlet.java | 13 +- ...BasicGuacamoleWebSocketTunnelListener.java | 13 +- .../BasicGuacamoleWebSocketTunnelServlet.java | 13 +- guacamole/src/main/webapp/WEB-INF/web.xml | 281 +---------- 77 files changed, 3930 insertions(+), 3477 deletions(-) create mode 100644 guacamole/lib/unknown/binary/javax.ws.rs-api-2.0/SNAPSHOT/javax.ws.rs-api-2.0-SNAPSHOT.jar delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingFilter.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{BasicLogout.java => BasicServletContextListener.java} (59%) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/RestrictedFilter.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/RestrictedHttpServlet.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{websocket/WebSocketSupportLoader.java => TunnelModule.java} (58%) rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{BasicTunnelRequestUtility.java => TunnelRequestService.java} (90%) delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/ConnectionGroupUtility.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Create.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Delete.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/DummyConnectionGroup.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/List.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Move.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Update.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/ConnectionUtility.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Create.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/DummyConnection.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/List.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Move.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Update.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/permissions/List.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Create.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/DummyUser.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/List.java delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Update.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/{LogbackInitializer.java => LogModule.java} (85%) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIConstants.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIPatch.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExposure.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/HTTPException.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/PATCH.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{BasicLogin.java => rest/auth/APIAuthToken.java} (52%) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthTokenGenerator.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/LoginRESTService.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/SecureRandomAuthTokenGenerator.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{crud/connections => rest/auth}/package-info.java (86%) rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{CaptureClipboard.java => rest/clipboard/ClipboardRESTService.java} (64%) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{crud/connections/Delete.java => rest/connection/ConnectionService.java} (51%) rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{crud/connectiongroups => rest/connection}/package-info.java (85%) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroupWrapper.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/package-info.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{crud/users => rest}/package-info.java (87%) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermission.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/package-info.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRESTService.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{crud/protocols/List.java => rest/protocol/ProtocolRetrievalService.java} (51%) rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{crud/protocols => rest/protocol}/package-info.java (86%) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWrapper.java create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{crud/users/Delete.java => rest/user/UserService.java} (54%) rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/{crud/permissions => rest/user}/package-info.java (86%) diff --git a/.gitignore b/.gitignore index bcc52fede..d26be9c0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ target/ +nb-configuration.xml diff --git a/guacamole/lib/unknown/binary/javax.ws.rs-api-2.0/SNAPSHOT/javax.ws.rs-api-2.0-SNAPSHOT.jar b/guacamole/lib/unknown/binary/javax.ws.rs-api-2.0/SNAPSHOT/javax.ws.rs-api-2.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..b9de41659bb09e930798cec27d5562cd17d82a49 GIT binary patch literal 109582 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%0AyR8JCuN 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�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 literal 0 HcmV?d00001 diff --git a/guacamole/pom.xml b/guacamole/pom.xml index 33fe0fd6b..723f43941 100644 --- a/guacamole/pom.xml +++ b/guacamole/pom.xml @@ -185,6 +185,55 @@ provided + + + com.google.inject + guice + 3.0 + + + + + com.google.inject.extensions + guice-servlet + 3.0 + + + + + com.sun.jersey + jersey-server + 1.17.1 + + + + + com.sun.jersey.contribs + jersey-guice + 1.17.1 + + + + + javax.annotation + jsr250-api + 1.0 + + + + + commons-codec + commons-codec + 1.4 + + + + + com.sun.jersey + jersey-json + 1.17.1 + + diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingFilter.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingFilter.java deleted file mode 100644 index 600dbfd41..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingFilter.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.Collection; -import java.util.regex.Pattern; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.xml.bind.DatatypeConverter; -import org.glyptodon.guacamole.GuacamoleClientException; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.AuthenticationProvider; -import org.glyptodon.guacamole.net.auth.Credentials; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.event.SessionListenerCollection; -import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; -import org.glyptodon.guacamole.net.event.AuthenticationFailureEvent; -import org.glyptodon.guacamole.net.event.AuthenticationSuccessEvent; -import org.glyptodon.guacamole.net.event.listener.AuthenticationFailureListener; -import org.glyptodon.guacamole.net.event.listener.AuthenticationSuccessListener; -import org.glyptodon.guacamole.properties.GuacamoleProperties; -import org.glyptodon.guacamole.protocol.GuacamoleStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Filter which provides watches requests for credentials, authenticating the - * user against the configured AuthenticationProvider if credentials are - * present. Note that if authentication fails, the request is still allowed. To - * restrict access based on the result of authentication, use - * RestrictedHttpServlet or RestrictedFilter. - * - * The user context is retrieved using the authentication provider defined in - * guacamole.properties. The authentication provider has access to the request - * and session, in addition to any submitted username and password, in order - * to authenticate the user. - * - * The user context will be stored in the current HttpSession. - * - * Success and failure are logged. - * - * @author Michael Jumper - */ -public class AuthenticatingFilter implements Filter { - - /** - * Logger for this class. - */ - private final Logger logger = LoggerFactory.getLogger(AuthenticatingFilter.class); - - /** - * The session attribute holding the current UserContext. - */ - public static final String CONTEXT_ATTRIBUTE = "GUAC_CONTEXT"; - - /** - * The session attribute holding the credentials authorizing this session. - */ - public static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS"; - - /** - * The session attribute holding the session-scoped clipboard storage. - */ - public static final String CLIPBOARD_ATTRIBUTE = "GUAC_CLIP"; - - /** - * The AuthenticationProvider to use to authenticate all requests. - */ - private AuthenticationProvider authProvider; - - /** - * Whether HTTP authentication should be used (the "Authorization" header). - */ - private boolean useHttpAuthentication; - - @Override - public void init(FilterConfig config) throws ServletException { - - // Parse Guacamole configuration - try { - - // Get auth provider instance - authProvider = GuacamoleProperties.getRequiredProperty(BasicGuacamoleProperties.AUTH_PROVIDER); - - // Enable HTTP auth, if requested - useHttpAuthentication = GuacamoleProperties.getProperty(BasicGuacamoleProperties.ENABLE_HTTP_AUTH, false); - - } - catch (GuacamoleException e) { - logger.error("Unable to read guacamole.properties: {}", e.getMessage()); - logger.debug("Error reading guacamole.properties.", e); - throw new ServletException(e); - } - - } - - /** - * Notifies all listeners in the given collection that authentication has - * failed. - * - * @param listeners A collection of all listeners that should be notified. - * @param credentials The credentials associated with the authentication - * request that failed. - */ - private void notifyFailed(Collection listeners, Credentials credentials) { - - // Build event for auth failure - AuthenticationFailureEvent event = new AuthenticationFailureEvent(credentials); - - // Notify all listeners - for (Object listener : listeners) { - try { - if (listener instanceof AuthenticationFailureListener) - ((AuthenticationFailureListener) listener).authenticationFailed(event); - } - catch (GuacamoleException e) { - logger.debug("Error notifying AuthenticationFailureListener: {}", e); - } - } - - } - - /** - * Notifies all listeners in the given collection that authentication was - * successful. - * - * @param listeners A collection of all listeners that should be notified. - * @param context The UserContext created as a result of authentication - * success. - * @param credentials The credentials associated with the authentication - * request that succeeded. - * @return true if all listeners are allowing the authentication success, - * or if there are no listeners, and false if any listener is - * canceling the authentication success. Note that once one - * listener cancels, no other listeners will run. - * @throws GuacamoleException If any listener throws an error while being - * notified. Note that if any listener throws an - * error, the success is canceled, and no other - * listeners will run. - */ - private boolean notifySuccess(Collection listeners, UserContext context, - Credentials credentials) throws GuacamoleException { - - // Build event for auth success - AuthenticationSuccessEvent event = - new AuthenticationSuccessEvent(context, credentials); - - // Notify all listeners - for (Object listener : listeners) { - if (listener instanceof AuthenticationSuccessListener) { - - // Cancel immediately if hook returns false - if (!((AuthenticationSuccessListener) listener).authenticationSucceeded(event)) - return false; - - } - } - - return true; - - } - - /** - * Sends an error on the given HTTP response using the information within - * the given GuacamoleStatus. - * - * @param response The HTTP response to use to send the error. - * @param guac_status The status to send - * @param message A human-readable message that can be presented to the - * user. - * @throws ServletException If an error prevents sending of the error - * code. - */ - public static void sendError(HttpServletResponse response, - GuacamoleStatus guac_status, String message) - throws ServletException { - - try { - - // If response not committed, send error code and message - if (!response.isCommitted()) { - response.addHeader("Guacamole-Status-Code", Integer.toString(guac_status.getGuacamoleStatusCode())); - response.addHeader("Guacamole-Error-Message", message); - response.sendError(guac_status.getHttpStatusCode()); - } - - } - catch (IOException ioe) { - - // If unable to send error at all due to I/O problems, - // rethrow as servlet exception - throw new ServletException(ioe); - - } - - } - - /** - * Returns the credentials associated with the given session. - * - * @param session The session to retrieve credentials from. - * @return The credentials associated with the given session. - */ - public static Credentials getCredentials(HttpSession session) { - return (Credentials) session.getAttribute(CREDENTIALS_ATTRIBUTE); - } - - /** - * Returns the UserContext associated with the given session. - * - * @param session The session to retrieve UserContext from. - * @return The UserContext associated with the given session. - */ - public static UserContext getUserContext(HttpSession session) { - return (UserContext) session.getAttribute(CONTEXT_ATTRIBUTE); - } - - /** - * Returns the ClipboardState associated with the given session. If none - * exists yet, one is created. - * - * @param session The session to retrieve the ClipboardState from. - * @return The ClipboardState associated with the given session. - */ - public static ClipboardState getClipboardState(HttpSession session) { - - ClipboardState clipboard = (ClipboardState) session.getAttribute(CLIPBOARD_ATTRIBUTE); - if (clipboard == null) { - clipboard = new ClipboardState(); - session.setAttribute(CLIPBOARD_ATTRIBUTE, clipboard); - } - - return clipboard; - - } - - /** - * Returns whether the request given has updated credentials. If this - * function returns false, the UserContext will not be updated. - * - * @param request The request to check for credentials. - * @return true if the request contains credentials, false otherwise. - */ - protected boolean hasNewCredentials(HttpServletRequest request) { - return true; - } - - /** - * Regular expression which matches any IPv4 address. - */ - private static final String IPV4_ADDRESS_REGEX = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})"; - - /** - * Regular expression which matches any IPv6 address. - */ - private static final String IPV6_ADDRESS_REGEX = "([0-9a-fA-F]*(:[0-9a-fA-F]*){0,7})"; - - /** - * Regular expression which matches any IP address, regardless of version. - */ - private static final String IP_ADDRESS_REGEX = "(" + IPV4_ADDRESS_REGEX + "|" + IPV6_ADDRESS_REGEX + ")"; - - /** - * Pattern which matches valid values of the de-facto standard - * "X-Forwarded-For" header. - */ - private static final Pattern X_FORWARDED_FOR = Pattern.compile("^" + IP_ADDRESS_REGEX + "(, " + IP_ADDRESS_REGEX + ")*$"); - - /** - * Returns a formatted string containing an IP address, or list of IP - * addresses, which represent the HTTP client and any involved proxies. As - * the headers used to determine proxies can easily be forged, this data is - * superficially validated to ensure that it at least looks like a list of - * IPs. - * - * @param request The HTTP request to format. - * @return A formatted string containing one or more IP addresses. - */ - private String getLoggableAddress(HttpServletRequest request) { - - // Log X-Forwarded-For, if present and valid - String header = request.getHeader("X-Forwarded-For"); - if (header != null && X_FORWARDED_FOR.matcher(header).matches()) - return "[" + header + ", " + request.getRemoteAddr() + "]"; - - // If header absent or invalid, just use source IP - return request.getRemoteAddr(); - - } - - @Override - public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) - throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) resp; - - // Set character encoding to UTF-8 if it's not already set - if(request.getCharacterEncoding() == null) { - try { - request.setCharacterEncoding("UTF-8"); - } catch (UnsupportedEncodingException exception) { - throw new ServletException(exception); - } - } - - try { - - // Obtain context from session - HttpSession httpSession = request.getSession(true); - UserContext context = getUserContext(httpSession); - - // If new credentials present, update/create context - if (hasNewCredentials(request)) { - - // Retrieve username and password from parms - String username = request.getParameter("username"); - String password = request.getParameter("password"); - - // If no username/password given, try Authorization header - if (useHttpAuthentication && username == null && password == null) { - - String authorization = request.getHeader("Authorization"); - if (authorization != null && authorization.startsWith("Basic ")) { - - // Decode base64 authorization - String basicBase64 = authorization.substring(6); - String basicCredentials = new String(DatatypeConverter.parseBase64Binary(basicBase64), "UTF-8"); - - // Pull username/password from auth data - int colon = basicCredentials.indexOf(':'); - if (colon != -1) { - username = basicCredentials.substring(0, colon); - password = basicCredentials.substring(colon+1); - } - - else - logger.info("Invalid HTTP Basic \"Authorization\" header received."); - - } - - } // end Authorization header fallback - - // Build credentials object - Credentials credentials = new Credentials(); - credentials.setSession(httpSession); - credentials.setRequest(request); - credentials.setUsername(username); - credentials.setPassword(password); - - SessionListenerCollection listeners = new SessionListenerCollection(httpSession); - - // If no cached context, attempt to get new context - if (context == null) { - - context = authProvider.getUserContext(credentials); - - // Log successful authentication - if (context != null && logger.isInfoEnabled()) - logger.info("User \"{}\" successfully authenticated from {}.", - context.self().getUsername(), getLoggableAddress(request)); - - } - - // Otherwise, update existing context - else - context = authProvider.updateUserContext(context, credentials); - - // If auth failed, notify listeners - if (context == null) { - - if (logger.isWarnEnabled()) { - - // Only bother logging failures involving usernames - if (credentials.getUsername() != null) - logger.info("Authentication attempt from {} for user \"{}\" failed.", - getLoggableAddress(request), credentials.getUsername()); - else - logger.debug("Authentication attempt from {} without username failed.", - getLoggableAddress(request)); - } - - notifyFailed(listeners, credentials); - } - - // If auth succeeded, notify and check with listeners - else if (!notifySuccess(listeners, context, credentials)) - logger.info("Successful authentication canceled by hook."); - - // If auth still OK, associate context with session - else { - httpSession.setAttribute(CONTEXT_ATTRIBUTE, context); - httpSession.setAttribute(CREDENTIALS_ATTRIBUTE, credentials); - } - - } // end if credentials present - - // Allow servlet to run now that authentication has been validated - chain.doFilter(request, response); - - } - - // Catch any thrown guacamole exception and attempt to pass within the - // HTTP response, logging each error appropriately. - catch (GuacamoleClientException e) { - logger.info("HTTP request rejected: {}", e.getMessage()); - logger.debug("HTTP request rejected by AuthenticatingFilter.", e); - sendError(response, e.getStatus(), e.getMessage()); - } - catch (GuacamoleException e) { - logger.error("Authentication failed internally: {}", e.getMessage()); - logger.debug("Internal server error during authentication.", e); - sendError(response, e.getStatus(), "Internal server error."); - } - - } - - @Override - public void destroy() { - // No destruction needed - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.java index f6da591d2..be6e328f6 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.java @@ -22,6 +22,8 @@ package org.glyptodon.guacamole.net.basic; +import com.google.inject.Inject; +import com.google.inject.Singleton; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; @@ -35,8 +37,15 @@ import org.slf4j.LoggerFactory; * * @author Michael Jumper */ +@Singleton public class BasicGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet { + /** + * Service for handling tunnel requests. + */ + @Inject + private TunnelRequestService tunnelRequestService; + /** * Logger for this class. */ @@ -46,8 +55,8 @@ public class BasicGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet { protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { // Attempt to create HTTP tunnel - GuacamoleTunnel tunnel = BasicTunnelRequestUtility.createTunnel(new HTTPTunnelRequest(request)); - + GuacamoleTunnel tunnel = tunnelRequestService.createTunnel(new HTTPTunnelRequest(request)); + // If successful, warn of lack of WebSocket logger.info("Using HTTP tunnel (not WebSocket). Performance may be sub-optimal."); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicLogout.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicServletContextListener.java similarity index 59% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicLogout.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicServletContextListener.java index eb40f0c84..d7ba6b499 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicLogout.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicServletContextListener.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,33 +22,31 @@ package org.glyptodon.guacamole.net.basic; -import java.io.IOException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; +import org.glyptodon.guacamole.net.basic.log.LogModule; +import org.glyptodon.guacamole.net.basic.rest.RESTModule; +import org.glyptodon.guacamole.net.basic.rest.RESTServletModule; /** - * Logs out the current user by invalidating the associated HttpSession and - * redirecting the user to the login page. - * - * @author Michael Jumper + * A ServletContextListener to listen for initialization of the servlet context + * in order to set up dependency injection. + * + * @author James Muehlner */ -public class BasicLogout extends HttpServlet { +public class BasicServletContextListener extends GuiceServletContextListener { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) - throws IOException { - - // Invalidate session, if any - HttpSession httpSession = request.getSession(false); - if (httpSession != null) - httpSession.invalidate(); - - // Redirect to index - response.sendRedirect("index.xhtml"); + protected Injector getInjector() { + return Guice.createInjector( + new LogModule(), + new RESTServletModule(), + new RESTModule(), + new TunnelModule() + ); + } - + } - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java new file mode 100644 index 000000000..912ad8146 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.Credentials; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; +import org.glyptodon.guacamole.properties.GuacamoleProperties; + +/** + * Contains Guacamole-specific user information which is tied to the current + * session, such as the UserContext and current clipboard state. + * + * @author Michael Jumper + */ +public class GuacamoleSession { + + /** + * The credentials provided when the user logged in. + */ + private final Credentials credentials; + + /** + * The user context associated with this session. + */ + private final UserContext userContext; + + /** + * Collection of all event listeners configured in guacamole.properties. + */ + private final Collection listeners = new ArrayList(); + + /** + * The current clipboard state. + */ + private final ClipboardState clipboardState = new ClipboardState(); + + /** + * Creates a new Guacamole session associated with the given user context. + * + * @param credentials The credentials provided by the user during login. + * @param userContext The user context to associate this session with. + * @throws GuacamoleException If an error prevents the session from being + * created. + */ + public GuacamoleSession(Credentials credentials, UserContext userContext) throws GuacamoleException { + + this.credentials = credentials; + this.userContext = userContext; + + // Load listeners from guacamole.properties + try { + + // Get all listener classes from properties + Collection listenerClasses = + GuacamoleProperties.getProperty(BasicGuacamoleProperties.EVENT_LISTENERS); + + // Add an instance of each class to the list + if (listenerClasses != null) { + for (Class listenerClass : listenerClasses) { + + // Instantiate listener + Object listener = listenerClass.getConstructor().newInstance(); + + // Add listener to collection of listeners + listeners.add(listener); + + } + } + + } + catch (InstantiationException e) { + throw new GuacamoleException("Listener class is abstract.", e); + } + catch (IllegalAccessException e) { + throw new GuacamoleException("No access to listener constructor.", e); + } + catch (IllegalArgumentException e) { + // This should not happen, given there ARE no arguments + throw new GuacamoleException("Illegal arguments to listener constructor.", e); + } + catch (InvocationTargetException e) { + throw new GuacamoleException("Error while instantiating listener.", e); + } + catch (NoSuchMethodException e) { + throw new GuacamoleException("Listener has no default constructor.", e); + } + catch (SecurityException e) { + throw new GuacamoleException("Security restrictions prevent instantiation of listener.", e); + } + + } + + /** + * Returns the credentials used when the user associated with this session + * logged in. + * + * @return The credentials used when the user associated with this session + * logged in. + */ + public Credentials getCredentials() { + return credentials; + } + + /** + * Returns the UserContext associated with this session. + * + * @return The UserContext associated with this session. + */ + public UserContext getUserContext() { + return userContext; + } + + /** + * Returns the ClipboardState associated with this session. + * + * @return The ClipboardState associated with this session. + */ + public ClipboardState getClipboardState() { + return clipboardState; + } + + /** + * Returns a collection which iterates over instances of all listeners + * defined in guacamole.properties. For each listener defined in + * guacamole.properties, a new instance is created and stored in this + * collection. The contents of this collection is stored within the + * HttpSession, and will be reused if available. Each listener is + * instantiated once per session. Listeners are singleton classes within the + * session, but not globally. + * + * @return A collection which iterates over instances of all listeners + * defined in guacamole.properties. + */ + public Collection getListeners() { + return Collections.unmodifiableCollection(listeners); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/RestrictedFilter.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/RestrictedFilter.java deleted file mode 100644 index 7fbb9c9e7..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/RestrictedFilter.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2014 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic; - -import java.io.IOException; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.protocol.GuacamoleStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Filter which enforces authentication. If no user context is associated with - * the current HTTP session, or no HTTP session exists, the request is denied. - * - * @author Michael Jumper - */ -public class RestrictedFilter implements Filter { - - /** - * Logger for this class. - */ - private final Logger logger = LoggerFactory.getLogger(RestrictedFilter.class); - - @Override - public void init(FilterConfig config) throws ServletException { - // No configuration - } - - @Override - public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) - throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) resp; - - // Pull user context from session - UserContext context = null; - HttpSession session = request.getSession(false); - if (session != null) - context = AuthenticatingFilter.getUserContext(session); - - // If authenticated, proceed with rest of chain - if (context != null) - chain.doFilter(req, resp); - - // Otherwise, deny entire request - else { - final GuacamoleStatus status = GuacamoleStatus.CLIENT_UNAUTHORIZED; - final String message = "Not authenticated"; - - logger.info("HTTP request rejected: {}", message); - response.addHeader("Guacamole-Status-Code", Integer.toString(status.getGuacamoleStatusCode())); - response.addHeader("Guacamole-Error-Message", message); - response.sendError(status.getHttpStatusCode()); - } - - } - - @Override - public void destroy() { - // No destruction needed - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/RestrictedHttpServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/RestrictedHttpServlet.java deleted file mode 100644 index ea6f1dc32..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/RestrictedHttpServlet.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import org.glyptodon.guacamole.GuacamoleClientException; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleUnauthorizedException; -import org.glyptodon.guacamole.GuacamoleUnsupportedException; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.protocol.GuacamoleStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Abstract servlet which provides an restrictedService() function that is only - * called if the current HTTP session has already been authenticated by the - * AuthenticatingFilter. - * - * @author Michael Jumper - */ -public abstract class RestrictedHttpServlet extends HttpServlet { - - /** - * Logger for this class. - */ - private final Logger logger = LoggerFactory.getLogger(RestrictedHttpServlet.class); - - /** - * Sends an error on the given HTTP response using the information within - * the given GuacamoleStatus. - * - * @param response The HTTP response to use to send the error. - * @param guac_status The status to send - * @param message A human-readable message that can be presented to the - * user. - * @throws ServletException If an error prevents sending of the error - * code. - */ - public static void sendError(HttpServletResponse response, - GuacamoleStatus guac_status, String message) - throws ServletException { - - try { - - // If response not committed, send error code and message - if (!response.isCommitted()) { - response.addHeader("Guacamole-Status-Code", Integer.toString(guac_status.getGuacamoleStatusCode())); - response.addHeader("Guacamole-Error-Message", message); - response.sendError(guac_status.getHttpStatusCode()); - } - - } - catch (IOException ioe) { - - // If unable to send error at all due to I/O problems, - // rethrow as servlet exception - throw new ServletException(ioe); - - } - - } - - @Override - protected void service(HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - - // Set character encoding to UTF-8 if it's not already set - if(request.getCharacterEncoding() == null) { - try { - request.setCharacterEncoding("UTF-8"); - } catch (UnsupportedEncodingException exception) { - throw new ServletException(exception); - } - } - - try { - - // Obtain context from session - HttpSession httpSession = request.getSession(true); - UserContext context = AuthenticatingFilter.getUserContext(httpSession); - - // If no context, no authorizaton present - if (context == null) - throw new GuacamoleUnauthorizedException("Not authenticated"); - - // Allow servlet to run now that authentication has been validated - restrictedService(context, request, response); - - } - - // Catch any thrown guacamole exception and attempt to pass within the - // HTTP response, logging each error appropriately. - catch (GuacamoleClientException e) { - logger.debug("HTTP request rejected by RestrictedHttpServlet.", e); - sendError(response, e.getStatus(), e.getMessage()); - } - catch (GuacamoleUnsupportedException e) { - logger.debug("Unsupported operation.", e); - sendError(response, e.getStatus(), e.getMessage()); - } - catch (GuacamoleException e) { - logger.error("HTTP request failed: {}", e.getMessage()); - logger.debug("Internal server error while handling HTTP request to restricted resource.", e); - sendError(response, e.getStatus(), "Internal server error."); - } - - } - - /** - * Function called after the request and associated session are validated. - * If the current session is not associated with valid credentials, this - * function will not be called. - * - * @param context The current UserContext. - * @param request The HttpServletRequest being serviced. - * @param response An HttpServletResponse which controls the HTTP response - * of this servlet. - * - * @throws GuacamoleException If an error occurs that interferes with the - * normal operation of this servlet. - */ - protected abstract void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException; - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/SessionKeepAlive.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/SessionKeepAlive.java index 94cc94060..b065e4730 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/SessionKeepAlive.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/SessionKeepAlive.java @@ -22,29 +22,43 @@ package org.glyptodon.guacamole.net.basic; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import com.google.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Dummy servlet which provides an endpoint for arbitrary requests intended to - * simply keep the HTTP session from expiring. + * REST service which updates the last access time of the Guacamole session, + * preventing the session from becoming invalid. * * @author Michael Jumper */ -public class SessionKeepAlive extends RestrictedHttpServlet { +@Path("/keep-alive") +public class SessionKeepAlive { + + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(SessionKeepAlive.class); - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) { + @GET + @AuthProviderRESTExposure + public void updateSession(@QueryParam("token") String authToken) throws GuacamoleException { + + // Tickle the session + UserContext context = authenticationService.getUserContext(authToken); // Do nothing logger.debug("Keep-alive signal received from user \"{}\".", context.self().getUsername()); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketSupportLoader.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelModule.java similarity index 58% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketSupportLoader.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelModule.java index 34ca01f09..4466aecbf 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketSupportLoader.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelModule.java @@ -20,36 +20,31 @@ * THE SOFTWARE. */ -package org.glyptodon.guacamole.net.basic.websocket; +package org.glyptodon.guacamole.net.basic; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import javax.servlet.Servlet; -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import com.google.inject.Provider; +import com.google.inject.servlet.ServletModule; +import java.util.Arrays; +import javax.servlet.http.HttpServlet; +import javax.websocket.DeploymentException; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpointConfig; import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader; +import org.glyptodon.guacamole.net.basic.websocket.BasicGuacamoleWebSocketTunnelEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Simple ServletContextListener which loads a WebSocket tunnel implementation - * if available, using the Servlet 3.0 API to dynamically load and install - * the tunnel servlet. - * - * Note that because Guacamole depends on the Servlet 2.5 API, and 3.0 may - * not be available or needed if WebSocket is not desired, the 3.0 API is - * detected and invoked dynamically via reflection. + * Module which loads tunnel implementations. * * @author Michael Jumper */ -public class WebSocketSupportLoader implements ServletContextListener { +public class TunnelModule extends ServletModule { /** * Logger for this class. */ - private final Logger logger = LoggerFactory.getLogger(WebSocketSupportLoader.class); + private final Logger logger = LoggerFactory.getLogger(TunnelModule.class); /** * Classnames of all legacy (non-JSR) WebSocket tunnel implementations. @@ -94,52 +89,17 @@ public class WebSocketSupportLoader implements ServletContextListener { } - private boolean loadWebSocketTunnel(ServletContext context, String classname) { + private boolean loadWebSocketTunnel(String classname) { try { // Attempt to find WebSocket servlet - Class servlet = (Class) + Class servlet = (Class) GuacamoleClassLoader.getInstance().findClass(classname); - // Dynamically add servlet IF SERVLET 3.0 API AVAILABLE! - try { - - // Get servlet registration class - Class regClass = Class.forName("javax.servlet.ServletRegistration"); - - // Get and invoke addServlet() - Method addServlet = ServletContext.class.getMethod("addServlet", - String.class, Class.class); - Object reg = addServlet.invoke(context, "WebSocketTunnel", servlet); - - // Get and invoke addMapping() - Method addMapping = regClass.getMethod("addMapping", String[].class); - addMapping.invoke(reg, (Object) new String[]{"/websocket-tunnel"}); - - // If we succesfully load and register the WebSocket tunnel servlet, - // WebSocket is supported. - return true; - - } - - // Servlet API 3.0 unsupported - catch (ClassNotFoundException e) { - logger.info("Servlet API 3.0 not found.", e); - } - catch (NoSuchMethodException e) { - logger.warn("Servlet API 3.0 found, but incomplete.", e); - } - - // Servlet API 3.0 found, but errors during use - catch (IllegalAccessException e) { - logger.error("Unable to load WebSocket tunnel servlet: {}", e.getMessage()); - logger.debug("Error loading WebSocket tunnel servlet.", e); - } - catch (InvocationTargetException e) { - logger.error("Unable to load WebSocket tunnel servlet: {}", e.getMessage()); - logger.debug("Error loading WebSocket tunnel servlet.", e); - } + // Add WebSocket servlet + serve("/websocket-tunnel").with(servlet); + return true; } @@ -160,21 +120,50 @@ public class WebSocketSupportLoader implements ServletContextListener { } @Override - public void contextDestroyed(ServletContextEvent sce) { - } + protected void configureServlets() { - @Override - public void contextInitialized(ServletContextEvent sce) { + bind(TunnelRequestService.class); + + // Set up HTTP tunnel + serve("/tunnel").with(BasicGuacamoleTunnelServlet.class); // Check for JSR 356 support if (implementsJSR_356()) { + logger.info("JSR-356 WebSocket support present."); + + // Get container + ServerContainer container = (ServerContainer) getServletContext().getAttribute("javax.websocket.server.ServerContainer"); + if (container == null) { + logger.warn("ServerContainer attribute required by JSR-356 is missing. Cannot load JSR-356 WebSocket support."); + return; + } + + Provider tunnelRequestServiceProvider = getProvider(TunnelRequestService.class); + + // Build configuration for WebSocket tunnel + ServerEndpointConfig config = + ServerEndpointConfig.Builder.create(BasicGuacamoleWebSocketTunnelEndpoint.class, "/websocket-tunnel") + .configurator(new BasicGuacamoleWebSocketTunnelEndpoint.Configurator(tunnelRequestServiceProvider)) + .subprotocols(Arrays.asList(new String[]{"guacamole"})) + .build(); + + try { + + // Add configuration to container + container.addEndpoint(config); + + } + catch (DeploymentException e) { + logger.error("Unable to deploy WebSocket tunnel.", e); + } + return; } // Try to load each WebSocket tunnel in sequence for (String classname : WEBSOCKET_CLASSES) { - if (loadWebSocketTunnel(sce.getServletContext(), classname)) { + if (loadWebSocketTunnel(classname)) { logger.info("Legacy (non-JSR) WebSocket support loaded: {}", classname); return; } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java similarity index 90% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java index 0fb90992b..bea981608 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java @@ -22,6 +22,9 @@ package org.glyptodon.guacamole.net.basic; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.glyptodon.guacamole.net.basic.rest.clipboard.ClipboardRESTService; import java.util.Collection; import java.util.List; import javax.servlet.http.HttpSession; @@ -37,6 +40,7 @@ import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.event.SessionListenerCollection; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.net.event.TunnelCloseEvent; import org.glyptodon.guacamole.net.event.TunnelConnectEvent; import org.glyptodon.guacamole.net.event.listener.TunnelCloseListener; @@ -55,12 +59,19 @@ import org.slf4j.LoggerFactory; * * @author Michael Jumper */ -public class BasicTunnelRequestUtility { +@Singleton +public class TunnelRequestService { /** * Logger for this class. */ - private static final Logger logger = LoggerFactory.getLogger(BasicTunnelRequestUtility.class); + private final Logger logger = LoggerFactory.getLogger(TunnelRequestService.class); + + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; /** * Notifies all listeners in the given collection that a tunnel has been @@ -79,7 +90,7 @@ public class BasicTunnelRequestUtility { * error, the connect is canceled, and no other * listeners will run. */ - private static boolean notifyConnect(Collection listeners, UserContext context, + private boolean notifyConnect(Collection listeners, UserContext context, Credentials credentials, GuacamoleTunnel tunnel) throws GuacamoleException { @@ -119,7 +130,7 @@ public class BasicTunnelRequestUtility { * error, the close is canceled, and no other * listeners will run. */ - private static boolean notifyClose(Collection listeners, UserContext context, + private boolean notifyClose(Collection listeners, UserContext context, Credentials credentials, GuacamoleTunnel tunnel) throws GuacamoleException { @@ -151,7 +162,7 @@ public class BasicTunnelRequestUtility { * @return The created tunnel, or null if the tunnel could not be created. * @throws GuacamoleException If an error occurs while creating the tunnel. */ - public static GuacamoleTunnel createTunnel(TunnelRequest request) + public GuacamoleTunnel createTunnel(TunnelRequest request) throws GuacamoleException { HttpSession httpSession = request.getSession(); @@ -169,6 +180,10 @@ public class BasicTunnelRequestUtility { throw e; } + // Get auth token and session + String authToken = request.getParameter("authToken"); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + // Get ID of connection String id = request.getParameter("id"); TunnelRequest.IdentifierType id_type = TunnelRequest.IdentifierType.getType(id); @@ -181,17 +196,17 @@ public class BasicTunnelRequestUtility { id = id.substring(id_type.PREFIX.length()); // Get credentials - final Credentials credentials = AuthenticatingFilter.getCredentials(httpSession); + final Credentials credentials = session.getCredentials(); // Get context - final UserContext context = AuthenticatingFilter.getUserContext(httpSession); - + final UserContext context = session.getUserContext(); + // If no context or no credentials, not logged in if (context == null || credentials == null) throw new GuacamoleSecurityException("Cannot connect - user not logged in."); // Get clipboard - final ClipboardState clipboard = AuthenticatingFilter.getClipboardState(httpSession); + final ClipboardState clipboard = session.getClipboardState(); // Get client information GuacamoleClientInformation info = new GuacamoleClientInformation(); @@ -279,7 +294,7 @@ public class BasicTunnelRequestUtility { // Monitor instructions which pertain to server-side events, if necessary try { - if (GuacamoleProperties.getProperty(CaptureClipboard.INTEGRATION_ENABLED, false)) + if (GuacamoleProperties.getProperty(ClipboardRESTService.INTEGRATION_ENABLED, false)) return new MonitoringGuacamoleReader(clipboard, super.acquireReader()); } catch (GuacamoleException e) { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/ConnectionGroupUtility.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/ConnectionGroupUtility.java deleted file mode 100644 index 52cc35f91..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/ConnectionGroupUtility.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connectiongroups; - -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; - -/** - * A class that provides helper methods for the ConnectionGroup CRUD servlets. - * - * @author James Muehlner - */ -class ConnectionGroupUtility { - - // This class should not be instantiated - private ConnectionGroupUtility() {} - - /** - * Get the ConnectionGroupDirectory with the parent connection group - * specified by parentID. - * - * @param context The UserContext to search for the connectionGroup directory. - * @param parentID The ID of the parent connection group to search for. - * - * @return The ConnectionGroupDirectory with the parent connection group, - * if found. - * @throws GuacamoleException If an error is encountered while getting the - * connection group directory. - */ - static Directory findConnectionGroupDirectory( - UserContext context, String parentID) throws GuacamoleException { - - // Find the correct connection group directory - ConnectionGroup rootGroup = context.getRootConnectionGroup(); - Directory directory; - - Directory connectionGroupDirectory = - rootGroup.getConnectionGroupDirectory(); - - ConnectionGroup parentGroup = connectionGroupDirectory.get(parentID); - - if(parentGroup == null) - return null; - - directory = parentGroup.getConnectionGroupDirectory(); - - return directory; - } -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Create.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Create.java deleted file mode 100644 index 702cc851f..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Create.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connectiongroups; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which handles connection group creation. - * - * @author James Muehlner - */ -public class Create extends RestrictedHttpServlet { - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get name and type - String name = request.getParameter("name"); - String type = request.getParameter("type"); - - // Get the ID of the parent connection group - String parentID = request.getParameter("parentID"); - - // Find the correct connection group directory - Directory directory = - ConnectionGroupUtility.findConnectionGroupDirectory(context, parentID); - - if(directory == null) - throw new GuacamoleException("Connection group directory not found."); - - // Create connection skeleton - ConnectionGroup connectionGroup = new DummyConnectionGroup(); - connectionGroup.setName(name); - - if("balancing".equals(type)) - connectionGroup.setType(ConnectionGroup.Type.BALANCING); - else if("organizational".equals(type)) - connectionGroup.setType(ConnectionGroup.Type.ORGANIZATIONAL); - - // Add connection - directory.add(connectionGroup); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Delete.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Delete.java deleted file mode 100644 index 64de349d7..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Delete.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connectiongroups; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which handles connection group deletion. - * - * @author Michael Jumper - */ -public class Delete extends RestrictedHttpServlet { - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get ID - String identifier = request.getParameter("id"); - - // Attempt to get connection group directory - Directory directory = - context.getRootConnectionGroup().getConnectionGroupDirectory(); - - // Remove connection - directory.remove(identifier); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/DummyConnectionGroup.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/DummyConnectionGroup.java deleted file mode 100644 index 5bfd8eafd..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/DummyConnectionGroup.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connectiongroups; - -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.GuacamoleSocket; -import org.glyptodon.guacamole.net.auth.AbstractConnectionGroup; -import org.glyptodon.guacamole.net.auth.Connection; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; - -/** - * Basic ConnectionGroup skeleton, providing a means of storing Connection data - * prior to CRUD operations. This ConnectionGroup has no functionality for actually - * performing a connection operation, and does not promote any of the - * semantics that would otherwise be present because of the authentication - * provider. It is up to the authentication provider to create a new - * ConnectionGroup based on the information contained herein. - * - * @author James Muehlner - */ -public class DummyConnectionGroup extends AbstractConnectionGroup { - - @Override - public GuacamoleSocket connect(GuacamoleClientInformation info) throws GuacamoleException { - throw new UnsupportedOperationException("Connection unsupported in DummyConnectionGroup."); - } - - @Override - public Directory getConnectionDirectory() throws GuacamoleException { - throw new UnsupportedOperationException("Connection directory unsupported in DummyConnectionGroup."); - } - - @Override - public Directory getConnectionGroupDirectory() throws GuacamoleException { - throw new UnsupportedOperationException("Connection group directory unsuppprted in DummyConnectionGroup."); - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/List.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/List.java deleted file mode 100644 index c91ba72e7..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/List.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connectiongroups; - -import java.io.IOException; -import java.util.Set; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleSecurityException; -import org.glyptodon.guacamole.GuacamoleServerException; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.User; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; -import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; -import org.glyptodon.guacamole.net.auth.permission.Permission; -import org.glyptodon.guacamole.net.auth.permission.SystemPermission; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which outputs XML containing a list of all authorized - * connection groups for the current user. - * - * @author Michael Jumper - */ -public class List extends RestrictedHttpServlet { - - /** - * System administration permission. - */ - private static final Permission SYSTEM_PERMISSION = - new SystemPermission(SystemPermission.Type.ADMINISTER); - - - /** - * Checks whether the given user has permission to perform the given - * object operation. Security exceptions are handled appropriately - only - * non-security exceptions pass through. - * - * @param user The user whose permissions should be verified. - * @param type The type of operation to check for permission for. - * @param identifier The identifier of the connection the operation - * would be performed upon. - * @return true if permission is granted, false otherwise. - * - * @throws GuacamoleException If an error occurs while checking permissions. - */ - private boolean hasConfigPermission(User user, ObjectPermission.Type type, - String identifier) - throws GuacamoleException { - - // Build permission - Permission permission = new ConnectionPermission( - type, - identifier - ); - - try { - // Return result of permission check, if possible - return user.hasPermission(permission); - } - catch (GuacamoleSecurityException e) { - // If cannot check due to security restrictions, no permission - return false; - } - - } - - /** - * Writes the XML for the given connection group. - * - * @param self The user whose permissions dictate the availability of the - * data written. - * @param xml The XMLStremWriter to use when writing the data. - * @param group The connection group whose XML representation will be - * written. - * @throws GuacamoleException If an error occurs while reading the - * requested data. - * @throws XMLStreamException If an error occurs while writing the XML. - */ - private void writeConnectionGroup(User self, XMLStreamWriter xml, - ConnectionGroup group) throws GuacamoleException, XMLStreamException { - - // Write group - xml.writeStartElement("group"); - xml.writeAttribute("id", group.getIdentifier()); - xml.writeAttribute("name", group.getName()); - - // Write group type - switch (group.getType()) { - - case ORGANIZATIONAL: - xml.writeAttribute("type", "organizational"); - break; - - case BALANCING: - xml.writeAttribute("type", "balancing"); - break; - - } - - // Write contained connection groups - writeConnectionGroups(self, xml, group.getConnectionGroupDirectory()); - - // End of group - xml.writeEndElement(); - - } - - /** - * Writes the XML for the given directory of connection groups. - * - * @param self The user whose permissions dictate the availability of the - * data written. - * @param xml The XMLStremWriter to use when writing the data. - * @param directory The directory whose XML representation will be - * written. - * @throws GuacamoleException If an error occurs while reading the - * requested data. - * @throws XMLStreamException If an error occurs while writing the XML. - */ - private void writeConnectionGroups(User self, XMLStreamWriter xml, - Directory directory) - throws GuacamoleException, XMLStreamException { - - // If no connections, write nothing - Set identifiers = directory.getIdentifiers(); - if (identifiers.isEmpty()) - return; - - // Begin connections - xml.writeStartElement("groups"); - - // For each entry, write corresponding connection element - for (String identifier : identifiers) { - - // Write each group - ConnectionGroup group = directory.get(identifier); - writeConnectionGroup(self, xml, group); - - } - - // End connections - xml.writeEndElement(); - - } - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Do not cache - response.setHeader("Cache-Control", "no-cache"); - - // Write XML content type - response.setHeader("Content-Type", "text/xml"); - - // Set encoding - response.setCharacterEncoding("UTF-8"); - - // Get root group - ConnectionGroup root = context.getRootConnectionGroup(); - - // Write actual XML - try { - - // Get self - User self = context.self(); - - XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); - XMLStreamWriter xml = outputFactory.createXMLStreamWriter(response.getWriter()); - - // Write content of root group - xml.writeStartDocument(); - writeConnectionGroup(self, xml, root); - xml.writeEndDocument(); - - } - catch (XMLStreamException e) { - throw new GuacamoleServerException( - "Unable to write connection group list XML.", e); - } - catch (IOException e) { - throw new GuacamoleServerException( - "I/O error writing connection group list XML.", e); - } - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Move.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Move.java deleted file mode 100644 index 363f10d83..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Move.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connectiongroups; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which handles moving connection groups. - * - * @author James Muehlner - */ -public class Move extends RestrictedHttpServlet { - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get ID - String identifier = request.getParameter("id"); - - // Get the identifier of the new parent connection group - String parentID = request.getParameter("parentID"); - - // Attempt to get the new parent connection group directory - Directory newParentDirectory = - ConnectionGroupUtility.findConnectionGroupDirectory(context, parentID); - - // Attempt to get root connection group directory - Directory directory = - context.getRootConnectionGroup().getConnectionGroupDirectory(); - - // Move connection group - directory.move(identifier, newParentDirectory); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Update.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Update.java deleted file mode 100644 index 09931ef87..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/Update.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connectiongroups; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which handles connection group update. - * - * @author James Muehlner - */ -public class Update extends RestrictedHttpServlet { - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get ID, name, and type - String identifier = request.getParameter("id"); - String name = request.getParameter("name"); - String type = request.getParameter("type"); - - // Attempt to get connection group directory - Directory directory = - context.getRootConnectionGroup().getConnectionGroupDirectory(); - - // Create connection group skeleton - ConnectionGroup connectionGroup = directory.get(identifier); - connectionGroup.setName(name); - - if("balancing".equals(type)) - connectionGroup.setType(ConnectionGroup.Type.BALANCING); - else if("organizational".equals(type)) - connectionGroup.setType(ConnectionGroup.Type.ORGANIZATIONAL); - - // Update connection group - directory.update(connectionGroup); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/ConnectionUtility.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/ConnectionUtility.java deleted file mode 100644 index de4e23942..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/ConnectionUtility.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connections; - -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.Connection; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; - -/** - * A class that provides helper methods for the Connection CRUD servlets. - * - * @author James Muehlner - */ -class ConnectionUtility { - - // This class should not be instantiated - private ConnectionUtility() {} - - /** - * Get the ConnectionDirectory with the parent connection group specified by - * parentID. - * - * @param context The UserContext to search for the connection directory. - * @param parentID The ID of the parent connection group to search for. - * - * @return The ConnectionDirectory with the parent connection group, - * if found. - * @throws GuacamoleException If an error is encountered while getting the - * connection directory. - */ - static Directory findConnectionDirectory( - UserContext context, String parentID) throws GuacamoleException { - - // Find the correct connection directory - ConnectionGroup rootGroup = context.getRootConnectionGroup(); - Directory directory; - - Directory connectionGroupDirectory = - rootGroup.getConnectionGroupDirectory(); - - ConnectionGroup parentGroup = connectionGroupDirectory.get(parentID); - - if(parentGroup == null) - return null; - - directory = parentGroup.getConnectionDirectory(); - - return directory; - } -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Create.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Create.java deleted file mode 100644 index 5fa050b36..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Create.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connections; - -import java.util.Enumeration; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.Connection; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; -import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; - -/** - * Simple HttpServlet which handles connection creation. - * - * @author Michael Jumper - */ -public class Create extends RestrictedHttpServlet { - - /** - * Prefix given to a parameter name when that parameter is a protocol- - * specific parameter meant for the configuration. - */ - public static final String PARAMETER_PREFIX = "_"; - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get name and protocol - String name = request.getParameter("name"); - String protocol = request.getParameter("protocol"); - - // Get the ID of the parent connection group - String parentID = request.getParameter("parentID"); - - // Find the correct connection directory - Directory directory = - ConnectionUtility.findConnectionDirectory(context, parentID); - - if(directory == null) - throw new GuacamoleException("Connection directory not found."); - - // Create config - GuacamoleConfiguration config = new GuacamoleConfiguration(); - config.setProtocol(protocol); - - // Load parameters into config - Enumeration params = request.getParameterNames(); - while (params.hasMoreElements()) { - - // If parameter starts with prefix, load corresponding parameter - // value into config - String param = params.nextElement(); - if (param.startsWith(PARAMETER_PREFIX)) - config.setParameter( - param.substring(PARAMETER_PREFIX.length()), - request.getParameter(param)); - - } - - // Create connection skeleton - Connection connection = new DummyConnection(); - connection.setName(name); - connection.setConfiguration(config); - - // Add connection - directory.add(connection); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/DummyConnection.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/DummyConnection.java deleted file mode 100644 index c1d7c173f..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/DummyConnection.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connections; - -import java.util.List; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.GuacamoleSocket; -import org.glyptodon.guacamole.net.auth.AbstractConnection; -import org.glyptodon.guacamole.net.auth.ConnectionRecord; -import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; - -/** - * Basic Connection skeleton, providing a means of storing Connection data - * prior to CRUD operations. This Connection has no functionality for actually - * performing a connection operation, and does not promote any of the - * semantics that would otherwise be present because of the authentication - * provider. It is up to the authentication provider to create a new - * Connection based on the information contained herein. - * - * @author Michael Jumper - */ -public class DummyConnection extends AbstractConnection { - - @Override - public GuacamoleSocket connect(GuacamoleClientInformation info) throws GuacamoleException { - throw new UnsupportedOperationException("Connection unsupported in DummyConnection."); - } - - @Override - public List getHistory() throws GuacamoleException { - throw new UnsupportedOperationException("History unsupported in DummyConnection."); - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/List.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/List.java deleted file mode 100644 index 73d353360..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/List.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connections; - -import java.io.IOException; -import java.util.Set; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleSecurityException; -import org.glyptodon.guacamole.GuacamoleServerException; -import org.glyptodon.guacamole.net.auth.Connection; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.ConnectionRecord; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.User; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.auth.permission.ConnectionGroupPermission; -import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; -import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; -import org.glyptodon.guacamole.net.auth.permission.Permission; -import org.glyptodon.guacamole.net.auth.permission.SystemPermission; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; -import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; - -/** - * Simple HttpServlet which outputs XML containing a list of all authorized - * configurations for the current user. - * - * @author Michael Jumper - */ -public class List extends RestrictedHttpServlet { - - /** - * System administration permission. - */ - private static final Permission SYSTEM_PERMISSION = - new SystemPermission(SystemPermission.Type.ADMINISTER); - - - /** - * Checks whether the given user has permission to perform the given - * object operation. Security exceptions are handled appropriately - only - * non-security exceptions pass through. - * - * @param user The user whose permissions should be verified. - * @param type The type of operation to check for permission for. - * @param identifier The identifier of the connection the operation - * would be performed upon. - * @return true if permission is granted, false otherwise. - * - * @throws GuacamoleException If an error occurs while checking permissions. - */ - private boolean hasConfigPermission(User user, ObjectPermission.Type type, - String identifier) - throws GuacamoleException { - - // Build permission - Permission permission = new ConnectionPermission( - type, - identifier - ); - - try { - // Return result of permission check, if possible - return user.hasPermission(permission); - } - catch (GuacamoleSecurityException e) { - // If cannot check due to security restrictions, no permission - return false; - } - - } - - /** - * Writes the XML for the given connection group. - * - * @param self The user whose permissions dictate the availability of the - * data written. - * @param xml The XMLStremWriter to use when writing the data. - * @param group The connection group whose XML representation will be - * written. - * @throws GuacamoleException If an error occurs while reading the - * requested data. - * @throws XMLStreamException If an error occurs while writing the XML. - */ - private void writeConnectionGroup(User self, XMLStreamWriter xml, - ConnectionGroup group) throws GuacamoleException, XMLStreamException { - - // Write group - xml.writeStartElement("group"); - xml.writeAttribute("id", group.getIdentifier()); - xml.writeAttribute("name", group.getName()); - - // Write group type - switch (group.getType()) { - - case ORGANIZATIONAL: - xml.writeAttribute("type", "organizational"); - break; - - case BALANCING: - xml.writeAttribute("type", "balancing"); - break; - - } - - Permission group_admin_permission = new ConnectionGroupPermission( - ObjectPermission.Type.ADMINISTER, group.getIdentifier()); - - // Attempt to list contained groups and connections ONLY if the group - // is organizational or we have admin rights to it - if (group.getType() == ConnectionGroup.Type.ORGANIZATIONAL - || self.hasPermission(SYSTEM_PERMISSION) - || self.hasPermission(group_admin_permission)) { - writeConnections(self, xml, group.getConnectionDirectory()); - writeConnectionGroups(self, xml, group.getConnectionGroupDirectory()); - } - - // End of group - xml.writeEndElement(); - - } - - /** - * Writes the XML for the given connection. - * - * @param self The user whose permissions dictate the availability of the - * data written. - * @param xml The XMLStremWriter to use when writing the data. - * @param connection The connection whose XML representation will be - * written. - * @throws GuacamoleException If an error occurs while reading the - * requested data. - * @throws XMLStreamException If an error occurs while writing the XML. - */ - private void writeConnection(User self, XMLStreamWriter xml, - Connection connection) throws GuacamoleException, XMLStreamException { - - // Write connection - xml.writeStartElement("connection"); - xml.writeAttribute("id", connection.getIdentifier()); - xml.writeAttribute("name", connection.getName()); - xml.writeAttribute("protocol", - connection.getConfiguration().getProtocol()); - - // If update permission available, include parameters - if (self.hasPermission(SYSTEM_PERMISSION) || - hasConfigPermission(self, ObjectPermission.Type.UPDATE, - connection.getIdentifier())) { - - // As update permission is present, also list parameters - GuacamoleConfiguration config = connection.getConfiguration(); - for (String name : config.getParameterNames()) { - - String value = connection.getConfiguration().getParameter(name); - xml.writeStartElement("param"); - xml.writeAttribute("name", name); - - if (value != null) - xml.writeCharacters(value); - - xml.writeEndElement(); - } - - } - - // Write history - xml.writeStartElement("history"); - for (ConnectionRecord record : connection.getHistory()) { - xml.writeStartElement("record"); - - // Start date - xml.writeAttribute("start", - Long.toString(record.getStartDate().getTime())); - - // End date - if (record.getEndDate() != null) - xml.writeAttribute("end", - Long.toString(record.getEndDate().getTime())); - - // Whether connection currently active - if (record.isActive()) - xml.writeAttribute("active", "yes"); - - // User involved - xml.writeCharacters(record.getUsername()); - - xml.writeEndElement(); - } - xml.writeEndElement(); - - // End connection - xml.writeEndElement(); - - } - - /** - * Writes the XML for the given directory of connection groups. - * - * @param self The user whose permissions dictate the availability of the - * data written. - * @param xml The XMLStremWriter to use when writing the data. - * @param directory The directory whose XML representation will be - * written. - * @throws GuacamoleException If an error occurs while reading the - * requested data. - * @throws XMLStreamException If an error occurs while writing the XML. - */ - private void writeConnectionGroups(User self, XMLStreamWriter xml, - Directory directory) - throws GuacamoleException, XMLStreamException { - - // If no connections, write nothing - Set identifiers = directory.getIdentifiers(); - if (identifiers.isEmpty()) - return; - - // Begin connections - xml.writeStartElement("groups"); - - // For each entry, write corresponding connection element - for (String identifier : identifiers) { - - // Write each group - ConnectionGroup group = directory.get(identifier); - writeConnectionGroup(self, xml, group); - - } - - // End connections - xml.writeEndElement(); - - } - - /** - * Writes the XML for the given directory of connections. - * - * @param self The user whose permissions dictate the availability of the - * data written. - * @param xml The XMLStremWriter to use when writing the data. - * @param directory The directory whose XML representation will be - * written. - * @throws GuacamoleException If an error occurs while reading the - * requested data. - * @throws XMLStreamException If an error occurs while writing the XML. - */ - private void writeConnections(User self, XMLStreamWriter xml, - Directory directory) - throws GuacamoleException, XMLStreamException { - - // If no connections, write nothing - Set identifiers = directory.getIdentifiers(); - if (identifiers.isEmpty()) - return; - - // Begin connections - xml.writeStartElement("connections"); - - // For each entry, write corresponding connection element - for (String identifier : identifiers) { - - // Write each connection - Connection connection = directory.get(identifier); - writeConnection(self, xml, connection); - - } - - // End connections - xml.writeEndElement(); - - } - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Do not cache - response.setHeader("Cache-Control", "no-cache"); - - // Write XML content type - response.setHeader("Content-Type", "text/xml"); - - // Set encoding - response.setCharacterEncoding("UTF-8"); - - // Get root group - ConnectionGroup root = context.getRootConnectionGroup(); - - // Write actual XML - try { - - // Get self - User self = context.self(); - - XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); - XMLStreamWriter xml = outputFactory.createXMLStreamWriter(response.getWriter()); - - // Write content of root group - xml.writeStartDocument(); - writeConnectionGroup(self, xml, root); - xml.writeEndDocument(); - - } - catch (XMLStreamException e) { - throw new GuacamoleServerException( - "Unable to write configuration list XML.", e); - } - catch (IOException e) { - throw new GuacamoleServerException( - "I/O error writing configuration list XML.", e); - } - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Move.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Move.java deleted file mode 100644 index e2fa7c555..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Move.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connections; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.Connection; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which handles moving connections. - * - * @author Michael Jumper - */ -public class Move extends RestrictedHttpServlet { - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get ID - String identifier = request.getParameter("id"); - - // Get the identifier of the new parent connection group - String parentID = request.getParameter("parentID"); - - // Attempt to get the new parent connection directory - Directory newParentDirectory = - ConnectionUtility.findConnectionDirectory(context, parentID); - - // Attempt to get root connection directory - Directory directory = - context.getRootConnectionGroup().getConnectionDirectory(); - - // Move connection - directory.move(identifier, newParentDirectory); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Update.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Update.java deleted file mode 100644 index 6b4c90d99..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Update.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.connections; - -import java.util.Enumeration; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.Connection; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; -import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; - -/** - * Simple HttpServlet which handles connection update. - * - * @author Michael Jumper - */ -public class Update extends RestrictedHttpServlet { - - /** - * Prefix given to a parameter name when that parameter is a protocol- - * specific parameter meant for the configuration. - */ - public static final String PARAMETER_PREFIX = "_"; - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get ID, name, and protocol - String identifier = request.getParameter("id"); - String name = request.getParameter("name"); - String protocol = request.getParameter("protocol"); - - // Attempt to get connection directory - Directory directory = - context.getRootConnectionGroup().getConnectionDirectory(); - - // Create config - GuacamoleConfiguration config = new GuacamoleConfiguration(); - config.setProtocol(protocol); - - // Load parameters into config - Enumeration params = request.getParameterNames(); - while (params.hasMoreElements()) { - - // If parameter starts with prefix, load corresponding parameter - // value into config - String param = params.nextElement(); - if (param.startsWith(PARAMETER_PREFIX)) - config.setParameter( - param.substring(PARAMETER_PREFIX.length()), - request.getParameter(param)); - - } - - // Create connection skeleton - Connection connection = directory.get(identifier); - connection.setName(name); - connection.setConfiguration(config); - - // Update connection - directory.update(connection); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/permissions/List.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/permissions/List.java deleted file mode 100644 index df0c197fb..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/permissions/List.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.permissions; - -import java.io.IOException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import org.glyptodon.guacamole.GuacamoleClientException; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleSecurityException; -import org.glyptodon.guacamole.GuacamoleServerException; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.User; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.auth.permission.ConnectionGroupPermission; -import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; -import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; -import org.glyptodon.guacamole.net.auth.permission.Permission; -import org.glyptodon.guacamole.net.auth.permission.SystemPermission; -import org.glyptodon.guacamole.net.auth.permission.UserPermission; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which outputs XML containing a list of all visible - * permissions of a given user. - * - * @author Michael Jumper - */ -public class List extends RestrictedHttpServlet { - - /** - * Returns the XML attribute value representation of the given - * SystemPermission.Type. - * - * @param type The SystemPermission.Type to translate into a String. - * @return The XML attribute value representation of the given - * SystemPermission.Type. - * - * @throws GuacamoleException If the type given is not implemented. - */ - private String toString(SystemPermission.Type type) - throws GuacamoleException { - - switch (type) { - case CREATE_USER: return "create-user"; - case CREATE_CONNECTION: return "create-connection"; - case CREATE_CONNECTION_GROUP: return "create-connection-group"; - case ADMINISTER: return "admin"; - } - - throw new GuacamoleException("Unknown permission type: " + type); - - } - - /** - * Returns the XML attribute value representation of the given - * ObjectPermission.Type. - * - * @param type The ObjectPermission.Type to translate into a String. - * @return The XML attribute value representation of the given - * ObjectPermission.Type. - * - * @throws GuacamoleException If the type given is not implemented. - */ - private String toString(ObjectPermission.Type type) - throws GuacamoleException { - - switch (type) { - case READ: return "read"; - case UPDATE: return "update"; - case DELETE: return "delete"; - case ADMINISTER: return "admin"; - } - - throw new GuacamoleException("Unknown permission type: " + type); - - } - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Do not cache - response.setHeader("Cache-Control", "no-cache"); - - // Set encoding - response.setCharacterEncoding("UTF-8"); - - // Write actual XML - try { - - User user; - - // Get username - String username = request.getParameter("user"); - if (username != null) { - - // Get user directory - Directory users = context.getUserDirectory(); - - // Get specific user - user = users.get(username); - } - else - user = context.self(); - - if (user == null) - throw new GuacamoleSecurityException("No such user."); - - // Write XML content type - response.setHeader("Content-Type", "text/xml"); - - XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); - XMLStreamWriter xml = outputFactory.createXMLStreamWriter(response.getWriter()); - - // Begin document - xml.writeStartDocument(); - xml.writeStartElement("permissions"); - xml.writeAttribute("user", user.getUsername()); - - // For each entry, write corresponding user element - for (Permission permission : user.getPermissions()) { - - // System permission - if (permission instanceof SystemPermission) { - - // Get permission - SystemPermission sp = (SystemPermission) permission; - - // Write permission - xml.writeEmptyElement("system"); - xml.writeAttribute("type", toString(sp.getType())); - - } - - // Config permission - else if (permission instanceof ConnectionPermission) { - - // Get permission - ConnectionPermission cp = - (ConnectionPermission) permission; - - // Write permission - xml.writeEmptyElement("connection"); - xml.writeAttribute("type", toString(cp.getType())); - xml.writeAttribute("name", cp.getObjectIdentifier()); - - } - - // Connection group permission - else if (permission instanceof ConnectionGroupPermission) { - - // Get permission - ConnectionGroupPermission cgp = - (ConnectionGroupPermission) permission; - - // Write permission - xml.writeEmptyElement("connection-group"); - xml.writeAttribute("type", toString(cgp.getType())); - xml.writeAttribute("name", cgp.getObjectIdentifier()); - - } - - // User permission - else if (permission instanceof UserPermission) { - - // Get permission - UserPermission up = (UserPermission) permission; - - // Write permission - xml.writeEmptyElement("user"); - xml.writeAttribute("type", toString(up.getType())); - xml.writeAttribute("name", up.getObjectIdentifier()); - - } - - else - throw new GuacamoleClientException( - "Unsupported permission type."); - - } - - // End document - xml.writeEndElement(); - xml.writeEndDocument(); - - } - catch (XMLStreamException e) { - throw new GuacamoleServerException( - "Unable to write permission list XML.", e); - } - catch (IOException e) { - throw new GuacamoleServerException( - "I/O error writing permission list XML.", e); - } - - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Create.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Create.java deleted file mode 100644 index 06bebc693..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Create.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.users; - -import java.util.UUID; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.User; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which handles user creation. - * - * @author Michael Jumper - */ -public class Create extends RestrictedHttpServlet { - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Create user as specified - String username = request.getParameter("name"); - - // Attempt to get user directory - Directory directory = - context.getUserDirectory(); - - // Create user skeleton - User user = new DummyUser(); - user.setUsername(username); - user.setPassword(UUID.randomUUID().toString()); - - // Add user - directory.add(user); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/DummyUser.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/DummyUser.java deleted file mode 100644 index 15cd9c356..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/DummyUser.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.users; - -import java.util.HashSet; -import java.util.Set; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.AbstractUser; -import org.glyptodon.guacamole.net.auth.permission.Permission; - -/** - * Basic User skeleton, providing a means of storing User data prior to CRUD - * operations. This User does not promote any of the semantics that would - * otherwise be present because of the authentication provider. It is up to the - * authentication provider to create a new User based on the information - * contained herein. - * - * @author Michael Jumper - */ -public class DummyUser extends AbstractUser { - - /** - * Set of all available permissions. - */ - private Set permissions = new HashSet(); - - @Override - public Set getPermissions() throws GuacamoleException { - return permissions; - } - - @Override - public boolean hasPermission(Permission permission) throws GuacamoleException { - return permissions.contains(permission); - } - - @Override - public void addPermission(Permission permission) throws GuacamoleException { - permissions.add(permission); - } - - @Override - public void removePermission(Permission permission) throws GuacamoleException { - permissions.remove(permission); - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/List.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/List.java deleted file mode 100644 index a62f8b6b1..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/List.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.users; - -import java.io.IOException; -import java.util.Set; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleServerException; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.User; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which outputs XML containing a list of all visible users. - * - * @author Michael Jumper - */ -public class List extends RestrictedHttpServlet { - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Do not cache - response.setHeader("Cache-Control", "no-cache"); - - // Write XML content type - response.setHeader("Content-Type", "text/xml"); - - // Set encoding - response.setCharacterEncoding("UTF-8"); - - // Write actual XML - try { - - // Get user directory - Directory directory = context.getUserDirectory(); - - // Get users - Set users = directory.getIdentifiers(); - - XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); - XMLStreamWriter xml = outputFactory.createXMLStreamWriter(response.getWriter()); - - // Begin document - xml.writeStartDocument(); - xml.writeStartElement("users"); - - // For each entry, write corresponding user element - for (String username : users) { - - // Get user - User user = directory.get(username); - - // Write user - xml.writeEmptyElement("user"); - xml.writeAttribute("name", user.getUsername()); - - } - - // End document - xml.writeEndElement(); - xml.writeEndDocument(); - - } - catch (XMLStreamException e) { - throw new GuacamoleServerException( - "Unable to write configuration list XML.", e); - } - catch (IOException e) { - throw new GuacamoleServerException( - "I/O error writing configuration list XML.", e); - } - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Update.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Update.java deleted file mode 100644 index 7487357ec..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Update.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.crud.users; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.GuacamoleClientException; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.User; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.auth.permission.ConnectionGroupPermission; -import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; -import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; -import org.glyptodon.guacamole.net.auth.permission.Permission; -import org.glyptodon.guacamole.net.auth.permission.SystemPermission; -import org.glyptodon.guacamole.net.auth.permission.UserPermission; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; - -/** - * Simple HttpServlet which handles user update. - * - * @author Michael Jumper - */ -public class Update extends RestrictedHttpServlet { - - /** - * String given for user creation permission. - */ - private static final String CREATE_USER_PERMISSION = "create-user"; - - /** - * String given for connection creation permission. - */ - private static final String CREATE_CONNECTION_PERMISSION = "create-connection"; - - /** - * String given for connection group creation permission. - */ - private static final String CREATE_CONNECTION_GROUP_PERMISSION = "create-connection-group"; - - /** - * String given for system administration permission. - */ - private static final String ADMIN_PERMISSION = "admin"; - - /** - * Prefix given before an object identifier for read permission. - */ - private static final String READ_PREFIX = "read:"; - - /** - * Prefix given before an object identifier for delete permission. - */ - private static final String DELETE_PREFIX = "delete:"; - - /** - * Prefix given before an object identifier for update permission. - */ - private static final String UPDATE_PREFIX = "update:"; - - /** - * Prefix given before an object identifier for administration permission. - */ - private static final String ADMIN_PREFIX = "admin:"; - - /** - * Given a permission string, returns the corresponding system permission. - * - * @param str The permission string to parse. - * @return The parsed system permission. - * @throws GuacamoleException If the given string could not be parsed. - */ - private Permission parseSystemPermission(String str) - throws GuacamoleException { - - // Create user - if (str.equals(CREATE_USER_PERMISSION)) - return new SystemPermission(SystemPermission.Type.CREATE_USER); - - // Create connection - if (str.equals(CREATE_CONNECTION_PERMISSION)) - return new SystemPermission(SystemPermission.Type.CREATE_CONNECTION); - - // Create connection group - if (str.equals(CREATE_CONNECTION_GROUP_PERMISSION)) - return new SystemPermission(SystemPermission.Type.CREATE_CONNECTION_GROUP); - - // Administration - if (str.equals(ADMIN_PERMISSION)) - return new SystemPermission(SystemPermission.Type.ADMINISTER); - - throw new GuacamoleException("Invalid permission string."); - - } - - /** - * Given a permission string, returns the corresponding user permission. - * - * @param str The permission string to parse. - * @return The parsed user permission. - * @throws GuacamoleException If the given string could not be parsed. - */ - private Permission parseUserPermission(String str) - throws GuacamoleException { - - // Read - if (str.startsWith(READ_PREFIX)) - return new UserPermission(ObjectPermission.Type.READ, - str.substring(READ_PREFIX.length())); - - // Update - if (str.startsWith(UPDATE_PREFIX)) - return new UserPermission(ObjectPermission.Type.UPDATE, - str.substring(UPDATE_PREFIX.length())); - - // Delete - if (str.startsWith(DELETE_PREFIX)) - return new UserPermission(ObjectPermission.Type.DELETE, - str.substring(DELETE_PREFIX.length())); - - // Administration - if (str.startsWith(ADMIN_PREFIX)) - return new UserPermission(ObjectPermission.Type.ADMINISTER, - str.substring(ADMIN_PREFIX.length())); - - throw new GuacamoleException("Invalid permission string."); - - } - - /** - * Given a permission string, returns the corresponding connection - * permission. - * - * @param str The permission string to parse. - * @return The parsed connection permission. - * @throws GuacamoleException If the given string could not be parsed. - */ - private Permission parseConnectionPermission(String str) - throws GuacamoleException { - - // Read - if (str.startsWith(READ_PREFIX)) - return new ConnectionPermission(ObjectPermission.Type.READ, - str.substring(READ_PREFIX.length())); - - // Update - if (str.startsWith(UPDATE_PREFIX)) - return new ConnectionPermission(ObjectPermission.Type.UPDATE, - str.substring(UPDATE_PREFIX.length())); - - // Delete - if (str.startsWith(DELETE_PREFIX)) - return new ConnectionPermission(ObjectPermission.Type.DELETE, - str.substring(DELETE_PREFIX.length())); - - // Administration - if (str.startsWith(ADMIN_PREFIX)) - return new ConnectionPermission(ObjectPermission.Type.ADMINISTER, - str.substring(ADMIN_PREFIX.length())); - - throw new GuacamoleClientException("Invalid permission string."); - - } - - /** - * Given a permission string, returns the corresponding connection group - * permission. - * - * @param str The permission string to parse. - * @return The parsed connection group permission. - * @throws GuacamoleException If the given string could not be parsed. - */ - private Permission parseConnectionGroupPermission(String str) - throws GuacamoleException { - - // Read - if (str.startsWith(READ_PREFIX)) - return new ConnectionGroupPermission(ObjectPermission.Type.READ, - str.substring(READ_PREFIX.length())); - - // Update - if (str.startsWith(UPDATE_PREFIX)) - return new ConnectionGroupPermission(ObjectPermission.Type.UPDATE, - str.substring(UPDATE_PREFIX.length())); - - // Delete - if (str.startsWith(DELETE_PREFIX)) - return new ConnectionGroupPermission(ObjectPermission.Type.DELETE, - str.substring(DELETE_PREFIX.length())); - - // Administration - if (str.startsWith(ADMIN_PREFIX)) - return new ConnectionGroupPermission(ObjectPermission.Type.ADMINISTER, - str.substring(ADMIN_PREFIX.length())); - - throw new GuacamoleClientException("Invalid permission string."); - - } - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Create user as specified - String username = request.getParameter("name"); - String password = request.getParameter("password"); - - // Attempt to get user directory - Directory directory = - context.getUserDirectory(); - - // Get user data, setting password if given - User user = directory.get(username); - user.setUsername(username); - if (password != null) - user.setPassword(password); - - /* - * NEW PERMISSIONS - */ - - // Set added system permissions - String[] add_sys_permission = request.getParameterValues("+sys"); - if (add_sys_permission != null) { - for (String str : add_sys_permission) - user.addPermission(parseSystemPermission(str)); - } - - // Set added user permissions - String[] add_user_permission = request.getParameterValues("+user"); - if (add_user_permission != null) { - for (String str : add_user_permission) - user.addPermission(parseUserPermission(str)); - } - - // Set added connection permissions - String[] add_connection_permission = request.getParameterValues("+connection"); - if (add_connection_permission != null) { - for (String str : add_connection_permission) - user.addPermission(parseConnectionPermission(str)); - } - - // Set added connection group permissions - String[] add_connection_group_permission = request.getParameterValues("+connection-group"); - if (add_connection_group_permission != null) { - for (String str : add_connection_group_permission) - user.addPermission(parseConnectionGroupPermission(str)); - } - - /* - * REMOVED PERMISSIONS - */ - - // Unset removed system permissions - String[] remove_sys_permission = request.getParameterValues("-sys"); - if (remove_sys_permission != null) { - for (String str : remove_sys_permission) - user.removePermission(parseSystemPermission(str)); - } - - // Unset removed user permissions - String[] remove_user_permission = request.getParameterValues("-user"); - if (remove_user_permission != null) { - for (String str : remove_user_permission) - user.removePermission(parseUserPermission(str)); - } - - // Unset removed connection permissions - String[] remove_connection_permission = request.getParameterValues("-connection"); - if (remove_connection_permission != null) { - for (String str : remove_connection_permission) - user.removePermission(parseConnectionPermission(str)); - } - - // Unset removed connection group permissions - String[] remove_connection_group_permission = request.getParameterValues("-connection-group"); - if (remove_connection_group_permission != null) { - for (String str : remove_connection_group_permission) - user.removePermission(parseConnectionGroupPermission(str)); - } - - // Update user - directory.update(user); - - } - -} - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/LogbackInitializer.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/LogModule.java similarity index 85% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/LogbackInitializer.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/LogModule.java index 3ea4ed6d8..a5ba1a039 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/LogbackInitializer.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/LogModule.java @@ -26,32 +26,26 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.util.StatusPrinter; +import com.google.inject.AbstractModule; import java.io.File; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; import org.glyptodon.guacamole.properties.GuacamoleHome; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Initializes the logback logging subsystem, used behind SLF4J within - * Guacamole for all logging. + * Initializes the logging subsystem. * * @author Michael Jumper */ -public class LogbackInitializer implements ServletContextListener { +public class LogModule extends AbstractModule { /** * Logger for this class. */ - private final Logger logger = LoggerFactory.getLogger(LogbackInitializer.class); + private final Logger logger = LoggerFactory.getLogger(LogModule.class); @Override - public void contextDestroyed(ServletContextEvent sce) { - } - - @Override - public void contextInitialized(ServletContextEvent sce) { + protected void configure() { // Only load logback configuration if GUACAMOLE_HOME exists File guacamoleHome = GuacamoleHome.getDirectory(); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIConstants.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIConstants.java new file mode 100644 index 000000000..f6277eeaa --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIConstants.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +/** + * Useful constants for the REST API. + * + * @author James Muehlner + */ +public class APIConstants { + + /** + * The identifier of the ROOT connection group. + */ + public static final String ROOT_CONNECTION_GROUP_IDENTIFIER = "ROOT"; + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java new file mode 100644 index 000000000..f08c2e402 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +/** + * A simple object to represent an error to be sent from the REST API. + * @author James Muehlner + */ +public class APIError { + + /** + * The error message. + */ + private final String message; + + /** + * Get the error message. + * @return The error message. + */ + public String getMessage() { + return message; + } + + /** + * Create a new APIError with the specified error message. + * @param message The error message. + */ + public APIError(String message) { + this.message = message; + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIPatch.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIPatch.java new file mode 100644 index 000000000..c5c2dec8e --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIPatch.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +/** + * An object for representing the body of a HTTP PATCH method. + * See https://tools.ietf.org/html/rfc6902 + * + * @author James Muehlner + * @param The type of object being patched. + */ +public class APIPatch { + + /** + * The possible operations for a PATCH request. + */ + public enum Operation { + add, remove, test, copy, replace, move + } + + /** + * The operation to perform for this patch. + */ + private Operation op; + + /** + * The value for this patch. + */ + private T value; + + /** + * The path for this patch. + */ + private String path; + + /** + * Returns the operation for this patch. + * @return the operation for this patch. + */ + public Operation getOp() { + return op; + } + + /** + * Set the operation for this patch. + * @param op The operation for this patch. + */ + public void setOp(Operation op) { + this.op = op; + } + + /** + * Returns the value of this patch. + * @return The value of this patch. + */ + public T getValue() { + return value; + } + + /** + * Sets the value of this patch. + * @param value The value of this patch. + */ + public void setValue(T value) { + this.value = value; + } + + /** + * Returns the path for this patch. + * @return The path for this patch. + */ + public String getPath() { + return path; + } + + /** + * Set the path for this patch. + * @param path The path for this patch. + */ + public void setPath(String path) { + this.path = path; + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java new file mode 100644 index 000000000..38ec68b5d --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +import javax.ws.rs.core.Response; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.glyptodon.guacamole.GuacamoleClientException; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleSecurityException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A method interceptor to wrap some custom exception handling around methods + * that expose AuthenticationProvider functionality through the REST interface. + * Translates various types of GuacamoleExceptions into appropriate HTTP responses. + * + * @author James Muehlner + */ +public class AuthProviderRESTExceptionWrapper implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + + // Get the logger for the intercepted class + Logger logger = LoggerFactory.getLogger(invocation.getMethod().getDeclaringClass()); + + try { + return invocation.proceed(); + } + catch(GuacamoleSecurityException e) { + throw new HTTPException(Response.Status.FORBIDDEN, e.getMessage() != null ? e.getMessage() : "Permission denied."); + } + catch(GuacamoleClientException e) { + throw new HTTPException(Response.Status.BAD_REQUEST, e.getMessage() != null ? e.getMessage() : "Invalid Request."); + } + catch(GuacamoleException e) { + logger.error("Unexpected GuacamoleException caught while executing " + invocation.getMethod().getName() + ".", e); + throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() != null ? e.getMessage() : "Unexpected server error."); + } + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExposure.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExposure.java new file mode 100644 index 000000000..2a450c2c3 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExposure.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks that a method exposes functionality from the Guacamole AuthenticationProvider + * using a REST interface. + * + * @author James Muehlner + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface AuthProviderRESTExposure {} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/HTTPException.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/HTTPException.java new file mode 100644 index 000000000..b4d00d540 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/HTTPException.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +/** + * An exception that will result in the given HTTP Status and message or entity + * being returned from the API layer. + * + * @author James Muehlner + */ +public class HTTPException extends WebApplicationException { + + /** + * Construct a new HTTPException with the given HTTP status and entity. + * + * @param status The HTTP Status to use for the response. + * @param entity The entity to use as the body of the response. + */ + public HTTPException(Status status, Object entity) { + super(Response.status(status).entity(entity).build()); + } + + /** + * Construct a new HTTPException with the given HTTP status and message. The + * message will be wrapped in an APIError container. + * + * @param status The HTTP Status to use for the response. + * @param message The message to build the response entity with. + */ + public HTTPException(Status status, String message) { + super(Response.status(status).entity(new APIError(message)).build()); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/PATCH.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/PATCH.java new file mode 100644 index 000000000..bca1b3389 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/PATCH.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +package org.glyptodon.guacamole.net.basic.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.ws.rs.HttpMethod; + +/** + * An annotation for using the HTTP PATCH method in the REST endpoints. + * + * @author James Muehlner + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@HttpMethod("PATCH") +public @interface PATCH {} \ No newline at end of file diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java new file mode 100644 index 000000000..049f37a7e --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +import com.google.inject.AbstractModule; +import com.google.inject.matcher.Matchers; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.AuthenticationProvider; +import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthTokenGenerator; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.glyptodon.guacamole.net.basic.rest.auth.BasicTokenSessionMap; +import org.glyptodon.guacamole.net.basic.rest.auth.SecureRandomAuthTokenGenerator; +import org.glyptodon.guacamole.net.basic.rest.auth.TokenSessionMap; +import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionService; +import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupService; +import org.glyptodon.guacamole.net.basic.rest.permission.PermissionService; +import org.glyptodon.guacamole.net.basic.rest.protocol.ProtocolRetrievalService; +import org.glyptodon.guacamole.net.basic.rest.user.UserService; +import org.glyptodon.guacamole.properties.GuacamoleProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Guice Module for setting up dependency injection for the + * Guacamole REST API. + * + * @author James Muehlner + */ +public class RESTModule extends AbstractModule { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(RESTModule.class); + + /** + * The AuthenticationProvider to use to authenticate all requests. + */ + private AuthenticationProvider authProvider; + + @Override + protected void configure() { + + // Get auth provider instance + try { + authProvider = GuacamoleProperties.getRequiredProperty(BasicGuacamoleProperties.AUTH_PROVIDER); + } + catch (GuacamoleException e) { + logger.error("Unable to read authentication provider from guacamole.properties: {}", e.getMessage()); + logger.debug("Error reading authentication provider from guacamole.properties.", e); + throw new RuntimeException(e); + } + + bind(AuthenticationProvider.class).toInstance(authProvider); + bind(TokenSessionMap.class).toInstance(new BasicTokenSessionMap()); + bind(ConnectionService.class); + bind(ConnectionGroupService.class); + bind(PermissionService.class); + bind(UserService.class); + bind(AuthenticationService.class); + bind(ProtocolRetrievalService.class); + + bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class); + + bindInterceptor(Matchers.any(), Matchers.annotatedWith(AuthProviderRESTExposure.class), new AuthProviderRESTExceptionWrapper()); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java new file mode 100644 index 000000000..399136fae --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +import com.google.inject.Scopes; +import com.google.inject.servlet.ServletModule; +import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; +import org.codehaus.jackson.jaxrs.JacksonJsonProvider; +import org.glyptodon.guacamole.net.basic.rest.auth.LoginRESTService; +import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService; +import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService; +import org.glyptodon.guacamole.net.basic.rest.permission.PermissionRESTService; +import org.glyptodon.guacamole.net.basic.rest.protocol.ProtocolRESTService; +import org.glyptodon.guacamole.net.basic.rest.user.UserRESTService; + +/** + * A Guice Module to set up the servlet mappings for the Guacamole REST API. + * + * @author James Muehlner + */ +public class RESTServletModule extends ServletModule { + + @Override + protected void configureServlets() { + + // Set up the API endpoints + bind(ConnectionRESTService.class); + bind(ConnectionGroupRESTService.class); + bind(PermissionRESTService.class); + bind(ProtocolRESTService.class); + bind(UserRESTService.class); + bind(LoginRESTService.class); + + // Set up the servlet and JSON mappings + bind(GuiceContainer.class); + bind(JacksonJsonProvider.class).in(Scopes.SINGLETON); + serve("/api/*").with(GuiceContainer.class); + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicLogin.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthToken.java similarity index 52% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicLogin.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthToken.java index a462d5e4a..5d71c9edb 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicLogin.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthToken.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,32 +20,50 @@ * THE SOFTWARE. */ -package org.glyptodon.guacamole.net.basic; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package org.glyptodon.guacamole.net.basic.rest.auth; /** - * Simple dummy AuthenticatingHttpServlet which provides an endpoint for arbitrary - * authentication requests that do not expect a response. - * - * @author Michael Jumper + * A simple object to represent an auth token/userID pair in the API. + * + * @author James Muehlner */ -public class BasicLogin extends RestrictedHttpServlet { +public class APIAuthToken { + + /** + * The auth token. + */ + private final String authToken; + + + /** + * The user ID. + */ + private final String userID; /** - * Logger for this class. + * Get the auth token. + * @return The auth token. */ - private final Logger logger = LoggerFactory.getLogger(BasicLogin.class); - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) { - logger.debug("Login was successful for user \"{}\".", context.self().getUsername()); + public String getAuthToken() { + return authToken; + } + + /** + * Get the user ID. + * @return The user ID. + */ + public String getUserID() { + return userID; + } + + /** + * Create a new APIAuthToken Object with the given auth token. + * + * @param authToken The auth token to create the new APIAuthToken with. + * @param userID The ID of the user owning the given token. + */ + public APIAuthToken(String authToken, String userID) { + this.authToken = authToken; + this.userID = userID; } - } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthTokenGenerator.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthTokenGenerator.java new file mode 100644 index 000000000..1c35d580e --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthTokenGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.auth; + +/** + * Generates an auth token for an authenticated user. + * + * @author James Muehlner + */ +public interface AuthTokenGenerator { + + /** + * Get a new auth token. + * + * @return A new auth token. + */ + public String getToken(); + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java new file mode 100644 index 000000000..46eb082c4 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.auth; + +import com.google.inject.Inject; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleUnauthorizedException; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; + +/** + * A service for performing authentication checks in REST endpoints. + * + * @author James Muehlner + */ +public class AuthenticationService { + + /** + * The map of auth tokens to sessions for the REST endpoints. + */ + @Inject + private TokenSessionMap tokenSessionMap; + + /** + * Finds the Guacamole session for a given auth token, if the auth token + * represents a currently logged in user. Throws an unauthorized error + * otherwise. + * + * @param authToken The auth token to check against the map of logged in users. + * @return The session that corresponds to the provided auth token. + * @throws GuacamoleException If the auth token does not correspond to any + * logged in user. + */ + public GuacamoleSession getGuacamoleSession(String authToken) + throws GuacamoleException { + + // Try to get the session from the map of logged in users. + GuacamoleSession session = tokenSessionMap.get(authToken); + + // Authentication failed. + if (session == null) + throw new GuacamoleUnauthorizedException("Permission Denied."); + + return session; + + } + + /** + * Finds the UserContext for a given auth token, if the auth token represents + * a currently logged in user. Throws an unauthorized error otherwise. + * + * @param authToken The auth token to check against the map of logged in users. + * @return The user context that corresponds to the provided auth token. + * @throws GuacamoleException If the auth token does not correspond to any + * logged in user. + */ + public UserContext getUserContext(String authToken) throws GuacamoleException { + return getGuacamoleSession(authToken).getUserContext(); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java new file mode 100644 index 000000000..7b5a3e608 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.auth; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; +import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; +import org.glyptodon.guacamole.properties.GuacamoleProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A basic, HashMap-based implementation of the TokenSessionMap with support + * for session timeouts. + * + * @author James Muehlner + */ +public class BasicTokenSessionMap implements TokenSessionMap { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(BasicTokenSessionMap.class); + + /** + * The last time a user with a specific auth token accessed the API. + */ + private final Map lastAccessTimeMap = new HashMap(); + + /** + * Keeps track of the authToken to GuacamoleSession mapping. + */ + private final Map sessionMap = new HashMap(); + + /** + * The session timeout configuration for an API session, in milliseconds. + */ + private final long SESSION_TIMEOUT; + + /** + * Create a new BasicTokenGuacamoleSessionMap and initialize the session timeout value. + */ + public BasicTokenSessionMap() { + + // Set up the SESSION_TIMEOUT value, with a one hour default. + long sessionTimeoutValue; + try { + sessionTimeoutValue = GuacamoleProperties.getProperty(BasicGuacamoleProperties.API_SESSION_TIMEOUT, 3600000l); + } + catch (GuacamoleException e) { + logger.error("Unexpected GuacamoleException caught while reading API_SESSION_TIMEOUT property. Defaulting to 1 hour.", e); + sessionTimeoutValue = 3600000l; + } + + SESSION_TIMEOUT = sessionTimeoutValue; + + } + + /** + * Evict an authentication token from the map of logged in users and last + * access times. + * + * @param authToken The authentication token to evict. + */ + private void evict(String authToken) { + sessionMap.remove(authToken); + lastAccessTimeMap.remove(authToken); + } + + /** + * Log that the user represented by this auth token has just used the API. + * + * @param authToken The authentication token to record access time for. + */ + private void logAccessTime(String authToken) { + lastAccessTimeMap.put(authToken, new Date().getTime()); + } + + /** + * Check if a session has timed out. + * @param authToken The auth token for the session. + * @return True if the session has timed out, false otherwise. + */ + private boolean sessionHasTimedOut(String authToken) { + + if (!lastAccessTimeMap.containsKey(authToken)) + return true; + + long lastAccessTime = lastAccessTimeMap.get(authToken); + long currentTime = new Date().getTime(); + + return currentTime - lastAccessTime > SESSION_TIMEOUT; + + } + + @Override + public GuacamoleSession get(String authToken) { + + // If the session has timed out, evict the token and force the user to log in again + if (sessionHasTimedOut(authToken)) { + evict(authToken); + return null; + } + + // Update the last access time and return the GuacamoleSession + logAccessTime(authToken); + return sessionMap.get(authToken); + + } + + @Override + public void put(String authToken, GuacamoleSession session) { + + // Update the last access time, and create the token/GuacamoleSession mapping + logAccessTime(authToken); + sessionMap.put(authToken, session); + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/LoginRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/LoginRESTService.java new file mode 100644 index 000000000..b4a85adf8 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/LoginRESTService.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.auth; + +import com.google.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response.Status; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.AuthenticationProvider; +import org.glyptodon.guacamole.net.auth.Credentials; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; +import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.HTTPException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A service for authenticating to the Guacamole REST API. Given valid + * credentials, the service will return an auth token. Invalid credentials will + * result in a permission error. + * + * @author James Muehlner + */ + +@Path("/login") +@Produces(MediaType.APPLICATION_JSON) +public class LoginRESTService { + + /** + * The authentication provider used to authenticate this user. + */ + @Inject + private AuthenticationProvider authProvider; + + /** + * The map of auth tokens to sessions for the REST endpoints. + */ + @Inject + private TokenSessionMap tokenSessionMap; + + /** + * A generator for creating new auth tokens. + */ + @Inject + private AuthTokenGenerator authTokenGenerator; + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(LoginRESTService.class); + + /** + * Authenticates a user, generates an auth token, associates that auth token + * with the user's UserContext for use by further requests. + * + * @param username The username of the user who is to be authenticated. + * @param password The password of the user who is to be authenticated. + * @param request The HttpServletRequest associated with the login attempt. + * @return The auth token for the newly logged-in user. + * @throws GuacamoleException If an error prevents successful login. + */ + @POST + @AuthProviderRESTExposure + public APIAuthToken login(@QueryParam("username") String username, + @QueryParam("password") String password, + @Context HttpServletRequest request) throws GuacamoleException { + + Credentials credentials = new Credentials(); + credentials.setUsername(username); + credentials.setPassword(password); + credentials.setRequest(request); + credentials.setSession(request.getSession(true)); + + UserContext userContext; + try { + userContext = authProvider.getUserContext(credentials); + } + catch(GuacamoleException e) { + logger.error("Exception caught while authenticating user.", e); + throw new HTTPException(Status.INTERNAL_SERVER_ERROR, + "Unexpected server error."); + } + + // Authentication failed. + if (userContext == null) + throw new HTTPException(Status.UNAUTHORIZED, "Permission Denied."); + + String authToken = authTokenGenerator.getToken(); + + tokenSessionMap.put(authToken, new GuacamoleSession(credentials, userContext)); + + logger.debug("Login was successful for user \"{}\".", userContext.self().getUsername()); + return new APIAuthToken(authToken, username); + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/SecureRandomAuthTokenGenerator.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/SecureRandomAuthTokenGenerator.java new file mode 100644 index 000000000..7fadd1014 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/SecureRandomAuthTokenGenerator.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.auth; + +import java.security.SecureRandom; +import org.apache.commons.codec.binary.Hex; + +/** + * An implementation of the AuthTokenGenerator based around SecureRandom. + * + * @author James Muehlner + */ +public class SecureRandomAuthTokenGenerator implements AuthTokenGenerator { + + /** + * Instance of SecureRandom for generating the auth token. + */ + private final SecureRandom secureRandom = new SecureRandom(); + + @Override + public String getToken() { + byte[] bytes = new byte[32]; + secureRandom.nextBytes(bytes); + + return Hex.encodeHexString(bytes); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java new file mode 100644 index 000000000..9991aab52 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.auth; + +import org.glyptodon.guacamole.net.basic.GuacamoleSession; + +/** + * Represents a mapping of auth token to Guacamole session for the REST + * authentication system. + * + * @author James Muehlner + */ +public interface TokenSessionMap { + + /** + * Registers that a user has just logged in with the specified authToken and + * GuacamoleSession. + * + * @param authToken The authentication token for the logged in user. + * @param session The GuacamoleSession for the logged in user. + */ + public void put(String authToken, GuacamoleSession session); + + /** + * Get the GuacamoleSession for a logged in user. If the auth token does not + * represent a user who is currently logged in, returns null. + * + * @param authToken The authentication token for the logged in user. + * @return The GuacamoleSession for the given auth token, if the auth token + * represents a currently logged in user, null otherwise. + */ + public GuacamoleSession get(String authToken); + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/package-info.java similarity index 86% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/package-info.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/package-info.java index 73acc5540..ece55a39c 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/package-info.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,7 @@ */ /** - * Servlets dedicated to CRUD operations related to Connections. + * Classes related to the authentication aspect of the Guacamole REST API. */ -package org.glyptodon.guacamole.net.basic.crud.connections; +package org.glyptodon.guacamole.net.basic.rest.auth; diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/CaptureClipboard.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/clipboard/ClipboardRESTService.java similarity index 64% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/CaptureClipboard.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/clipboard/ClipboardRESTService.java index 4b1fc1cd4..972895cc4 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/CaptureClipboard.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/clipboard/ClipboardRESTService.java @@ -20,26 +20,36 @@ * THE SOFTWARE. */ -package org.glyptodon.guacamole.net.basic; +package org.glyptodon.guacamole.net.basic.rest.clipboard; -import java.io.IOException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; +import com.google.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; -import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.ClipboardState; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; +import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty; import org.glyptodon.guacamole.properties.GuacamoleProperties; /** - * Servlet which dumps the current contents of the clipboard. + * A REST service for reading the current contents of the clipboard. * * @author Michael Jumper */ -public class CaptureClipboard extends RestrictedHttpServlet { +@Path("/clipboard") +public class ClipboardRESTService { + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; + /** * The amount of time to wait for clipboard changes, in milliseconds. */ @@ -55,30 +65,23 @@ public class CaptureClipboard extends RestrictedHttpServlet { }; - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) + @GET + @AuthProviderRESTExposure + public Response getClipboard(@QueryParam("token") String authToken) throws GuacamoleException { // Only bother if actually enabled if (GuacamoleProperties.getProperty(INTEGRATION_ENABLED, false)) { // Get clipboard - final HttpSession session = request.getSession(true); - final ClipboardState clipboard = AuthenticatingFilter.getClipboardState(session); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + final ClipboardState clipboard = session.getClipboardState(); // Send clipboard contents - try { - synchronized (clipboard) { - clipboard.waitForContents(CLIPBOARD_TIMEOUT); - response.setContentType(clipboard.getMimetype()); - response.getOutputStream().write(clipboard.getContents()); - } - } - catch (IOException e) { - throw new GuacamoleServerException("Unable to send clipboard contents", e); + synchronized (clipboard) { + clipboard.waitForContents(CLIPBOARD_TIMEOUT); + return Response.ok(clipboard.getContents(), + clipboard.getMimetype()).build(); } } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.java new file mode 100644 index 000000000..dab0c3884 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.connection; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.Connection; +import org.glyptodon.guacamole.net.auth.ConnectionRecord; +import org.glyptodon.guacamole.net.basic.rest.APIConstants; +import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; + +/** + * A simple connection to expose through the REST endpoints. + * + * @author James Muehlner + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class APIConnection { + + /** + * The name of this connection. + */ + private String name; + + /** + * The identifier of this connection. + */ + private String identifier; + + /** + * The identifier of the parent connection group for this connection. + */ + private String parentIdentifier; + + /** + * The protocol of this connection. + */ + private String protocol; + + /** + * The history records associated with this connection. + */ + private List history; + + /** + * Map of all associated parameter values, indexed by parameter name. + */ + private Map parameters = new HashMap(); + + /** + * Create an empty APIConnection. + */ + public APIConnection() {} + + /** + * Create an APIConnection from a Connection record. + * @param connection The connection to create this APIConnection from. + * @throws GuacamoleException If a problem is encountered while + * instantiating this new APIConnection. + */ + public APIConnection(Connection connection) + throws GuacamoleException { + + this.name = connection.getName(); + this.identifier = connection.getIdentifier(); + this.parentIdentifier = connection.getParentIdentifier(); + this.history = connection.getHistory(); + + // Use the explicit ROOT group ID + if (this.parentIdentifier == null) + this.parentIdentifier = APIConstants.ROOT_CONNECTION_GROUP_IDENTIFIER; + + GuacamoleConfiguration configuration = connection.getConfiguration(); + + this.protocol = configuration.getProtocol(); + + for (String key: configuration.getParameterNames()) + this.parameters.put(key, configuration.getParameter(key)); + + } + + /** + * Returns the name of this connection. + * @return The name of this connection. + */ + public String getName() { + return name; + } + + /** + * Set the name of this connection. + * @param name The name of this connection. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the unique identifier for this connection. + * @return The unique identifier for this connection. + */ + public String getIdentifier() { + return identifier; + } + + /** + * Sets the unique identifier for this connection. + * @param identifier The unique identifier for this connection. + */ + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + /** + * Returns the unique identifier for this connection. + * @return The unique identifier for this connection. + */ + public String getParentIdentifier() { + return parentIdentifier; + } + + /** + * Sets the parent connection group identifier for this connection. + * @param parentIdentifier The parent connection group identifier + * for this connection. + */ + public void setParentIdentifier(String parentIdentifier) { + this.parentIdentifier = parentIdentifier; + } + + /** + * Returns the history records associated with this connection. + * @return The history records associated with this connection. + */ + public List getHistory() { + return history; + } + + /** + * Returns the parameter map for this connection. + * @return The parameter map for this connection. + */ + public Map getParameters() { + return parameters; + } + + /** + * Sets the parameter map for this connection. + * @param parameters The parameter map for this connection. + */ + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + /** + * Returns the protocol for this connection. + * @return The protocol for this connection. + */ + public String getProtocol() { + return protocol; + } + + /** + * Sets the protocol for this connection. + * @param protocol protocol for this connection. + */ + public void setProtocol(String protocol) { + this.protocol = protocol; + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.java new file mode 100644 index 000000000..aca765c40 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.connection; + +import java.util.List; +import java.util.Map; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.GuacamoleSocket; +import org.glyptodon.guacamole.net.auth.Connection; +import org.glyptodon.guacamole.net.auth.ConnectionRecord; +import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; +import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; + +/** + * A wrapper to make an APIConnection look like a Connection. Useful where a + * org.glyptodon.guacamole.net.auth.Connection is required. + * + * @author James Muehlner + */ +public class APIConnectionWrapper implements Connection { + + private final APIConnection apiConnection; + + public APIConnectionWrapper(APIConnection apiConnection) { + this.apiConnection = apiConnection; + } + + @Override + public String getName() { + return apiConnection.getName(); + } + + @Override + public void setName(String name) { + apiConnection.setName(name); + } + + @Override + public String getIdentifier() { + return apiConnection.getIdentifier(); + } + + @Override + public void setIdentifier(String identifier) { + apiConnection.setIdentifier(identifier); + } + + @Override + public String getParentIdentifier() { + return apiConnection.getParentIdentifier(); + } + + @Override + public void setParentIdentifier(String parentIdentifier) { + apiConnection.setParentIdentifier(parentIdentifier); + } + + @Override + public GuacamoleConfiguration getConfiguration() { + + // Create the GuacamoleConfiguration from the parameter map + GuacamoleConfiguration configuration = new GuacamoleConfiguration(); + + Map parameters = apiConnection.getParameters(); + + for(Map.Entry entry : parameters.entrySet()) + configuration.setParameter(entry.getKey(), entry.getValue()); + + configuration.setProtocol(apiConnection.getProtocol()); + + return configuration; + } + + @Override + public void setConfiguration(GuacamoleConfiguration config) { + + // Create a parameter map from the GuacamoleConfiguration + Map parameters = apiConnection.getParameters(); + for(String key : config.getParameterNames()) + parameters.put(key, config.getParameter(key)); + + // Set the protocol + apiConnection.setProtocol(config.getProtocol()); + } + + @Override + public GuacamoleSocket connect(GuacamoleClientInformation info) throws GuacamoleException { + throw new UnsupportedOperationException("Operation not supported."); + } + + @Override + public List getHistory() throws GuacamoleException { + return apiConnection.getHistory(); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java new file mode 100644 index 000000000..103b56643 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.connection; + +import com.google.inject.Inject; +import java.util.List; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response.Status; +import org.glyptodon.guacamole.GuacamoleClientException; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.Connection; +import org.glyptodon.guacamole.net.auth.ConnectionGroup; +import org.glyptodon.guacamole.net.auth.Directory; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.HTTPException; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A REST Service for handling connection CRUD operations. + * + * @author James Muehlner + */ +@Path("/connection") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ConnectionRESTService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(ConnectionRESTService.class); + + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; + + /** + * A service for managing the REST endpoint APIConnection objects. + */ + @Inject + private ConnectionService connectionService; + + /** + * Gets a list of connections with the given ConnectionGroup parentID. + * If no parentID is provided, returns the connections from the root group. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param parentID The ID of the ConnectionGroup the connections + * belong to. If null, the root connection group will be used. + * @return The connection list. + * @throws GuacamoleException If a problem is encountered while listing connections. + */ + @GET + @AuthProviderRESTExposure + public List getConnections(@QueryParam("token") String authToken, @QueryParam("parentID") String parentID) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // If the parent connection group is passed in, try to find it. + ConnectionGroup parentConnectionGroup; + if (parentID == null) + parentConnectionGroup = userContext.getRootConnectionGroup(); + + else { + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); + parentConnectionGroup = connectionGroupDirectory.get(parentID); + } + + if (parentConnectionGroup == null) + throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); + + Directory connectionDirectory = + parentConnectionGroup.getConnectionDirectory(); + + // Return the converted connection directory + return connectionService.convertConnectionList(connectionDirectory); + + } + + /** + * Gets an individual connection. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param connectionID The ID of the Connection.. + * @return The connection. + * @throws GuacamoleException If a problem is encountered while retrieving the connection. + */ + @GET + @Path("/{connectionID}") + @AuthProviderRESTExposure + public APIConnection getConnection(@QueryParam("token") String authToken, + @PathParam("connectionID") String connectionID) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the connection directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionDirectory = + rootGroup.getConnectionDirectory(); + + // Get the connection + Connection connection = connectionDirectory.get(connectionID); + if (connection == null) + throw new HTTPException(Status.NOT_FOUND, "No Connection found with the provided ID."); + + return new APIConnection(connection); + + } + + /** + * Deletes an individual connection. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param connectionID The ID of the Connection to delete. + * @throws GuacamoleException If a problem is encountered while deleting the connection. + */ + @DELETE + @Path("/{connectionID}") + @AuthProviderRESTExposure + public void deleteConnection(@QueryParam("token") String authToken, @PathParam("connectionID") String connectionID) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the connection directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionDirectory = + rootGroup.getConnectionDirectory(); + + // Make sure the connection is there before trying to delete + if (connectionDirectory.get(connectionID) == null) + throw new HTTPException(Status.NOT_FOUND, "No Connection found with the provided ID."); + + // Delete the connection + connectionDirectory.remove(connectionID); + + } + + /** + * Creates a new connection and returns the identifier of the new connection. + * If a parentID is provided, the connection will be created in the + * connection group with the parentID. Otherwise, the root connection group + * will be used. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param parentID The ID of the ConnectionGroup the connections + * belong to. If null, the root connection group will be used. + * @param connection The connection to create. + * @return The identifier of the new connection. + * @throws GuacamoleException If a problem is encountered while creating the connection. + */ + @POST + @AuthProviderRESTExposure + public String createConnection(@QueryParam("token") String authToken, + @QueryParam("parentID") String parentID, APIConnection connection) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + if (connection == null) + throw new GuacamoleClientException("A connection is required for this request."); + + // If the parent connection group is passed in, try to find it. + ConnectionGroup parentConnectionGroup; + if (parentID == null) + parentConnectionGroup = userContext.getRootConnectionGroup(); + + else { + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); + parentConnectionGroup = connectionGroupDirectory.get(parentID); + } + + if (parentConnectionGroup == null) + throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); + + Directory connectionDirectory = + parentConnectionGroup.getConnectionDirectory(); + + // Create the connection + connectionDirectory.add(new APIConnectionWrapper(connection)); + + // Return the new connection identifier + return connection.getIdentifier(); + + } + + /** + * Updates a connection. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param connectionID The ID of the Connection to move. + * @param connection The connection to update. + * @throws GuacamoleException If a problem is encountered while updating the connection. + */ + @POST + @Path("/{connectionID}") + @AuthProviderRESTExposure + public void updateConnection(@QueryParam("token") String authToken, + @PathParam("connectionID") String connectionID, APIConnection connection) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + if (connection == null) + throw new GuacamoleClientException("A connection is required for this request."); + + // Get the connection directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionDirectory = + rootGroup.getConnectionDirectory(); + + Connection connectionFromAuthProvider = connectionDirectory.get(connectionID); + + // Make sure the connection is there before trying to update + if (connectionFromAuthProvider == null) + throw new HTTPException(Status.NOT_FOUND, "No Connection found with the provided ID."); + + // Copy the information from this connection over to an object from the Auth Provider + APIConnectionWrapper wrappedConnection = new APIConnectionWrapper(connection); + connectionFromAuthProvider.setConfiguration(wrappedConnection.getConfiguration()); + connectionFromAuthProvider.setName(wrappedConnection.getName()); + + // Update the connection + connectionDirectory.update(connectionFromAuthProvider); + + } + + /** + * Moves an individual connection to a different connection group. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param connectionID The ID of the Connection to move. + * @param parentID The ID of the ConnectionGroup the connection is to be moved to. + * @throws GuacamoleException If a problem is encountered while moving the connection. + */ + @PUT + @Path("/{connectionID}") + @AuthProviderRESTExposure + public void moveConnection(@QueryParam("token") String authToken, + @PathParam("connectionID") String connectionID, @QueryParam("parentID") String parentID) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the connection directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionDirectory = + rootGroup.getConnectionDirectory(); + + // Find the new parent connection group + Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); + ConnectionGroup parentConnectionGroup = connectionGroupDirectory.get(parentID); + + if (parentConnectionGroup == null) + throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); + + // Move the connection + connectionDirectory.move(connectionID, parentConnectionGroup.getConnectionDirectory()); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Delete.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionService.java similarity index 51% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Delete.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionService.java index 3c2957ce3..44449818d 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connections/Delete.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,40 +20,40 @@ * THE SOFTWARE. */ -package org.glyptodon.guacamole.net.basic.crud.connections; +package org.glyptodon.guacamole.net.basic.rest.connection; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.Directory; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; /** - * Simple HttpServlet which handles connection deletion. - * - * @author Michael Jumper + * A service for performing useful manipulations on REST Connections. + * + * @author James Muehlner */ -public class Delete extends RestrictedHttpServlet { +public class ConnectionService { + + /** + * Converts a Connection Directory to a list of APIConnection objects for + * exposing with the REST endpoints. + * + * @param connectionDirectory The Connection Directory to convert for REST endpoint use. + * @return A List of APIConnection objects for use with the REST endpoint. + * @throws GuacamoleException If an error occurs while converting the + * connection directory. + */ + public List convertConnectionList(Directory connectionDirectory) + throws GuacamoleException { - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get ID - String identifier = request.getParameter("id"); - - // Attempt to get connection directory - Directory directory = - context.getRootConnectionGroup().getConnectionDirectory(); - - // Remove connection - directory.remove(identifier); + List restConnections = new ArrayList(); + + for (String connectionID : connectionDirectory.getIdentifiers()) + restConnections.add(new APIConnection(connectionDirectory.get(connectionID))); + + return restConnections; } } - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/package-info.java similarity index 85% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/package-info.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/package-info.java index af137a42b..3d55216fd 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/connectiongroups/package-info.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,7 @@ */ /** - * Servlets dedicated to CRUD operations related to ConnectionGroups. + * Classes related to the connection manipulation aspect of the Guacamole REST API. */ -package org.glyptodon.guacamole.net.basic.crud.connectiongroups; +package org.glyptodon.guacamole.net.basic.rest.connection; diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java new file mode 100644 index 000000000..4ac155d39 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.connectiongroup; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.glyptodon.guacamole.net.auth.ConnectionGroup; +import org.glyptodon.guacamole.net.auth.ConnectionGroup.Type; +import org.glyptodon.guacamole.net.basic.rest.APIConstants; + +/** + * A simple connection group to expose through the REST endpoints. + * + * @author James Muehlner + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class APIConnectionGroup { + + /** + * The name of this connection group. + */ + private String name; + + /** + * The identifier of this connection group. + */ + private String identifier; + + /** + * The identifier of the parent connection group for this connection group. + */ + private String parentIdentifier; + + /** + * The type of this connection group. + */ + private Type type; + + /** + * Create an empty APIConnectionGroup. + */ + public APIConnectionGroup() {} + + /** + * Create a new APIConnectionGroup from the given ConnectionGroup record. + * + * @param connectionGroup The ConnectionGroup record to initialize this + * APIConnectionGroup from. + */ + public APIConnectionGroup(ConnectionGroup connectionGroup) { + + this.identifier = connectionGroup.getIdentifier(); + this.parentIdentifier = connectionGroup.getParentIdentifier(); + + // Use the explicit ROOT group ID + if (this.parentIdentifier == null) + this.parentIdentifier = APIConstants.ROOT_CONNECTION_GROUP_IDENTIFIER; + + this.name = connectionGroup.getName(); + this.type = connectionGroup.getType(); + + } + + /** + * Returns the name of this connection group. + * @return The name of this connection group. + */ + public String getName() { + return name; + } + + /** + * Set the name of this connection group. + * @param name The name of this connection group. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the identifier of this connection group. + * @return The identifier of this connection group. + */ + public String getIdentifier() { + return identifier; + } + + /** + * Set the identifier of this connection group. + * @param identifier The identifier of this connection group. + */ + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + /** + * Returns the unique identifier for this connection group. + * @return The unique identifier for this connection group. + */ + public String getParentIdentifier() { + return parentIdentifier; + } + /** + * Sets the parent connection group identifier for this connection group. + * @param parentIdentifier The parent connection group identifier + * for this connection group. + */ + public void setParentIdentifier(String parentIdentifier) { + this.parentIdentifier = parentIdentifier; + } + + /** + * Returns the type of this connection group. + * @return The type of this connection group. + */ + public Type getType() { + return type; + } + + /** + * Set the type of this connection group. + * @param type The Type of this connection group. + */ + public void setType(Type type) { + this.type = type; + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroupWrapper.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroupWrapper.java new file mode 100644 index 000000000..c38b22977 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroupWrapper.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.connectiongroup; + +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.GuacamoleSocket; +import org.glyptodon.guacamole.net.auth.Connection; +import org.glyptodon.guacamole.net.auth.ConnectionGroup; +import org.glyptodon.guacamole.net.auth.Directory; +import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; + +/** + * A wrapper to make an APIConnection look like a ConnectionGroup. + * Useful where a org.glyptodon.guacamole.net.auth.ConnectionGroup is required. + * + * @author James Muehlner + */ +public class APIConnectionGroupWrapper implements ConnectionGroup { + + /** + * The wrapped APIConnectionGroup. + */ + private final APIConnectionGroup apiConnectionGroup; + + /** + * Create a new APIConnectionGroupWrapper to wrap the given + * APIConnectionGroup as a ConnectionGroup. + * @param apiConnectionGroup the APIConnectionGroup to wrap. + */ + public APIConnectionGroupWrapper(APIConnectionGroup apiConnectionGroup) { + this.apiConnectionGroup = apiConnectionGroup; + } + + @Override + public String getName() { + return apiConnectionGroup.getName(); + } + + @Override + public void setName(String name) { + apiConnectionGroup.setName(name); + } + + @Override + public String getIdentifier() { + return apiConnectionGroup.getIdentifier(); + } + + @Override + public void setIdentifier(String identifier) { + apiConnectionGroup.setIdentifier(identifier); + } + + @Override + public String getParentIdentifier() { + return apiConnectionGroup.getParentIdentifier(); + } + + @Override + public void setParentIdentifier(String parentIdentifier) { + apiConnectionGroup.setParentIdentifier(parentIdentifier); + } + + @Override + public void setType(Type type) { + apiConnectionGroup.setType(type); + } + + @Override + public Type getType() { + return apiConnectionGroup.getType(); + } + + @Override + public Directory getConnectionDirectory() throws GuacamoleException { + throw new UnsupportedOperationException("Operation not supported."); + } + + @Override + public Directory getConnectionGroupDirectory() throws GuacamoleException { + throw new UnsupportedOperationException("Operation not supported."); + } + + @Override + public GuacamoleSocket connect(GuacamoleClientInformation info) throws GuacamoleException { + throw new UnsupportedOperationException("Operation not supported."); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java new file mode 100644 index 000000000..aa04a5f60 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.connectiongroup; + +import com.google.inject.Inject; +import java.util.List; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response.Status; +import org.glyptodon.guacamole.GuacamoleClientException; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.ConnectionGroup; +import org.glyptodon.guacamole.net.auth.Directory; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.HTTPException; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A REST Service for handling connection group CRUD operations. + * + * @author James Muehlner + */ +@Path("/connectionGroup") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ConnectionGroupRESTService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(ConnectionGroupRESTService.class); + + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; + + /** + * A service for managing the REST endpoint APIConnection objects. + */ + @Inject + private ConnectionGroupService connectionGroupService; + + /** + * The ID that will be guaranteed to refer to the root connection group. + */ + private static final String ROOT_CONNECTION_GROUP_ID = "ROOT"; + + /** + * Gets a list of connection groups with the given ConnectionGroup parentID. + * If no parentID is provided, returns the connection groups from the root group. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param parentID The ID of the ConnectionGroup the connection groups + * belong to. If null, the root connection group will be used. + * @return The connection list. + * @throws GuacamoleException If a problem is encountered while listing connection groups. + */ + @GET + @AuthProviderRESTExposure + public List getConnectionGroups(@QueryParam("token") String authToken, @QueryParam("parentID") String parentID) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // If the parent connection group is passed in, try to find it. + ConnectionGroup parentConnectionGroup; + if (parentID == null) + parentConnectionGroup = userContext.getRootConnectionGroup(); + + else { + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); + parentConnectionGroup = connectionGroupDirectory.get(parentID); + } + + if (parentConnectionGroup == null) + throw new HTTPException(Status.NOT_FOUND, "No connection group found with the provided parentID."); + + Directory connectionGroupDirectory = + parentConnectionGroup.getConnectionGroupDirectory(); + + // Return the converted connection group list + return connectionGroupService.convertConnectionGroupList(connectionGroupDirectory); + } + + /** + * Gets an individual connection group. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param connectionGroupID The ID of the ConnectionGroup. + * @return The connection group. + * @throws GuacamoleException If a problem is encountered while retrieving the connection group. + */ + @GET + @Path("/{connectionGroupID}") + @AuthProviderRESTExposure + public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken, + @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the connection group directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + + // Return the root group if it was asked for + if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + return new APIConnectionGroup(rootGroup); + + Directory connectionGroupDirectory = + rootGroup.getConnectionGroupDirectory(); + + // Get the connection group + ConnectionGroup connectionGroup = connectionGroupDirectory.get(connectionGroupID); + if (connectionGroup == null) + throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided ID."); + + // Return the connectiion group + return new APIConnectionGroup(connectionGroup); + + } + + /** + * Deletes an individual connection group. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param connectionGroupID The ID of the ConnectionGroup to delete. + * @throws GuacamoleException If a problem is encountered while deleting the connection group. + */ + @DELETE + @Path("/{connectionGroupID}") + @AuthProviderRESTExposure + public void deleteConnectionGroup(@QueryParam("token") String authToken, + @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the connection group directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + + // Use the root group if it was asked for + if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + connectionGroupID = rootGroup.getIdentifier(); + + Directory connectionGroupDirectory = + rootGroup.getConnectionGroupDirectory(); + + // Make sure the connection is there before trying to delete + if (connectionGroupDirectory.get(connectionGroupID) == null) + throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided ID."); + + // Delete the connection group + connectionGroupDirectory.remove(connectionGroupID); + + } + + /** + * Creates a new connection group and returns the identifier of the new connection group. + * If a parentID is provided, the connection group will be created in the + * connection group with the parentID. Otherwise, the root connection group + * will be used. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param parentID The ID of the ConnectionGroup the connection groups + * belong to. If null, the root connection group will be used. + * @param connectionGroup The connection group to create. + * @return The identifier of the new connection group. + * @throws GuacamoleException If a problem is encountered while creating the connection group. + */ + @POST + @AuthProviderRESTExposure + public String createConnectionGroup(@QueryParam("token") String authToken, + @QueryParam("parentID") String parentID, APIConnectionGroup connectionGroup) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + if (connectionGroup == null) + throw new GuacamoleClientException("A connection group is required for this request."); + + // If the parent connection group is passed in, try to find it. + ConnectionGroup parentConnectionGroup; + if (parentID == null) + parentConnectionGroup = userContext.getRootConnectionGroup(); + + else { + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); + parentConnectionGroup = connectionGroupDirectory.get(parentID); + } + + if (parentConnectionGroup == null) + throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); + + Directory connectionGroupDirectory = + parentConnectionGroup.getConnectionGroupDirectory(); + + // Create the connection group + connectionGroupDirectory.add(new APIConnectionGroupWrapper(connectionGroup)); + + // Return the new connection group identifier + return connectionGroup.getIdentifier(); + + } + + /** + * Updates a connection group. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param connectionGroupID The ID of the ConnectionGroup to update. + * @param connectionGroup The connection group to update. + * @throws GuacamoleException If a problem is encountered while updating the connection group. + */ + @POST + @Path("/{connectionGroupID}") + @AuthProviderRESTExposure + public void updateConnectionGroup(@QueryParam("token") String authToken, + @PathParam("connectionGroupID") String connectionGroupID, APIConnectionGroup connectionGroup) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + if (connectionGroup == null) + throw new GuacamoleClientException("A connection group is required for this request."); + + // Get the connection directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + + // Use the root group if it was asked for + if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + connectionGroupID = rootGroup.getIdentifier(); + + Directory connectionGroupDirectory = + rootGroup.getConnectionGroupDirectory(); + + // Make sure the connection group is there before trying to update + if (connectionGroupDirectory.get(connectionGroupID) == null) + throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided ID."); + + // Update the connection group + connectionGroupDirectory.update(new APIConnectionGroupWrapper(connectionGroup)); + + } + + /** + * Moves an individual connection group to a different connection group. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param connectionGroupID The ID of the ConnectionGroup to move. + * @param parentID The ID of the ConnectionGroup the connection group is to be moved to. + * @throws GuacamoleException If a problem is encountered while moving the connection group. + */ + @PUT + @Path("/{connectionGroupID}") + @AuthProviderRESTExposure + public void moveConnectionGroup(@QueryParam("token") String authToken, + @PathParam("connectionGroupID") String connectionGroupID, + @QueryParam("parentID") String parentID) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the connection group directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + + // Use the root group if it was asked for + if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + connectionGroupID = rootGroup.getIdentifier(); + + Directory connectionGroupDirectory = + rootGroup.getConnectionGroupDirectory(); + + // Find the new parent connection group + Directory newConnectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); + ConnectionGroup parentConnectionGroup = newConnectionGroupDirectory.get(parentID); + + if (parentConnectionGroup == null) + throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); + + // Move the connection group + connectionGroupDirectory.move(connectionGroupID, parentConnectionGroup.getConnectionGroupDirectory()); + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java new file mode 100644 index 000000000..ecbe5fffd --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.connectiongroup; + +import java.util.ArrayList; +import java.util.List; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.ConnectionGroup; +import org.glyptodon.guacamole.net.auth.Directory; + +/** + * A service for performing useful manipulations on REST ConnectionGroups. + * + * @author James Muehlner + */ +public class ConnectionGroupService { + + /** + * Converts a ConnectionGroup directory to a list of APIConnectionGroup + * objects for exposing with the REST endpoints. + * + * @param connectionGroupDirectory The ConnectionGroup Directory to convert for REST endpoint use. + * @return A List of APIConnectionGroup objects for use with the REST endpoint. + * @throws GuacamoleException If an error occurs while converting the + * connection group directory. + */ + public List convertConnectionGroupList( + Directory connectionGroupDirectory) throws GuacamoleException { + + List restConnectionGroups = new ArrayList(); + + for (String connectionGroupID : connectionGroupDirectory.getIdentifiers()) + restConnectionGroups.add(new APIConnectionGroup(connectionGroupDirectory.get(connectionGroupID))); + + return restConnectionGroups; + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/package-info.java new file mode 100644 index 000000000..dc404cbc1 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Classes related to the connection group manipulation aspect + * of the Guacamole REST API. + */ +package org.glyptodon.guacamole.net.basic.rest.connectiongroup; + diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/package-info.java similarity index 87% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/package-info.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/package-info.java index bad264863..31a00e57c 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/package-info.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,7 @@ */ /** - * Servlets dedicated to CRUD operations related to Users. + * Classes related to the basic Guacamole REST API. */ -package org.glyptodon.guacamole.net.basic.crud.users; +package org.glyptodon.guacamole.net.basic.rest; diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermission.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermission.java new file mode 100644 index 000000000..ee97de0fd --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermission.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.permission; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.glyptodon.guacamole.net.auth.permission.ConnectionGroupPermission; +import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; +import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; +import org.glyptodon.guacamole.net.auth.permission.Permission; +import org.glyptodon.guacamole.net.auth.permission.SystemPermission; +import org.glyptodon.guacamole.net.auth.permission.UserPermission; + +/** + * A simple user permission to expose through the REST endpoints. + * + * @author James Muehlner + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) +public class APIPermission { + + /** + * Create an empty APIPermission. + */ + public APIPermission() {} + + /** + * The type of object that this permission refers to. + */ + private ObjectType objectType; + + /** + * The type of object that a permission can refer to. + */ + public enum ObjectType { + + /** + * A normal connection. + */ + CONNECTION, + + /** + * A connection group. + */ + CONNECTION_GROUP, + + /** + * A Guacamole user. + */ + USER, + + /** + * The Guacamole system itself. + */ + SYSTEM + + } + + /** + * The identifier of the object that this permission refers to. + */ + private String objectIdentifier; + + /** + * The object permission type for this APIPermission, if relevant. This is + * only used if this.objectType is CONNECTION, CONNECTION_GROUP, or USER. + */ + private ObjectPermission.Type objectPermissionType; + + /** + * The system permission type for this APIPermission, if relevant. This is + * only used if this.objectType is SYSTEM. + */ + private SystemPermission.Type systemPermissionType; + + /** + * Create an APIConnection from a Connection record. + * + * @param permission The permission to create this APIPermission from. + */ + public APIPermission(Permission permission) { + + // Connection permission + if (permission instanceof ConnectionPermission) { + this.objectType = ObjectType.CONNECTION; + this.objectPermissionType = ((ConnectionPermission) permission).getType(); + this.objectIdentifier = ((ConnectionPermission) permission).getObjectIdentifier(); + } + + // Connection group permission + else if (permission instanceof ConnectionGroupPermission) { + this.objectType = ObjectType.CONNECTION_GROUP; + this.objectPermissionType = ((ConnectionGroupPermission) permission).getType(); + this.objectIdentifier = ((ConnectionGroupPermission) permission).getObjectIdentifier(); + } + + // User permission + else if (permission instanceof UserPermission) { + this.objectType = ObjectType.USER; + this.objectPermissionType = ((UserPermission) permission).getType(); + this.objectIdentifier = ((UserPermission) permission).getObjectIdentifier(); + } + + // System permission + else if (permission instanceof SystemPermission) { + this.objectType = ObjectType.SYSTEM; + this.systemPermissionType = ((SystemPermission) permission).getType(); + } + + } + + /** + * Returns the type of object that this permission refers to. + * + * @return The type of object that this permission refers to. + */ + public ObjectType getObjectType() { + return objectType; + } + + /** + * Set the type of object that this permission refers to. + * @param objectType The type of object that this permission refers to. + */ + public void setObjectType(ObjectType objectType) { + this.objectType = objectType; + } + + /** + * Returns a string representation of the permission type. + * + * @return A string representation of the permission type. + */ + public String getPermissionType() { + switch(this.objectType) { + case CONNECTION: + case CONNECTION_GROUP: + case USER: + return this.objectPermissionType.toString(); + case SYSTEM: + return this.systemPermissionType.toString(); + default: + return null; + } + } + + /** + * Set the permission type from a string representation of that type. + * Since it's not clear at this point whether this is an object permission or + * system permission, try to set both of them. + * + * @param permissionType The string representation of the permission type. + */ + public void setPermissionType(String permissionType) { + try { + this.objectPermissionType = ObjectPermission.Type.valueOf(permissionType); + } catch(IllegalArgumentException e) {} + + try { + this.systemPermissionType = SystemPermission.Type.valueOf(permissionType); + } catch(IllegalArgumentException e) {} + } + + /** + * Returns the identifier of the object that this permission refers to. + * + * @return The identifier of the object that this permission refers to. + */ + public String getObjectIdentifier() { + return objectIdentifier; + } + + /** + * Set the identifier of the object that this permission refers to. + * + * @param objectIdentifier The identifier of the object that this permission refers to. + */ + public void setObjectIdentifier(String objectIdentifier) { + this.objectIdentifier = objectIdentifier; + } + + /** + * Returns an org.glyptodon.guacamole.net.auth.permission.Permission + * representation of this APIPermission. + * + * @return An org.glyptodon.guacamole.net.auth.permission.Permission + * representation of this APIPermission. + */ + public Permission toPermission() { + switch(this.objectType) { + case CONNECTION: + return new ConnectionPermission + (this.objectPermissionType, this.objectIdentifier); + case CONNECTION_GROUP: + return new ConnectionGroupPermission + (this.objectPermissionType, this.objectIdentifier); + case USER: + return new UserPermission + (this.objectPermissionType, this.objectIdentifier); + case SYSTEM: + return new SystemPermission(this.systemPermissionType); + default: + return null; + } + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java new file mode 100644 index 000000000..08e49812b --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.permission; + +import com.google.inject.Inject; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response.Status; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.Directory; +import org.glyptodon.guacamole.net.auth.User; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.auth.permission.Permission; +import org.glyptodon.guacamole.net.basic.rest.APIPatch; +import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.HTTPException; +import org.glyptodon.guacamole.net.basic.rest.PATCH; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A REST Service for handling connection CRUD operations. + * + * @author James Muehlner + */ +@Path("/permission") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class PermissionRESTService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(PermissionRESTService.class); + + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; + + /** + * A service for managing the REST endpoint APIPermission objects. + */ + @Inject + private PermissionService permissionService; + + /** + * Gets a list of permissions for the user with the given userID. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param userID The ID of the user to retrieve permissions for. + * @return The permission list. + * @throws GuacamoleException If a problem is encountered while listing permissions. + */ + @GET + @Path("/{userID}") + @AuthProviderRESTExposure + public List getPermissions(@QueryParam("token") String authToken, @PathParam("userID") String userID) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the user + User user = userContext.getUserDirectory().get(userID); + if (user == null) + throw new HTTPException(Status.NOT_FOUND, "User not found with the provided userID."); + + return permissionService.convertPermissionList(user.getPermissions()); + + } + + /** + * Adds a permissions for a user with the given userID. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param userID The user ID to add the permission for. + * @param permission The permission to add for the user with the given userID. + * @throws GuacamoleException If a problem is encountered while adding the permission. + */ + @POST + @Path("/{userID}") + @AuthProviderRESTExposure + public void addPermission(@QueryParam("token") String authToken, + @PathParam("userID") String userID, APIPermission permission) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the user + User user = userContext.getUserDirectory().get(userID); + if (user == null) + throw new HTTPException(Status.NOT_FOUND, "User not found with the provided userID."); + + // Add the new permission + user.addPermission(permission.toPermission()); + userContext.getUserDirectory().update(user); + + } + + /** + * Removes a permissions for a user with the given userID. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param userID The user ID to remove the permission for. + * @param permission The permission to remove for the user with the given userID. + * @throws GuacamoleException If a problem is encountered while removing the permission. + */ + @POST + @Path("/remove/{userID}/") + @AuthProviderRESTExposure + public void removePermission(@QueryParam("token") String authToken, + @PathParam("userID") String userID, APIPermission permission) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the user + User user = userContext.getUserDirectory().get(userID); + if (user == null) + throw new HTTPException(Status.NOT_FOUND, "User not found with the provided userID."); + + // Remove the permission + user.removePermission(permission.toPermission()); + userContext.getUserDirectory().update(user); + + } + + + /** + * Applies a given list of permission patches. + * + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param patches The permission patches to apply for this request. + * @throws GuacamoleException If a problem is encountered while removing the permission. + */ + @PATCH + @AuthProviderRESTExposure + public void patchPermissions(@QueryParam("token") String authToken, + List> patches) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the user directory + Directory userDirectory = userContext.getUserDirectory(); + + // All users who have had permissions added or removed + Map modifiedUsers = new HashMap(); + + for (APIPatch patch : patches) { + + String userID = patch.getPath(); + Permission permission = patch.getValue().toPermission(); + + // See if we've already modified this user in this request + User user = modifiedUsers.get(userID); + if (user == null) + user = userDirectory.get(userID); + + if (user == null) + throw new HTTPException(Status.NOT_FOUND, "User not found with userID " + userID + "."); + + // Only the add and remove operations are supported for permissions + switch(patch.getOp()) { + case add: + user.addPermission(permission); + modifiedUsers.put(userID, user); + break; + case remove: + user.removePermission(permission); + modifiedUsers.put(userID, user); + break; + } + + } + + // Save the permission changes for all modified users + for (User user : modifiedUsers.values()) + userDirectory.update(user); + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java new file mode 100644 index 000000000..bdff3d84b --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.permission; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.glyptodon.guacamole.net.auth.permission.Permission; + +/** + * A service for performing useful manipulations on REST Permissions. + * + * @author James Muehlner + */ +public class PermissionService { + + /** + * Converts a list of Permission to a list of APIPermission objects for + * exposing with the REST endpoints. + * + * @param permissions The Connections to convert for REST endpoint use. + * @return A List of APIPermission objects for use with the REST endpoint. + */ + public List convertPermissionList(Iterable permissions) { + + List restPermissions = new ArrayList(); + + for(Permission permission : permissions) + restPermissions.add(new APIPermission(permission)); + + return restPermissions; + + } + + /** + * Converts a list of APIPermission to a set of Permission objects for internal + * Guacamole use. + * + * @param restPermissions The APIPermission objects from the REST endpoints. + * @return a List of Permission objects for internal Guacamole use. + */ + public Set convertAPIPermissionList(Iterable restPermissions) { + + Set permissions = new HashSet(); + + for(APIPermission restPermission : restPermissions) + permissions.add(restPermission.toPermission()); + + return permissions; + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/package-info.java new file mode 100644 index 000000000..274d74b0b --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Classes related to the permission manipulation aspect of the Guacamole REST API. + */ +package org.glyptodon.guacamole.net.basic.rest.permission; + diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRESTService.java new file mode 100644 index 000000000..dacdf8f56 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRESTService.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.protocol; + +import com.google.inject.Inject; +import java.util.Map; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.basic.ProtocolInfo; +import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A REST Service for handling the listing of protocols. + * + * @author James Muehlner + */ +@Path("/protocol") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ProtocolRESTService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(ProtocolRESTService.class); + + /** + * Service for retrieving protocol definitions. + */ + @Inject + private ProtocolRetrievalService protocolRetrievalservice; + + /** + * Gets a map of protocols defined in the system - protocol name to protocol. + * + * @return The protocol map. + * @throws GuacamoleException If a problem is encountered while listing protocols. + */ + @GET + @AuthProviderRESTExposure + public Map getProtocols() throws GuacamoleException { + + // Get and return a map of all protocols. + return protocolRetrievalservice.getProtocolMap(); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/protocols/List.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRetrievalService.java similarity index 51% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/protocols/List.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRetrievalService.java index f8eda88e9..9f8958f8d 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/protocols/List.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRetrievalService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,7 @@ * THE SOFTWARE. */ -package org.glyptodon.guacamole.net.basic.crud.protocols; +package org.glyptodon.guacamole.net.basic.rest.protocol; import java.io.BufferedInputStream; import java.io.File; @@ -30,18 +30,8 @@ import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleServerException; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; import org.glyptodon.guacamole.net.basic.ProtocolInfo; -import org.glyptodon.guacamole.net.basic.ProtocolParameter; -import org.glyptodon.guacamole.net.basic.ProtocolParameterOption; import org.glyptodon.guacamole.net.basic.xml.DocumentHandler; import org.glyptodon.guacamole.net.basic.xml.protocol.ProtocolTagHandler; import org.glyptodon.guacamole.properties.GuacamoleHome; @@ -53,18 +43,18 @@ import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; /** - * Simple HttpServlet which outputs XML containing a list of all visible - * protocols. - * - * @author Michael Jumper + * A service for retrieving protocol definitions from the + * XML files storing the information. + * + * @author James Muehlner */ -public class List extends RestrictedHttpServlet { +public class ProtocolRetrievalService { /** * Logger for this class. */ - private Logger logger = LoggerFactory.getLogger(List.class); - + private static final Logger logger = LoggerFactory.getLogger(ProtocolRetrievalService.class); + /** * Array of all known protocol names. */ @@ -108,110 +98,22 @@ public class List extends RestrictedHttpServlet { } catch (IOException e) { - throw new GuacamoleException("Error reading basic user mapping file.", e); + throw new GuacamoleException(e); } catch (SAXException e) { - throw new GuacamoleException("Error parsing basic user mapping XML.", e); + throw new GuacamoleException(e); } } /** - * Given an XML stream and a fully-populated ProtocolInfo object, writes - * out the corresponding protocol XML describing all available parameters. + * Retrieves a map of protocol name to ProtocolInfo for all protocols defined + * in the XML files. * - * @param xml The XMLStreamWriter to use to write the XML. - * @param protocol The ProtocolInfo object to read parameters and protocol - * information from. - * @throws XMLStreamException If an error occurs while writing the XML. + * @return A map of all defined protocols. + * @throws GuacamoleException If an error occurs while retrieving the protocols. */ - private void writeProtocol(XMLStreamWriter xml, ProtocolInfo protocol) - throws XMLStreamException { - - // Write protocol - xml.writeStartElement("protocol"); - xml.writeAttribute("name", protocol.getName()); - xml.writeAttribute("title", protocol.getTitle()); - - // Write parameters - for (ProtocolParameter param : protocol.getParameters()) { - - // Write param tag - xml.writeStartElement("param"); - xml.writeAttribute("name", param.getName()); - xml.writeAttribute("title", param.getTitle()); - - // Write type - switch (param.getType()) { - - // Text parameter - case TEXT: - xml.writeAttribute("type", "text"); - break; - - // Password parameter - case PASSWORD: - xml.writeAttribute("type", "password"); - break; - - // Numeric parameter - case NUMERIC: - xml.writeAttribute("type", "numeric"); - break; - - // Boolean parameter - case BOOLEAN: - xml.writeAttribute("type", "boolean"); - xml.writeAttribute("value", param.getValue()); - break; - - // Enumerated parameter - case ENUM: - xml.writeAttribute("type", "enum"); - break; - - // Multiline parameter - case MULTILINE: - xml.writeAttribute("type", "multiline"); - break; - - // If unknown, fail explicitly - default: - throw new UnsupportedOperationException( - "Parameter type not supported: " + param.getType()); - - } - - // Write options - for (ProtocolParameterOption option : param.getOptions()) { - xml.writeStartElement("option"); - xml.writeAttribute("value", option.getValue()); - xml.writeCharacters(option.getTitle()); - xml.writeEndElement(); - } - - // End parameter - xml.writeEndElement(); - - } - - // End protocol - xml.writeEndElement(); - - } - - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Do not cache - response.setHeader("Cache-Control", "no-cache"); - - // Set encoding - response.setCharacterEncoding("UTF-8"); - + public Map getProtocolMap() throws GuacamoleException { // Map of all available protocols Map protocols = new HashMap(); @@ -225,20 +127,16 @@ public class List extends RestrictedHttpServlet { // Get all XML files File[] files = protocol_directory.listFiles( new FilenameFilter() { - @Override public boolean accept(File file, String string) { return string.endsWith(".xml"); } - } ); // Load each protocol from each file for (File file : files) { - try { - // Parse protocol FileInputStream stream = new FileInputStream(file); ProtocolInfo protocol = getProtocol(stream); @@ -252,9 +150,7 @@ public class List extends RestrictedHttpServlet { logger.error("Unable to read connection parameter information from \"{}\": {}", file.getAbsolutePath(), e.getMessage()); logger.debug("Error reading protocol XML.", e); } - } - } // If known protocols are not already defined, read from classpath @@ -262,8 +158,7 @@ public class List extends RestrictedHttpServlet { // If protocol not defined yet, attempt to load from classpath if (!protocols.containsKey(protocol)) { - - InputStream stream = List.class.getResourceAsStream( + InputStream stream = ProtocolRetrievalService.class.getResourceAsStream( "/net/sourceforge/guacamole/net/protocols/" + protocol + ".xml"); @@ -272,39 +167,9 @@ public class List extends RestrictedHttpServlet { protocols.put(protocol, getProtocol(stream)); } - } - - // Write actual XML - try { - // Write XML content type - response.setHeader("Content-Type", "text/xml"); - - XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); - XMLStreamWriter xml = outputFactory.createXMLStreamWriter(response.getWriter()); - - // Begin document - xml.writeStartDocument(); - xml.writeStartElement("protocols"); - - // Write all protocols - for (ProtocolInfo protocol : protocols.values()) - writeProtocol(xml, protocol); - - // End document - xml.writeEndElement(); - xml.writeEndDocument(); - - } - catch (XMLStreamException e) { - throw new GuacamoleServerException( - "Unable to write protocol list XML.", e); - } - catch (IOException e) { - throw new GuacamoleServerException( - "I/O error writing protocol list XML.", e); - } - + + return protocols; } } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/protocols/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/package-info.java similarity index 86% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/protocols/package-info.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/package-info.java index ada38e392..445dc5cef 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/protocols/package-info.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,7 @@ */ /** - * Servlets dedicated to CRUD operations related to protocols. + * Classes related to the protocol retrieval aspect of the Guacamole REST API. */ -package org.glyptodon.guacamole.net.basic.crud.protocols; +package org.glyptodon.guacamole.net.basic.rest.protocol; diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.java new file mode 100644 index 000000000..2efcb6992 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.user; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.glyptodon.guacamole.net.auth.User; + +/** + * A simple User to expose through the REST endpoints. + * + * @author James Muehlner + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) +public class APIUser { + + /** + * The username of this user. + */ + private String username; + + /** + * The password of this user. + */ + private String password; + + /** + * Construct a new empty APIUser. + */ + public APIUser() {} + + /** + * Construct a new APIUser from the provided User. + * @param user The User to construct the APIUser from. + */ + public APIUser(User user) { + this.username = user.getUsername(); + this.password = user.getPassword(); + } + + /** + * Returns the username for this user. + * @return The username for this user. + */ + public String getUsername() { + return username; + } + + /** + * Set the username for this user. + * @param username The username for this user. + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Returns the password for this user. + * @return The password for this user. + */ + public String getPassword() { + return password; + } + + /** + * Set the password for this user. + * @param password The password for this user. + */ + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWrapper.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWrapper.java new file mode 100644 index 000000000..dec5a9a65 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWrapper.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.user; + +import java.util.Collections; +import java.util.Set; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.User; +import org.glyptodon.guacamole.net.auth.permission.Permission; + +/** + * A wrapper to make an APIConnection look like a User. Useful where a + * org.glyptodon.guacamole.net.auth.User is required. + * + * @author James Muehlner + */ +public class APIUserWrapper implements User { + + /** + * The wrapped APIUser. + */ + private final APIUser apiUser; + + /** + * The set of permissions for this user. + * NOTE: Not exposed by the REST endpoints. + */ + private Set permissionSet = Collections.EMPTY_SET; + + /** + * Wrap a given APIUser to expose as a User. + * @param apiUser The APIUser to wrap. + */ + public APIUserWrapper(APIUser apiUser) { + this.apiUser = apiUser; + } + + /** + * Wrap a given APIUser to expose as a User, with the given permission set. + * @param apiUser The APIUser to wrap. + * @param permissionSet The set of permissions for the wrapped user. + */ + public APIUserWrapper(APIUser apiUser, Set permissionSet) { + this.apiUser = apiUser; + this.permissionSet = permissionSet; + } + + @Override + public String getUsername() { + return apiUser.getUsername(); + } + + @Override + public void setUsername(String username) { + apiUser.setUsername(username); + } + + @Override + public String getPassword() { + return apiUser.getPassword(); + } + + @Override + public void setPassword(String password) { + apiUser.setPassword(password); + } + + @Override + public Set getPermissions() throws GuacamoleException { + return permissionSet; + } + + @Override + public boolean hasPermission(Permission permission) throws GuacamoleException { + return permissionSet.contains(permission); + } + + @Override + public void addPermission(Permission permission) throws GuacamoleException { + throw new UnsupportedOperationException("Operation not supported."); + } + + @Override + public void removePermission(Permission permission) throws GuacamoleException { + throw new UnsupportedOperationException("Operation not supported."); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java new file mode 100644 index 000000000..176615b1d --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest.user; + +import com.google.inject.Inject; +import java.util.List; +import java.util.UUID; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.Directory; +import org.glyptodon.guacamole.net.auth.User; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.HTTPException; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A REST Service for handling user CRUD operations. + * + * @author James Muehlner + */ +@Path("/user") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class UserRESTService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(UserRESTService.class); + + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; + + /** + * A service for managing the REST endpoint APIPermission objects. + */ + @Inject + private UserService userService; + + /** + * Gets a list of users in the system. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @return The user list. + * @throws GuacamoleException If a problem is encountered while listing users. + */ + @GET + @AuthProviderRESTExposure + public List getUsers(@QueryParam("token") String authToken) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + // Convert and return the user directory listing + return userService.convertUserList(userDirectory); + + } + + /** + * Gets an individual user. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param userID The ID of the user to retrieve. + * @return user The user. + * @throws GuacamoleException If a problem is encountered while retrieving the user. + */ + @GET + @Path("/{userID}") + @AuthProviderRESTExposure + public APIUser getUser(@QueryParam("token") String authToken, @PathParam("userID") String userID) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + // Get the user + User user = userDirectory.get(userID); + if (user == null) + throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + + // Return the user + return new APIUser(user); + + } + + /** + * Creates a new user and returns the username. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param user The new user to create. + * @throws GuacamoleException If a problem is encountered while creating the user. + * + * @return The username of the newly created user. + */ + @POST + @AuthProviderRESTExposure + public String createUser(@QueryParam("token") String authToken, APIUser user) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + // Randomly set the password if it wasn't provided + if (user.getPassword() == null) + user.setPassword(UUID.randomUUID().toString()); + + // Create the user + userDirectory.add(new APIUserWrapper(user)); + + return user.getUsername(); + + } + + /** + * Updates an individual existing user. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param userID The unique identifier of the user to update. + * @param user The updated user. + * @throws GuacamoleException If a problem is encountered while updating the user. + */ + @POST + @Path("/{userID}") + @AuthProviderRESTExposure + public void updateUser(@QueryParam("token") String authToken, @PathParam("userID") String userID, APIUser user) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + if (!user.getUsername().equals(userID)) + throw new HTTPException(Response.Status.BAD_REQUEST, "Username does not match provided userID."); + + // Get the user + User existingUser = userDirectory.get(userID); + if (existingUser == null) + throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + + // Do not update the user password if no password was provided + if (user.getPassword() != null) { + /* + * Update the user with the permission set from the existing user + * since the user REST endpoints do not expose permissions. + */ + existingUser.setPassword(user.getPassword()); + userDirectory.update(existingUser); + } + + } + + /** + * Deletes an individual existing user. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param userID The unique identifier of the user to delete. + * @throws GuacamoleException If a problem is encountered while deleting the user. + */ + @DELETE + @Path("/{userID}") + @AuthProviderRESTExposure + public void deleteUser(@QueryParam("token") String authToken, @PathParam("userID") String userID) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + // Get the user + User existingUser = userDirectory.get(userID); + if (existingUser == null) + throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + + // Delete the user + userDirectory.remove(userID); + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Delete.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserService.java similarity index 54% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Delete.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserService.java index 8eb5916d3..c671b5246 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/users/Delete.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,39 +20,40 @@ * THE SOFTWARE. */ -package org.glyptodon.guacamole.net.basic.crud.users; +package org.glyptodon.guacamole.net.basic.rest.user; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet; /** - * Simple HttpServlet which handles user deletion. - * - * @author Michael Jumper + * A service for performing useful manipulations on REST Users. + * + * @author James Muehlner */ -public class Delete extends RestrictedHttpServlet { +public class UserService { + + /** + * Converts a user directory to a list of APIUser objects for + * exposing with the REST endpoints. + * + * @param userDirectory The user directory to convert for REST endpoint use. + * @return A List of APIUser objects for use with the REST endpoint. + * @throws GuacamoleException If an error occurs while converting the + * user directory. + */ + public List convertUserList(Directory userDirectory) + throws GuacamoleException { - @Override - protected void restrictedService( - UserContext context, - HttpServletRequest request, HttpServletResponse response) - throws GuacamoleException { - - // Get username - String username = request.getParameter("name"); - - // Attempt to get user directory - Directory directory = context.getUserDirectory(); - - // Remove user - directory.remove(username); + List restUsers = new ArrayList(); + + for(String username : userDirectory.getIdentifiers()) + restUsers.add(new APIUser(userDirectory.get(username))); + + return restUsers; } - + } - diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/permissions/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/package-info.java similarity index 86% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/permissions/package-info.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/package-info.java index 9617b8020..d45b5f4f8 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/crud/permissions/package-info.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,7 @@ */ /** - * Servlets dedicated to CRUD operations related to Permissions. + * Classes related to the user manipulation aspect of the Guacamole REST API. */ -package org.glyptodon.guacamole.net.basic.crud.permissions; +package org.glyptodon.guacamole.net.basic.rest.user; diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/BasicGuacamoleWebSocketTunnelEndpoint.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/BasicGuacamoleWebSocketTunnelEndpoint.java index 77db63f68..0dbc11efc 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/BasicGuacamoleWebSocketTunnelEndpoint.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/BasicGuacamoleWebSocketTunnelEndpoint.java @@ -22,25 +22,22 @@ package org.glyptodon.guacamole.net.basic.websocket; +import com.google.inject.Provider; import java.util.Map; import javax.websocket.EndpointConfig; import javax.websocket.HandshakeResponse; import javax.websocket.Session; import javax.websocket.server.HandshakeRequest; -import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; -import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility; +import org.glyptodon.guacamole.net.basic.TunnelRequestService; import org.glyptodon.guacamole.websocket.GuacamoleWebSocketTunnelEndpoint; /** * Tunnel implementation which uses WebSocket as a tunnel backend, rather than * HTTP, properly parsing connection IDs included in the connection request. */ -@ServerEndpoint(value = "/websocket-tunnel", - subprotocols = {"guacamole"}, - configurator = BasicGuacamoleWebSocketTunnelEndpoint.Configurator.class) public class BasicGuacamoleWebSocketTunnelEndpoint extends GuacamoleWebSocketTunnelEndpoint { /** @@ -62,6 +59,25 @@ public class BasicGuacamoleWebSocketTunnelEndpoint extends GuacamoleWebSocketTun */ public static class Configurator extends ServerEndpointConfig.Configurator { + /** + * Provider which provides instances of a service for handling + * tunnel requests. + */ + private final Provider tunnelRequestServiceProvider; + + /** + * Creates a new Configurator which uses the given tunnel request + * service provider to retrieve the necessary service to handle new + * connections requests. + * + * @param tunnelRequestServiceProvider The tunnel request service + * provider to use for all new + * connections. + */ + public Configurator(Provider tunnelRequestServiceProvider) { + this.tunnelRequestServiceProvider = tunnelRequestServiceProvider; + } + @Override public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { @@ -72,8 +88,11 @@ public class BasicGuacamoleWebSocketTunnelEndpoint extends GuacamoleWebSocketTun userProperties.clear(); try { + // Get tunnel request service + TunnelRequestService tunnelRequestService = tunnelRequestServiceProvider.get(); + // Store new tunnel within user properties - GuacamoleTunnel tunnel = BasicTunnelRequestUtility.createTunnel(new WebSocketTunnelRequest(request)); + GuacamoleTunnel tunnel = tunnelRequestService.createTunnel(new WebSocketTunnelRequest(request)); if (tunnel != null) userProperties.put(TUNNEL_USER_PROPERTY, tunnel); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/BasicGuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/BasicGuacamoleWebSocketTunnelServlet.java index f9e6106a3..035e8d3c1 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/BasicGuacamoleWebSocketTunnelServlet.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/BasicGuacamoleWebSocketTunnelServlet.java @@ -22,10 +22,12 @@ package org.glyptodon.guacamole.net.basic.websocket.jetty8; +import com.google.inject.Inject; +import com.google.inject.Singleton; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; -import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility; +import org.glyptodon.guacamole.net.basic.TunnelRequestService; import org.glyptodon.guacamole.net.basic.HTTPTunnelRequest; /** @@ -33,12 +35,19 @@ import org.glyptodon.guacamole.net.basic.HTTPTunnelRequest; * rather than HTTP, properly parsing connection IDs included in the connection * request. */ +@Singleton public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet { + /** + * Service for handling tunnel requests. + */ + @Inject + private TunnelRequestService tunnelRequestService; + @Override protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { - return BasicTunnelRequestUtility.createTunnel(new HTTPTunnelRequest(request)); + return tunnelRequestService.createTunnel(new HTTPTunnelRequest(request)); } } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/BasicGuacamoleWebSocketTunnelListener.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/BasicGuacamoleWebSocketTunnelListener.java index 093bcbfaa..12c4660f2 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/BasicGuacamoleWebSocketTunnelListener.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/BasicGuacamoleWebSocketTunnelListener.java @@ -22,10 +22,12 @@ package org.glyptodon.guacamole.net.basic.websocket.jetty9; +import com.google.inject.Inject; +import com.google.inject.Singleton; import org.eclipse.jetty.websocket.api.Session; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; -import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility; +import org.glyptodon.guacamole.net.basic.TunnelRequestService; /** * WebSocket listener implementation which properly parses connection IDs @@ -33,11 +35,18 @@ import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility; * * @author Michael Jumper */ +@Singleton public class BasicGuacamoleWebSocketTunnelListener extends GuacamoleWebSocketTunnelListener { + /** + * Service for handling tunnel requests. + */ + @Inject + private TunnelRequestService tunnelRequestService; + @Override protected GuacamoleTunnel createTunnel(Session session) throws GuacamoleException { - return BasicTunnelRequestUtility.createTunnel(new WebSocketTunnelRequest(session.getUpgradeRequest())); + return tunnelRequestService.createTunnel(new WebSocketTunnelRequest(session.getUpgradeRequest())); } } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java index e54cf8bdb..d0de4cbf3 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java @@ -22,10 +22,12 @@ package org.glyptodon.guacamole.net.basic.websocket.tomcat; +import com.google.inject.Inject; +import com.google.inject.Singleton; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; -import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility; +import org.glyptodon.guacamole.net.basic.TunnelRequestService; import org.glyptodon.guacamole.net.basic.HTTPTunnelRequest; /** @@ -33,12 +35,19 @@ import org.glyptodon.guacamole.net.basic.HTTPTunnelRequest; * rather than HTTP, properly parsing connection IDs included in the connection * request. */ +@Singleton public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet { + /** + * Service for handling tunnel requests. + */ + @Inject + private TunnelRequestService tunnelRequestService; + @Override protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { - return BasicTunnelRequestUtility.createTunnel(new HTTPTunnelRequest(request)); + return tunnelRequestService.createTunnel(new HTTPTunnelRequest(request)); }; } diff --git a/guacamole/src/main/webapp/WEB-INF/web.xml b/guacamole/src/main/webapp/WEB-INF/web.xml index 7c9985f62..a59ddbe24 100644 --- a/guacamole/src/main/webapp/WEB-INF/web.xml +++ b/guacamole/src/main/webapp/WEB-INF/web.xml @@ -20,286 +20,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - + index.xhtml - - 30 - + 30 - - - org.glyptodon.guacamole.net.basic.websocket.WebSocketSupportLoader - - - - - org.glyptodon.guacamole.net.basic.log.LogbackInitializer - - - + - AuthenticatingFilter - org.glyptodon.guacamole.net.basic.AuthenticatingFilter + guiceFilter + com.google.inject.servlet.GuiceFilter - AuthenticatingFilter - *.xhtml - /login - /clipboard - /connections/* - /connectiongroups/* - /users/* - /permissions/* - /protocols/* + guiceFilter + /* - - - RestrictedFilter - org.glyptodon.guacamole.net.basic.RestrictedFilter - - - RestrictedFilter - /websocket-tunnel - + + org.glyptodon.guacamole.net.basic.BasicServletContextListener + - - - Login servlet. - Login - org.glyptodon.guacamole.net.basic.BasicLogin - - - Login - /login - - - - - Logout servlet. - Logout - org.glyptodon.guacamole.net.basic.BasicLogout - - - Logout - /logout - - - - - Session keep-alive servlet. - SessionKeepAlive - org.glyptodon.guacamole.net.basic.SessionKeepAlive - - - SessionKeepAlive - /keep-alive - - - - - Clipboard state servlet. - Clipboard - org.glyptodon.guacamole.net.basic.CaptureClipboard - - - Clipboard - /clipboard - - - - - Connection creation servlet. - ConnectionCreate - org.glyptodon.guacamole.net.basic.crud.connections.Create - - - ConnectionCreate - /connections/create - - - - - Connection list servlet. - Connections - org.glyptodon.guacamole.net.basic.crud.connections.List - - - Connections - /connections - - - - - Connection update servlet. - ConnectionUpdate - org.glyptodon.guacamole.net.basic.crud.connections.Update - - - ConnectionUpdate - /connections/update - - - - - Connection move servlet. - ConnectionMove - org.glyptodon.guacamole.net.basic.crud.connections.Move - - - ConnectionMove - /connections/move - - - - - Connection deletion servlet. - ConnectionDelete - org.glyptodon.guacamole.net.basic.crud.connections.Delete - - - ConnectionDelete - /connections/delete - - - - - ConnectionGroup creation servlet. - ConnectionGroupCreate - org.glyptodon.guacamole.net.basic.crud.connectiongroups.Create - - - ConnectionGroupCreate - /connectiongroups/create - - - - - ConnectionGroup list servlet. - ConnectionGroups - org.glyptodon.guacamole.net.basic.crud.connectiongroups.List - - - ConnectionGroups - /connectiongroups - - - - - ConnectionGroup update servlet. - ConnectionGroupUpdate - org.glyptodon.guacamole.net.basic.crud.connectiongroups.Update - - - ConnectionGroupUpdate - /connectiongroups/update - - - - - ConnectionGroup move servlet. - ConnectionGroupMove - org.glyptodon.guacamole.net.basic.crud.connectiongroups.Move - - - ConnectionGroupMove - /connectiongroups/move - - - - - ConnectionGroup deletion servlet. - ConnectionGroupDelete - org.glyptodon.guacamole.net.basic.crud.connectiongroups.Delete - - - ConnectionGroupDelete - /connectiongroups/delete - - - - - User creation servlet. - UserCreate - org.glyptodon.guacamole.net.basic.crud.users.Create - - - UserCreate - /users/create - - - - - User list servlet. - Users - org.glyptodon.guacamole.net.basic.crud.users.List - - - Users - /users - - - - - User update servlet. - UserUpdate - org.glyptodon.guacamole.net.basic.crud.users.Update - - - UserUpdate - /users/update - - - - - User deletion servlet. - UserDelete - org.glyptodon.guacamole.net.basic.crud.users.Delete - - - UserDelete - /users/delete - - - - - Permission list servlet. - Permissions - org.glyptodon.guacamole.net.basic.crud.permissions.List - - - Permissions - /permissions - - - - - Protocol list servlet. - Protocols - org.glyptodon.guacamole.net.basic.crud.protocols.List - - - Protocols - /protocols - - - - - Tunnel servlet. - Tunnel - org.glyptodon.guacamole.net.basic.BasicGuacamoleTunnelServlet - - - Tunnel - /tunnel - - + mp3 audio/mpeg