diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.java index 53c2d3b5d..c15e5e067 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.java @@ -123,6 +123,7 @@ public class ConnectionService extends ModeledGroupedDirectoryObjectService getParameterValues(String name); - + public abstract List getParameterValues(String name); + + /** + * Returns the value of the parameter having the given name, throwing an + * exception if the parameter is missing. + * + * @param name + * The name of the parameter to return. + * + * @return + * The value of the parameter having the given name. + * + * @throws GuacamoleException + * If the parameter is not present in the request. + */ + public String getRequiredParameter(String name) throws GuacamoleException { + + // Pull requested parameter, aborting if absent + String value = getParameter(name); + if (value == null) + throw new GuacamoleClientException("Parameter \"" + name + "\" is required."); + + return value; + + } + + /** + * Returns the integer value of the parameter having the given name, + * throwing an exception if the parameter cannot be parsed. + * + * @param name + * The name of the parameter to return. + * + * @return + * The integer value of the parameter having the given name, or null if + * the parameter is missing. + * + * @throws GuacamoleException + * If the parameter is not a valid integer. + */ + public Integer getIntegerParameter(String name) throws GuacamoleException { + + // Pull requested parameter + String value = getParameter(name); + if (value == null) + return null; + + // Attempt to parse as an integer + try { + return Integer.parseInt(value); + } + + // Rethrow any parsing error as a GuacamoleClientException + catch (NumberFormatException e) { + throw new GuacamoleClientException("Parameter \"" + name + "\" must be a valid integer.", e); + } + + } + + /** + * Returns the authentication token associated with this tunnel request. + * + * @return + * The authentication token associated with this tunnel request, or + * null if no authentication token is present. + */ + public String getAuthenticationToken() { + return getParameter(AUTH_TOKEN_PARAMETER); + } + + /** + * Returns the identifier of the AuthenticationProvider associated with the + * UserContext from which the connection or connection group is to be + * retrieved when the tunnel is created. In the context of the REST API and + * the JavaScript side of the web application, this is referred to as the + * data source identifier. + * + * @return + * The identifier of the AuthenticationProvider associated with the + * UserContext from which the connection or connection group is to be + * retrieved when the tunnel is created. + * + * @throws GuacamoleException + * If the identifier was not present in the request. + */ + public String getAuthenticationProviderIdentifier() + throws GuacamoleException { + return getRequiredParameter(AUTH_PROVIDER_IDENTIFIER_PARAMETER); + } + + /** + * Returns the type of object for which the tunnel is being requested. + * + * @return + * The type of object for which the tunnel is being requested. + * + * @throws GuacamoleException + * If the type was not present in the request, or if the type requested + * is in the wrong format. + */ + public Type getType() throws GuacamoleException { + + String type = getRequiredParameter(TYPE_PARAMETER); + + // For each possible object type + for (Type possibleType : Type.values()) { + + // Match against defined parameter value + if (type.equals(possibleType.PARAMETER_VALUE)) + return possibleType; + + } + + throw new GuacamoleClientException("Illegal identifier - unknown type."); + + } + + /** + * Returns the identifier of the destination of the tunnel being requested. + * As there are multiple types of destination objects available, and within + * multiple data sources, the associated object type and data source are + * also necessary to determine what this identifier refers to. + * + * @return + * The identifier of the destination of the tunnel being requested. + * + * @throws GuacamoleException + * If the identifier was not present in the request. + */ + public String getIdentifier() throws GuacamoleException { + return getRequiredParameter(IDENTIFIER_PARAMETER); + } + + /** + * Returns the display width desired for the Guacamole session over the + * tunnel being requested. + * + * @return + * The display width desired for the Guacamole session over the tunnel + * being requested, or null if no width was given. + * + * @throws GuacamoleException + * If the width specified was not a valid integer. + */ + public Integer getWidth() throws GuacamoleException { + return getIntegerParameter(WIDTH_PARAMETER); + } + + /** + * Returns the display height desired for the Guacamole session over the + * tunnel being requested. + * + * @return + * The display height desired for the Guacamole session over the tunnel + * being requested, or null if no width was given. + * + * @throws GuacamoleException + * If the height specified was not a valid integer. + */ + public Integer getHeight() throws GuacamoleException { + return getIntegerParameter(HEIGHT_PARAMETER); + } + + /** + * Returns the display resolution desired for the Guacamole session over + * the tunnel being requested, in DPI. + * + * @return + * The display resolution desired for the Guacamole session over the + * tunnel being requested, or null if no resolution was given. + * + * @throws GuacamoleException + * If the resolution specified was not a valid integer. + */ + public Integer getDPI() throws GuacamoleException { + return getIntegerParameter(DPI_PARAMETER); + } + + /** + * Returns a list of all audio mimetypes declared as supported within the + * tunnel request. + * + * @return + * A list of all audio mimetypes declared as supported within the + * tunnel request, or null if no mimetypes were specified. + */ + public List getAudioMimetypes() { + return getParameterValues(AUDIO_PARAMETER); + } + + /** + * Returns a list of all video mimetypes declared as supported within the + * tunnel request. + * + * @return + * A list of all video mimetypes declared as supported within the + * tunnel request, or null if no mimetypes were specified. + */ + public List getVideoMimetypes() { + return getParameterValues(VIDEO_PARAMETER); + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java index 9c20eedeb..5953a31ed 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java @@ -25,16 +25,15 @@ package org.glyptodon.guacamole.net.basic; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.List; -import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; -import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel; import org.glyptodon.guacamole.net.GuacamoleTunnel; 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.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.slf4j.Logger; @@ -53,12 +52,6 @@ import org.slf4j.LoggerFactory; @Singleton public class TunnelRequestService { - /** - * The Guacamole server environment. - */ - @Inject - private Environment environment; - /** * Logger for this class. */ @@ -70,6 +63,12 @@ public class TunnelRequestService { @Inject private AuthenticationService authenticationService; + /** + * Service for convenient retrieval of objects. + */ + @Inject + private ObjectRetrievalService retrievalService; + /** * Reads and returns the client information provided within the given * request. @@ -80,35 +79,40 @@ public class TunnelRequestService { * @return GuacamoleClientInformation * An object containing information about the client sending the tunnel * request. + * + * @throws GuacamoleException + * If the parameters of the tunnel request are invalid. */ - protected GuacamoleClientInformation getClientInformation(TunnelRequest request) { + protected GuacamoleClientInformation getClientInformation(TunnelRequest request) + throws GuacamoleException { + // Get client information GuacamoleClientInformation info = new GuacamoleClientInformation(); // Set width if provided - String width = request.getParameter("width"); + Integer width = request.getWidth(); if (width != null) - info.setOptimalScreenWidth(Integer.parseInt(width)); + info.setOptimalScreenWidth(width); // Set height if provided - String height = request.getParameter("height"); + Integer height = request.getHeight(); if (height != null) - info.setOptimalScreenHeight(Integer.parseInt(height)); + info.setOptimalScreenHeight(height); // Set resolution if provided - String dpi = request.getParameter("dpi"); + Integer dpi = request.getDPI(); if (dpi != null) - info.setOptimalResolution(Integer.parseInt(dpi)); + info.setOptimalResolution(dpi); // Add audio mimetypes - List audio_mimetypes = request.getParameterValues("audio"); - if (audio_mimetypes != null) - info.getAudioMimetypes().addAll(audio_mimetypes); + List audioMimetypes = request.getAudioMimetypes(); + if (audioMimetypes != null) + info.getAudioMimetypes().addAll(audioMimetypes); // Add video mimetypes - List video_mimetypes = request.getParameterValues("video"); - if (video_mimetypes != null) - info.getVideoMimetypes().addAll(video_mimetypes); + List videoMimetypes = request.getVideoMimetypes(); + if (videoMimetypes != null) + info.getVideoMimetypes().addAll(videoMimetypes); return info; } @@ -122,7 +126,7 @@ public class TunnelRequestService { * The UserContext associated with the user for whom the tunnel is * being created. * - * @param idType + * @param type * The type of object being connected to (connection or group). * * @param id @@ -138,13 +142,13 @@ public class TunnelRequestService { * If an error occurs while creating the tunnel. */ protected GuacamoleTunnel createConnectedTunnel(UserContext context, - final TunnelRequest.IdentifierType idType, String id, + final TunnelRequest.Type type, String id, GuacamoleClientInformation info) throws GuacamoleException { // Create connected tunnel from identifier GuacamoleTunnel tunnel = null; - switch (idType) { + switch (type) { // Connection identifiers case CONNECTION: { @@ -205,7 +209,7 @@ public class TunnelRequestService { * @param session * The Guacamole session to associate the tunnel with. * - * @param idType + * @param type * The type of object being connected to (connection or group). * * @param id @@ -220,7 +224,7 @@ public class TunnelRequestService { * If an error occurs while obtaining the tunnel. */ protected GuacamoleTunnel createAssociatedTunnel(final GuacamoleSession session, - GuacamoleTunnel tunnel, final TunnelRequest.IdentifierType idType, + GuacamoleTunnel tunnel, final TunnelRequest.Type type, final String id) throws GuacamoleException { // Monitor tunnel closure and data @@ -239,7 +243,7 @@ public class TunnelRequestService { long duration = connectionEndTime - connectionStartTime; // Log closure - switch (idType) { + switch (type) { // Connection identifiers case CONNECTION: @@ -289,27 +293,20 @@ public class TunnelRequestService { public GuacamoleTunnel createTunnel(TunnelRequest request) throws GuacamoleException { - // Get auth token and session - final String authToken = request.getParameter("authToken"); - final GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - - // Get client information and connection ID from request - String id = request.getParameter("id"); - final GuacamoleClientInformation info = getClientInformation(request); - - // Determine ID type - TunnelRequest.IdentifierType idType = TunnelRequest.IdentifierType.getType(id); - if (idType == null) - throw new GuacamoleClientException("Illegal identifier - unknown type."); - - // Remove prefix - id = id.substring(idType.PREFIX.length()); + // Parse request parameters + String authToken = request.getAuthenticationToken(); + String id = request.getIdentifier(); + TunnelRequest.Type type = request.getType(); + String authProviderIdentifier = request.getAuthenticationProviderIdentifier(); + GuacamoleClientInformation info = getClientInformation(request); // Create connected tunnel using provided connection ID and client information - final GuacamoleTunnel tunnel = createConnectedTunnel(session.getUserContext(), idType, id, info); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type, id, info); // Associate tunnel with session - return createAssociatedTunnel(session, tunnel, idType, id); + return createAssociatedTunnel(session, tunnel, type, id); } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java index ac72a34ae..dbf8bfc1e 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java @@ -333,7 +333,10 @@ public class ExtensionModule extends ServletModule { logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read."); return; } - + + // Sort files lexicographically + Arrays.sort(extensionFiles); + // Load each extension within the extension directory for (File extensionFile : extensionFiles) { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java index 981233eb5..a9af8c6c0 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java @@ -25,6 +25,7 @@ package org.glyptodon.guacamole.net.basic.rest; import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; +import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; @@ -48,9 +49,9 @@ public class ObjectRetrievalService { * @param session * The GuacamoleSession to retrieve the UserContext from. * - * @param identifier + * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider that created the - * UserContext being retrieved. Only one UserContext per + * UserContext being retrieved. Only one UserContext per User per * AuthenticationProvider can exist. * * @return @@ -62,7 +63,7 @@ public class ObjectRetrievalService { * UserContext does not exist. */ public UserContext retrieveUserContext(GuacamoleSession session, - String identifier) throws GuacamoleException { + String authProviderIdentifier) throws GuacamoleException { // Get list of UserContexts List userContexts = session.getUserContexts(); @@ -70,11 +71,17 @@ public class ObjectRetrievalService { // Locate and return the UserContext associated with the // AuthenticationProvider having the given identifier, if any for (UserContext userContext : userContexts) { - if (userContext.getAuthenticationProvider().getIdentifier().equals(identifier)) + + // Get AuthenticationProvider associated with current UserContext + AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); + + // If AuthenticationProvider identifier matches, done + if (authProvider.getIdentifier().equals(authProviderIdentifier)) return userContext; + } - throw new GuacamoleResourceNotFoundException("Session not associated with authentication provider \"" + identifier + "\"."); + throw new GuacamoleResourceNotFoundException("Session not associated with authentication provider \"" + authProviderIdentifier + "\"."); } @@ -109,6 +116,35 @@ public class ObjectRetrievalService { } + /** + * Retrieves a single user from the given GuacamoleSession. + * + * @param session + * The GuacamoleSession to retrieve the user from. + * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider that created the + * UserContext from which the user should be retrieved. Only one + * UserContext per User per AuthenticationProvider can exist. + * + * @param identifier + * The identifier of the user to retrieve. + * + * @return + * The user having the given identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the user, or if the + * user does not exist. + */ + public User retrieveUser(GuacamoleSession session, String authProviderIdentifier, + String identifier) throws GuacamoleException { + + UserContext userContext = retrieveUserContext(session, authProviderIdentifier); + return retrieveUser(userContext, identifier); + + } + /** * Retrieves a single connection from the given user context. * @@ -140,6 +176,36 @@ public class ObjectRetrievalService { } + /** + * Retrieves a single connection from the given GuacamoleSession. + * + * @param session + * The GuacamoleSession to retrieve the connection from. + * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider that created the + * UserContext from which the connection should be retrieved. Only one + * UserContext per User per AuthenticationProvider can exist. + * + * @param identifier + * The identifier of the connection to retrieve. + * + * @return + * The connection having the given identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the connection, or if the + * connection does not exist. + */ + public Connection retrieveConnection(GuacamoleSession session, + String authProviderIdentifier, String identifier) + throws GuacamoleException { + + UserContext userContext = retrieveUserContext(session, authProviderIdentifier); + return retrieveConnection(userContext, identifier); + + } + /** * Retrieves a single connection group from the given user context. If * the given identifier the REST API root identifier, the root connection @@ -178,4 +244,37 @@ public class ObjectRetrievalService { } + /** + * Retrieves a single connection group from the given GuacamoleSession. If + * the given identifier is the REST API root identifier, the root + * connection group will be returned. The underlying authentication + * provider may additionally use a different identifier for root. + * + * @param session + * The GuacamoleSession to retrieve the connection group from. + * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider that created the + * UserContext from which the connection group should be retrieved. + * Only one UserContext per User per AuthenticationProvider can exist. + * + * @param identifier + * The identifier of the connection group to retrieve. + * + * @return + * The connection group having the given identifier, or the root + * connection group if the identifier is the root identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the connection group, or if the + * connection group does not exist. + */ + public ConnectionGroup retrieveConnectionGroup(GuacamoleSession session, + String authProviderIdentifier, String identifier) throws GuacamoleException { + + UserContext userContext = retrieveUserContext(session, authProviderIdentifier); + return retrieveConnectionGroup(userContext, identifier); + + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java index 107f97ef8..42ae1c15f 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java @@ -30,6 +30,7 @@ import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; 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; @@ -44,8 +45,10 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.APIPatch; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.PATCH; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; @@ -56,7 +59,7 @@ import org.slf4j.LoggerFactory; * * @author Michael Jumper */ -@Path("/activeConnections") +@Path("/data/{dataSource}/activeConnections") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ActiveConnectionRESTService { @@ -72,6 +75,12 @@ public class ActiveConnectionRESTService { @Inject private AuthenticationService authenticationService; + /** + * Service for convenient retrieval of objects. + */ + @Inject + private ObjectRetrievalService retrievalService; + /** * Gets a list of active connections in the system, filtering the returned * list by the given permissions, if specified. @@ -80,6 +89,10 @@ public class ActiveConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the active connections to be retrieved. + * * @param permissions * The set of permissions to filter with. A user must have one or more * of these permissions for a user to appear in the result. @@ -96,10 +109,12 @@ public class ActiveConnectionRESTService { @GET @AuthProviderRESTExposure public Map getActiveConnections(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @QueryParam("permission") List permissions) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User self = userContext.self(); // Do not filter on permissions if no permissions are specified @@ -140,6 +155,10 @@ public class ActiveConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the active connections to be deleted. + * * @param patches * The active connection patches to apply for this request. * @@ -149,9 +168,11 @@ public class ActiveConnectionRESTService { @PATCH @AuthProviderRESTExposure public void patchTunnels(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, List> patches) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the directory Directory activeConnectionDirectory = userContext.getActiveConnectionDirectory(); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResponse.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResponse.java new file mode 100644 index 000000000..6c4126bc9 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResponse.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2015 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; + +/** + * A simple object to represent an auth token/username pair in the API. + * + * @author James Muehlner + */ +public class APIAuthenticationResponse { + + /** + * The auth token. + */ + private final String authToken; + + + /** + * The username of the user that authenticated. + */ + private final String username; + + /** + * The unique identifier of the data source from which this user account + * came. Although this user account may exist across several data sources + * (AuthenticationProviders), this will be the unique identifier of the + * AuthenticationProvider that authenticated this user for the current + * session. + */ + private final String dataSource; + + /** + * Returns the unique authentication token which identifies the current + * session. + * + * @return + * The user's authentication token. + */ + public String getAuthToken() { + return authToken; + } + + /** + * Returns the user identified by the authentication token associated with + * the current session. + * + * @return + * The user identified by this authentication token. + */ + public String getUsername() { + return username; + } + + /** + * Returns the unique identifier of the data source associated with the user + * account associated with this auth token. + * + * @return + * The unique identifier of the data source associated with the user + * account associated with this auth token. + */ + public String getDataSource() { + return dataSource; + } + + /** + * Create a new APIAuthToken Object with the given auth token. + * + * @param dataSource + * The unique identifier of the AuthenticationProvider which + * authenticated the user. + * + * @param authToken + * The auth token to create the new APIAuthToken with. + * + * @param username + * The username of the user owning the given token. + */ + public APIAuthenticationResponse(String dataSource, String authToken, String username) { + this.dataSource = dataSource; + this.authToken = authToken; + this.username = username; + } + +} 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 index b730a7163..7667bb43c 100644 --- 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 @@ -66,19 +66,6 @@ public class AuthenticationService { } - /** - * 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(); - } - /** * Returns all UserContexts associated with a given auth token, if the auth * token represents a currently logged in user. Throws an unauthorized 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 index 2f29fbe81..270e3c302 100644 --- 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 @@ -48,6 +48,7 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; @@ -60,7 +61,7 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/connections") +@Path("/data/{dataSource}/connections") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ConnectionRESTService { @@ -89,6 +90,10 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection to be retrieved. + * * @param connectionID * The identifier of the connection to retrieve. * @@ -102,12 +107,14 @@ public class ConnectionRESTService { @Path("/{connectionID}") @AuthProviderRESTExposure public APIConnection getConnection(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID) + throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested connection - return new APIConnection(retrievalService.retrieveConnection(userContext, connectionID)); + return new APIConnection(retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID)); } @@ -118,6 +125,11 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection whose parameters are to be + * retrieved. + * * @param connectionID * The identifier of the connection. * @@ -131,9 +143,12 @@ public class ConnectionRESTService { @Path("/{connectionID}/parameters") @AuthProviderRESTExposure public Map getConnectionParameters(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID) + throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User self = userContext.self(); // Retrieve permission sets @@ -163,6 +178,11 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection whose history is to be + * retrieved. + * * @param connectionID * The identifier of the connection. * @@ -177,12 +197,14 @@ public class ConnectionRESTService { @Path("/{connectionID}/history") @AuthProviderRESTExposure public List getConnectionHistory(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID) + throws GuacamoleException { + + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = authenticationService.getUserContext(authToken); - // Retrieve the requested connection - Connection connection = retrievalService.retrieveConnection(userContext, connectionID); + Connection connection = retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID); // Retrieve the requested connection's history List apiRecords = new ArrayList(); @@ -201,6 +223,10 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection to be deleted. + * * @param connectionID * The identifier of the connection to delete. * @@ -210,10 +236,13 @@ public class ConnectionRESTService { @DELETE @Path("/{connectionID}") @AuthProviderRESTExposure - public void deleteConnection(@QueryParam("token") String authToken, @PathParam("connectionID") String connectionID) + public void deleteConnection(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the connection directory Directory connectionDirectory = userContext.getConnectionDirectory(); @@ -231,6 +260,10 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the connection is to be created. + * * @param connection * The connection to create. * @@ -244,9 +277,11 @@ public class ConnectionRESTService { @Produces(MediaType.TEXT_PLAIN) @AuthProviderRESTExposure public String createConnection(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, APIConnection connection) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection data was provided if (connection == null) @@ -270,6 +305,10 @@ public class ConnectionRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection to be updated. + * * @param connectionID * The identifier of the connection to update. * @@ -283,9 +322,12 @@ public class ConnectionRESTService { @Path("/{connectionID}") @AuthProviderRESTExposure public void updateConnection(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID, APIConnection connection) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionID") String connectionID, + APIConnection connection) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection data was provided if (connection == null) 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 index bb3b843cd..5899d32d7 100644 --- 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 @@ -40,6 +40,7 @@ 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.auth.permission.ObjectPermission; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; @@ -51,7 +52,7 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/connectionGroups") +@Path("/data/{dataSource}/connectionGroups") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ConnectionGroupRESTService { @@ -80,6 +81,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection group to be retrieved. + * * @param connectionGroupID * The ID of the connection group to retrieve. * @@ -92,13 +97,15 @@ public class ConnectionGroupRESTService { @GET @Path("/{connectionGroupID}") @AuthProviderRESTExposure - public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken, - @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { + public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionGroupID") String connectionGroupID) + throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested connection group - return new APIConnectionGroup(retrievalService.retrieveConnectionGroup(userContext, connectionGroupID)); + return new APIConnectionGroup(retrievalService.retrieveConnectionGroup(session, authProviderIdentifier, connectionGroupID)); } @@ -109,6 +116,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection group to be retrieved. + * * @param connectionGroupID * The ID of the connection group to retrieve. * @@ -129,11 +140,13 @@ public class ConnectionGroupRESTService { @Path("/{connectionGroupID}/tree") @AuthProviderRESTExposure public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID, @QueryParam("permission") List permissions) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Retrieve the requested tree, filtering by the given permissions ConnectionGroup treeRoot = retrievalService.retrieveConnectionGroup(userContext, connectionGroupID); @@ -151,6 +164,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection group to be deleted. + * * @param connectionGroupID * The identifier of the connection group to delete. * @@ -161,9 +178,12 @@ public class ConnectionGroupRESTService { @Path("/{connectionGroupID}") @AuthProviderRESTExposure public void deleteConnectionGroup(@QueryParam("token") String authToken, - @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionGroupID") String connectionGroupID) + throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the connection group directory Directory connectionGroupDirectory = userContext.getConnectionGroupDirectory(); @@ -183,6 +203,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the connection group is to be created. + * * @param connectionGroup * The connection group to create. * @@ -196,9 +220,11 @@ public class ConnectionGroupRESTService { @Produces(MediaType.TEXT_PLAIN) @AuthProviderRESTExposure public String createConnectionGroup(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, APIConnectionGroup connectionGroup) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection group data was provided if (connectionGroup == null) @@ -222,6 +248,10 @@ public class ConnectionGroupRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext containing the connection group to be updated. + * * @param connectionGroupID * The identifier of the existing connection group to update. * @@ -235,10 +265,13 @@ public class ConnectionGroupRESTService { @Path("/{connectionGroupID}") @AuthProviderRESTExposure public void updateConnectionGroup(@QueryParam("token") String authToken, - @PathParam("connectionGroupID") String connectionGroupID, APIConnectionGroup connectionGroup) + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("connectionGroupID") String connectionGroupID, + APIConnectionGroup connectionGroup) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection group data was provided if (connectionGroup == null) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java index 05d0c6d77..6a09cba32 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java @@ -28,6 +28,7 @@ import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; 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; @@ -36,7 +37,9 @@ import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.form.Form; 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.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.protocols.ProtocolInfo; @@ -46,7 +49,7 @@ import org.glyptodon.guacamole.protocols.ProtocolInfo; * * @author Michael Jumper */ -@Path("/schema") +@Path("/schema/{dataSource}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class SchemaRESTService { @@ -57,6 +60,12 @@ public class SchemaRESTService { @Inject private AuthenticationService authenticationService; + /** + * Service for convenient retrieval of objects. + */ + @Inject + private ObjectRetrievalService retrievalService; + /** * Retrieves the possible attributes of a user object. * @@ -64,6 +73,10 @@ public class SchemaRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext dictating the available user attributes. + * * @return * A collection of forms which describe the possible attributes of a * user object. @@ -74,10 +87,13 @@ public class SchemaRESTService { @GET @Path("/users/attributes") @AuthProviderRESTExposure - public Collection
getUserAttributes(@QueryParam("token") String authToken) throws GuacamoleException { + public Collection getUserAttributes(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier) + throws GuacamoleException { // Retrieve all possible user attributes - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getUserAttributes(); } @@ -89,6 +105,10 @@ public class SchemaRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext dictating the available connection attributes. + * * @return * A collection of forms which describe the possible attributes of a * connection object. @@ -99,10 +119,13 @@ public class SchemaRESTService { @GET @Path("/connections/attributes") @AuthProviderRESTExposure - public Collection getConnectionAttributes(@QueryParam("token") String authToken) throws GuacamoleException { + public Collection getConnectionAttributes(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier) + throws GuacamoleException { // Retrieve all possible connection attributes - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getConnectionAttributes(); } @@ -114,6 +137,11 @@ public class SchemaRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext dictating the available connection group + * attributes. + * * @return * A collection of forms which describe the possible attributes of a * connection group object. @@ -124,10 +152,13 @@ public class SchemaRESTService { @GET @Path("/connectionGroups/attributes") @AuthProviderRESTExposure - public Collection getConnectionGroupAttributes(@QueryParam("token") String authToken) throws GuacamoleException { + public Collection getConnectionGroupAttributes(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier) + throws GuacamoleException { // Retrieve all possible connection group attributes - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getConnectionGroupAttributes(); } @@ -139,6 +170,13 @@ public class SchemaRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext dictating the protocols available. Currently, the + * UserContext actually does not dictate this, the the same set of + * protocols will be retrieved for all users, though the identifier + * given here will be validated. + * * @return * A map of protocol information, where each key is the unique name * associated with that protocol. @@ -149,10 +187,13 @@ public class SchemaRESTService { @GET @Path("/protocols") @AuthProviderRESTExposure - public Map getProtocols(@QueryParam("token") String authToken) throws GuacamoleException { + public Map getProtocols(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier) + throws GuacamoleException { - // Verify the given auth token is valid - authenticationService.getUserContext(authToken); + // Verify the given auth token and auth provider identifier are valid + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get and return a map of all protocols. Environment env = new LocalEnvironment(); 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 index 37fda2bff..687d4ff85 100644 --- 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 @@ -41,6 +41,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; +import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; @@ -51,6 +52,7 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.APIError; import org.glyptodon.guacamole.net.basic.rest.APIPatch; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.add; @@ -68,8 +70,9 @@ import org.slf4j.LoggerFactory; * A REST Service for handling user CRUD operations. * * @author James Muehlner + * @author Michael Jumper */ -@Path("/users") +@Path("/data/{dataSource}/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class UserRESTService { @@ -120,42 +123,44 @@ public class UserRESTService { */ @Inject private ObjectRetrievalService retrievalService; - + /** - * Gets a list of users in the system, filtering the returned list by the - * given permission, if specified. - * + * Gets a list of users in the given data source (UserContext), filtering + * the returned list by the given permission, if specified. + * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext from which the users are to be retrieved. + * * @param permissions * The set of permissions to filter with. A user must have one or more - * of these permissions for a user to appear in the result. + * of these permissions for a user to appear in the result. * If null, no filtering will be performed. - * + * * @return * A list of all visible users. If a permission was specified, this * list will contain only those users for whom the current user has * that permission. - * + * * @throws GuacamoleException * If an error is encountered while retrieving users. */ @GET @AuthProviderRESTExposure public List getUsers(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @QueryParam("permission") List permissions) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - User self = userContext.self(); - - // Do not filter on permissions if no permissions are specified - if (permissions != null && permissions.isEmpty()) - permissions = null; + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // An admin user has access to any user + User self = userContext.self(); SystemPermissionSet systemPermissions = self.getSystemPermissions(); boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER); @@ -164,7 +169,7 @@ public class UserRESTService { // Filter users, if requested Collection userIdentifiers = userDirectory.getIdentifiers(); - if (!isAdmin && permissions != null) { + if (!isAdmin && permissions != null && !permissions.isEmpty()) { ObjectPermissionSet userPermissions = self.getUserPermissions(); userIdentifiers = userPermissions.getAccessibleObjects(permissions, userIdentifiers); } @@ -174,7 +179,6 @@ public class UserRESTService { for (User user : userDirectory.getAll(userIdentifiers)) apiUsers.add(new APIUser(user)); - // Return the converted user list return apiUsers; } @@ -186,6 +190,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext from which the requested user is to be retrieved. + * * @param username * The username of the user to retrieve. * @@ -198,34 +206,49 @@ public class UserRESTService { @GET @Path("/{username}") @AuthProviderRESTExposure - public APIUser getUser(@QueryParam("token") String authToken, @PathParam("username") String username) + public APIUser getUser(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("username") String username) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested user - User user = retrievalService.retrieveUser(userContext, username); + User user = retrievalService.retrieveUser(session, authProviderIdentifier, username); 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. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be created. + * + * @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 @Produces(MediaType.TEXT_PLAIN) @AuthProviderRESTExposure - public String createUser(@QueryParam("token") String authToken, APIUser user) + public String createUser(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, APIUser user) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Get the directory Directory userDirectory = userContext.getUserDirectory(); @@ -247,6 +270,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be updated. + * * @param username * The username of the user to update. * @@ -260,11 +287,13 @@ public class UserRESTService { @Path("/{username}") @AuthProviderRESTExposure public void updateUser(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, APIUser user) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Get the directory Directory userDirectory = userContext.getUserDirectory(); @@ -301,6 +330,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be updated. + * * @param username * The username of the user to update. * @@ -318,12 +351,14 @@ public class UserRESTService { @Path("/{username}/password") @AuthProviderRESTExposure public void updatePassword(@QueryParam("token") String authToken, - @PathParam("username") String username, + @PathParam("dataSource") String authProviderIdentifier, + @PathParam("username") String username, APIUserPasswordUpdate userPasswordUpdate, @Context HttpServletRequest request) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Build credentials Credentials credentials = new Credentials(); credentials.setUsername(username); @@ -333,7 +368,8 @@ public class UserRESTService { // Verify that the old password was correct try { - if (userContext.getAuthenticationProvider().authenticateUser(credentials) == null) { + AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); + if (authProvider.authenticateUser(credentials) == null) { throw new APIException(APIError.Type.PERMISSION_DENIED, "Permission denied."); } @@ -366,6 +402,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext from which the requested user is to be deleted. + * * @param username * The username of the user to delete. * @@ -376,11 +416,13 @@ public class UserRESTService { @Path("/{username}") @AuthProviderRESTExposure public void deleteUser(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Get the directory Directory userDirectory = userContext.getUserDirectory(); @@ -401,6 +443,10 @@ public class UserRESTService { * The authentication token that is used to authenticate the user * performing the operation. * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be found. + * * @param username * The username of the user to retrieve permissions for. * @@ -414,10 +460,12 @@ public class UserRESTService { @Path("/{username}/permissions") @AuthProviderRESTExposure public APIPermissionSet getPermissions(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User user; @@ -489,7 +537,11 @@ public class UserRESTService { * @param authToken * The authentication token that is used to authenticate the user * performing the operation. - * + * + * @param authProviderIdentifier + * The unique identifier of the AuthenticationProvider associated with + * the UserContext in which the requested user is to be found. + * * @param username * The username of the user to modify the permissions of. * @@ -503,11 +555,13 @@ public class UserRESTService { @Path("/{username}/permissions") @AuthProviderRESTExposure public void patchPermissions(@QueryParam("token") String authToken, + @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, List> patches) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); - + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); + // Get the user User user = userContext.getUserDirectory().get(username); if (user == null) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.java index cf6922157..6a687f8f0 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.java @@ -32,7 +32,7 @@ import org.glyptodon.guacamole.net.basic.TunnelRequest; * * @author Michael Jumper */ -public class WebSocketTunnelRequest implements TunnelRequest { +public class WebSocketTunnelRequest extends TunnelRequest { /** * All parameters passed via HTTP to the WebSocket handshake. diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.java index 1c4c96bd8..5e71b3824 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.java @@ -33,7 +33,7 @@ import org.glyptodon.guacamole.net.basic.TunnelRequest; * * @author Michael Jumper */ -public class WebSocketTunnelRequest implements TunnelRequest { +public class WebSocketTunnelRequest extends TunnelRequest { /** * All parameters passed via HTTP to the WebSocket handshake. diff --git a/guacamole/src/main/webapp/app/auth/service/authenticationService.js b/guacamole/src/main/webapp/app/auth/service/authenticationService.js index 8c17cbea2..5dd549a34 100644 --- a/guacamole/src/main/webapp/app/auth/service/authenticationService.js +++ b/guacamole/src/main/webapp/app/auth/service/authenticationService.js @@ -99,9 +99,10 @@ angular.module('auth').factory('authenticationService', ['$injector', // Store auth data $cookieStore.put(AUTH_COOKIE_ID, { - authToken : data.authToken, - username : data.username, - dataSource : data.dataSource + 'authToken' : data.authToken, + 'username' : data.username, + 'dataSource' : data.dataSource, + 'availableDataSources' : data.availableDataSources }); // Process is complete @@ -320,7 +321,7 @@ angular.module('auth').factory('authenticationService', ['$injector', * * @returns {String[]} * The identifiers of all data sources availble to the current user, - * or null if no authentication data is present. + * or an empty array if no authentication data is present. */ service.getAvailableDataSources = function getAvailableDataSources() { @@ -330,7 +331,7 @@ angular.module('auth').factory('authenticationService', ['$injector', return authData.availableDataSources; // No auth data present - return null; + return []; }; diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 692051bc1..2fab8e37b 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -158,7 +158,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams var RECONNECT_ACTION = { name : "CLIENT.ACTION_RECONNECT", callback : function reconnectCallback() { - $scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params); + $scope.client = guacClientManager.replaceManagedClient($routeParams.id, $routeParams.params); guacNotification.showStatus(false); } }; @@ -219,13 +219,13 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) { $scope.clipboardData = clipboardData; }); - - /* - * Parse the type, name, and id out of the url paramteres, - * as well as any extra parameters if set. + + /** + * The client which should be attached to the client UI. + * + * @type ManagedClient */ - var uniqueId = $routeParams.type + '/' + $routeParams.id; - $scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params); + $scope.client = guacClientManager.getManagedClient($routeParams.id, $routeParams.params); var keysCurrentlyPressed = {}; diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index 822a80870..c2a3bf035 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -28,6 +28,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Required types var ClientProperties = $injector.get('ClientProperties'); + var ClientIdentifier = $injector.get('ClientIdentifier'); var ManagedClientState = $injector.get('ManagedClientState'); var ManagedDisplay = $injector.get('ManagedDisplay'); var ManagedFileDownload = $injector.get('ManagedFileDownload'); @@ -153,8 +154,8 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * desired connection ID, display resolution, and supported audio/video * codecs. * - * @param {String} id - * The ID of the connection or group to connect to. + * @param {ClientIdentifier} identifier + * The identifier representing the connection or group to connect to. * * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. @@ -163,7 +164,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * The string of connection parameters to be passed to the Guacamole * client. */ - var getConnectString = function getConnectString(id, connectionParameters) { + var getConnectString = function getConnectString(identifier, connectionParameters) { // Calculate optimal width/height for display var pixel_density = $window.devicePixelRatio || 1; @@ -173,21 +174,23 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Build base connect string var connectString = - "id=" + encodeURIComponent(id) - + "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken()) - + "&width=" + Math.floor(optimal_width) - + "&height=" + Math.floor(optimal_height) - + "&dpi=" + Math.floor(optimal_dpi) + "token=" + encodeURIComponent(authenticationService.getCurrentToken()) + + "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource) + + "&GUAC_ID=" + encodeURIComponent(identifier.id) + + "&GUAC_TYPE=" + encodeURIComponent(identifier.type) + + "&GUAC_WIDTH=" + Math.floor(optimal_width) + + "&GUAC_HEIGHT=" + Math.floor(optimal_height) + + "&GUAC_DPI=" + Math.floor(optimal_dpi) + (connectionParameters ? '&' + connectionParameters : ''); // Add audio mimetypes to connect_string guacAudio.supported.forEach(function(mimetype) { - connectString += "&audio=" + encodeURIComponent(mimetype); + connectString += "&GUAC_AUDIO=" + encodeURIComponent(mimetype); }); // Add video mimetypes to connect_string guacVideo.supported.forEach(function(mimetype) { - connectString += "&video=" + encodeURIComponent(mimetype); + connectString += "&GUAC_VIDEO=" + encodeURIComponent(mimetype); }); return connectString; @@ -238,7 +241,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * or group. * * @param {String} id - * The ID of the connection or group to connect to. + * The ID of the connection or group to connect to. This String must be + * a valid ClientIdentifier string, as would be generated by + * ClientIdentifier.toString(). * * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. @@ -402,23 +407,23 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Manage the client display managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); - // Connect the Guacamole client - client.connect(getConnectString(id, connectionParameters)); + // Parse connection details from ID + var clientIdentifier = ClientIdentifier.fromString(id); - // Determine type of connection - var typePrefix = id.substring(0, 2); + // Connect the Guacamole client + client.connect(getConnectString(clientIdentifier, connectionParameters)); // If using a connection, pull connection name - if (typePrefix === 'c/') { - connectionService.getConnection(id.substring(2)) + if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) { + connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id) .success(function connectionRetrieved(connection) { managedClient.name = connection.name; }); } // If using a connection group, pull connection name - else if (typePrefix === 'g/') { - connectionGroupService.getConnectionGroup(id.substring(2)) + else if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION_GROUP) { + connectionGroupService.getConnectionGroup(clientIdentifier.dataSource, clientIdentifier.id) .success(function connectionGroupRetrieved(group) { managedClient.name = group.name; }); diff --git a/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js b/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js index 85483b195..0410567ee 100644 --- a/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js +++ b/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js @@ -697,15 +697,15 @@ angular.module('form').controller('timeZoneFieldController', ['$scope', '$inject */ $scope.region = ''; - // Restore time zone selection when region changes - $scope.$watch('region', function restoreSelection(region) { - $scope.model = selectedTimeZone[region] || null; - }); - // Ensure corresponding region is selected $scope.$watch('model', function setModel(model) { $scope.region = timeZoneRegions[model] || ''; selectedTimeZone[$scope.region] = model; }); + // Restore time zone selection when region changes + $scope.$watch('region', function restoreSelection(region) { + $scope.model = selectedTimeZone[region] || null; + }); + }]); diff --git a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js index d5ba313d4..2241ce11a 100644 --- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js +++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js @@ -32,11 +32,12 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() scope: { /** - * The connection group to display. + * The connection groups to display as a map of data source + * identifier to corresponding root group. * - * @type ConnectionGroup|Object + * @type Object. */ - connectionGroup : '=', + connectionGroups : '=', /** * Arbitrary object which shall be made available to the connection @@ -92,43 +93,38 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() // Required services var activeConnectionService = $injector.get('activeConnectionService'); + var dataSourceService = $injector.get('dataSourceService'); // Required types var GroupListItem = $injector.get('GroupListItem'); /** - * The number of active connections associated with a given - * connection identifier. If this information is unknown, or there - * are no active connections for a given identifier, no number will - * be stored. + * Map of data source identifier to the number of active + * connections associated with a given connection identifier. + * If this information is unknown, or there are no active + * connections for a given identifier, no number will be stored. * - * @type Object. + * @type Object.> */ var connectionCount = {}; - // Count active connections by connection identifier - activeConnectionService.getActiveConnections() - .success(function activeConnectionsRetrieved(activeConnections) { - - // Count each active connection by identifier - angular.forEach(activeConnections, function addActiveConnection(activeConnection) { - - // If counter already exists, increment - var identifier = activeConnection.connectionIdentifier; - if (connectionCount[identifier]) - connectionCount[identifier]++; - - // Otherwise, initialize counter to 1 - else - connectionCount[identifier] = 1; - - }); - - }); + /** + * A list of all items which should appear at the root level. As + * connections and connection groups from multiple data sources may + * be included in a guacGroupList, there may be multiple root + * items, even if the root connection group is shown. + * + * @type GroupListItem[] + */ + $scope.rootItems = []; /** * Returns the number of active usages of a given connection. * + * @param {String} dataSource + * The identifier of the data source containing the given + * connection. + * * @param {Connection} connection * The connection whose active connections should be counted. * @@ -136,8 +132,8 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() * The number of currently-active usages of the given * connection. */ - var countActiveConnections = function countActiveConnections(connection) { - return connectionCount[connection.identifier]; + var countActiveConnections = function countActiveConnections(dataSource, connection) { + return connectionCount[dataSource][connection.identifier]; }; /** @@ -173,29 +169,66 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() }; // Set contents whenever the connection group is assigned or changed - $scope.$watch("connectionGroup", function setContents(connectionGroup) { + $scope.$watch('connectionGroups', function setContents(connectionGroups) { - if (connectionGroup) { + // Reset stored data + var dataSources = []; + $scope.rootItems = []; + connectionCount = {}; - // Create item hierarchy, including connections only if they will be visible - var rootItem = GroupListItem.fromConnectionGroup(connectionGroup, - !!$scope.connectionTemplate, countActiveConnections); + // If connection groups are given, add them to the interface + if (connectionGroups) { - // If root group is to be shown, wrap that group as the child of a fake root group - if ($scope.showRootGroup) - $scope.rootItem = new GroupListItem({ - isConnectionGroup : true, - isBalancing : false, - children : [ rootItem ] + // Add each provided connection group + angular.forEach(connectionGroups, function addConnectionGroup(connectionGroup, dataSource) { + + // Prepare data source for active connection counting + dataSources.push(dataSource); + connectionCount[dataSource] = {}; + + // Create root item for current connection group + var rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, + !!$scope.connectionTemplate, countActiveConnections); + + // If root group is to be shown, add it as a root item + if ($scope.showRootGroup) + $scope.rootItems.push(rootItem); + + // Otherwise, add its children as root items + else { + angular.forEach(rootItem.children, function addRootItem(child) { + $scope.rootItems.push(child); + }); + } + + }); + + // Count active connections by connection identifier + dataSourceService.apply( + activeConnectionService.getActiveConnections, + dataSources + ) + .then(function activeConnectionsRetrieved(activeConnectionMap) { + + // Within each data source, count each active connection by identifier + angular.forEach(activeConnectionMap, function addActiveConnections(activeConnections, dataSource) { + angular.forEach(activeConnections, function addActiveConnection(activeConnection) { + + // If counter already exists, increment + var identifier = activeConnection.connectionIdentifier; + if (connectionCount[dataSource][identifier]) + connectionCount[dataSource][identifier]++; + + // Otherwise, initialize counter to 1 + else + connectionCount[dataSource][identifier] = 1; + + }); }); - // If not wrapped, only the descendants of the root will be shown - else - $scope.rootItem = rootItem; + }); } - else - $scope.rootItem = null; }); diff --git a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html index ae1ef679f..9059ab633 100644 --- a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html +++ b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html @@ -57,7 +57,7 @@ - diff --git a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js index aa9a0f0de..09d639082 100644 --- a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js +++ b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js @@ -39,6 +39,14 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Use empty object by default template = template || {}; + /** + * The identifier of the data source associated with the connection or + * connection group this item represents. + * + * @type String + */ + this.dataSource = template.dataSource; + /** * The unique identifier associated with the connection or connection * group this item represents. @@ -124,6 +132,10 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio /** * Creates a new GroupListItem using the contents of the given connection. * + * @param {String} dataSource + * The identifier of the data source containing the given connection + * group. + * * @param {ConnectionGroup} connection * The connection whose contents should be represented by the new * GroupListItem. @@ -131,12 +143,15 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * @param {Function} [countActiveConnections] * A getter which returns the current number of active connections for * the given connection. If omitted, the number of active connections - * known at the time this function was called is used instead. + * known at the time this function was called is used instead. This + * function will be passed, in order, the data source identifier and + * the connection in question. * * @returns {GroupListItem} * A new GroupListItem which represents the given connection. */ - GroupListItem.fromConnection = function fromConnection(connection, countActiveConnections) { + GroupListItem.fromConnection = function fromConnection(dataSource, + connection, countActiveConnections) { // Return item representing the given connection return new GroupListItem({ @@ -145,6 +160,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio name : connection.name, identifier : connection.identifier, protocol : connection.protocol, + dataSource : dataSource, // Type information isConnection : true, @@ -155,7 +171,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Use getter, if provided if (countActiveConnections) - return countActiveConnections(connection); + return countActiveConnections(dataSource, connection); return connection.activeConnections; @@ -172,6 +188,10 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * Creates a new GroupListItem using the contents and descendants of the * given connection group. * + * @param {String} dataSource + * The identifier of the data source containing the given connection + * group. + * * @param {ConnectionGroup} connectionGroup * The connection group whose contents and descendants should be * represented by the new GroupListItem and its descendants. @@ -183,34 +203,41 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * @param {Function} [countActiveConnections] * A getter which returns the current number of active connections for * the given connection. If omitted, the number of active connections - * known at the time this function was called is used instead. + * known at the time this function was called is used instead. This + * function will be passed, in order, the data source identifier and + * the connection group in question. * * @param {Function} [countActiveConnectionGroups] * A getter which returns the current number of active connections for * the given connection group. If omitted, the number of active * connections known at the time this function was called is used - * instead. + * instead. This function will be passed, in order, the data source + * identifier and the connection group in question. * * @returns {GroupListItem} * A new GroupListItem which represents the given connection group, * including all descendants. */ - GroupListItem.fromConnectionGroup = function fromConnectionGroup(connectionGroup, - includeConnections, countActiveConnections, countActiveConnectionGroups) { + GroupListItem.fromConnectionGroup = function fromConnectionGroup(dataSource, + connectionGroup, includeConnections, countActiveConnections, + countActiveConnectionGroups) { var children = []; // Add any child connections if (connectionGroup.childConnections && includeConnections !== false) { connectionGroup.childConnections.forEach(function addChildConnection(child) { - children.push(GroupListItem.fromConnection(child, countActiveConnections)); + children.push(GroupListItem.fromConnection(dataSource, child, + countActiveConnections)); }); } // Add any child groups if (connectionGroup.childConnectionGroups) { connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { - children.push(GroupListItem.fromConnectionGroup(child, includeConnections, countActiveConnections, countActiveConnectionGroups)); + children.push(GroupListItem.fromConnectionGroup(dataSource, + child, includeConnections, countActiveConnections, + countActiveConnectionGroups)); }); } @@ -220,6 +247,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Identifying information name : connectionGroup.name, identifier : connectionGroup.identifier, + dataSource : dataSource, // Type information isConnection : false, @@ -234,7 +262,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Use getter, if provided if (countActiveConnectionGroups) - return countActiveConnectionGroups(connectionGroup); + return countActiveConnectionGroups(dataSource, connectionGroup); return connectionGroup.activeConnections; diff --git a/guacamole/src/main/webapp/app/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js index 89f88e25a..12eb1c25e 100644 --- a/guacamole/src/main/webapp/app/home/controllers/homeController.js +++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js @@ -27,19 +27,22 @@ angular.module('home').controller('homeController', ['$scope', '$injector', function homeController($scope, $injector) { // Get required types - var ConnectionGroup = $injector.get("ConnectionGroup"); + var ConnectionGroup = $injector.get('ConnectionGroup'); + var ClientIdentifier = $injector.get('ClientIdentifier'); // Get required services - var authenticationService = $injector.get("authenticationService"); - var connectionGroupService = $injector.get("connectionGroupService"); - + var authenticationService = $injector.get('authenticationService'); + var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); + /** - * The root connection group, or null if the connection group hierarchy has - * not yet been loaded. + * Map of data source identifier to the root connection group of that data + * source, or null if the connection group hierarchy has not yet been + * loaded. * - * @type ConnectionGroup + * @type Object. */ - $scope.rootConnectionGroup = null; + $scope.rootConnectionGroups = null; /** * Returns whether critical data has completed being loaded. @@ -54,10 +57,59 @@ angular.module('home').controller('homeController', ['$scope', '$injector', }; - // Retrieve root group and all descendants - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) - .success(function rootGroupRetrieved(rootConnectionGroup) { - $scope.rootConnectionGroup = rootConnectionGroup; + /** + * Object passed to the guacGroupList directive, providing context-specific + * functions or data. + */ + $scope.context = { + + /** + * Returns the unique string identifier which must be used when + * connecting to a connection or connection group represented by the + * given GroupListItem. + * + * @param {GroupListItem} item + * The GroupListItem to determine the client identifier of. + * + * @returns {String} + * The client identifier associated with the connection or + * connection group represented by the given GroupListItem, or null + * if the GroupListItem cannot have an associated client + * identifier. + */ + getClientIdentifier : function getClientIdentifier(item) { + + // If the item is a connection, generate a connection identifier + if (item.isConnection) + return ClientIdentifier.toString({ + dataSource : item.dataSource, + type : ClientIdentifier.Types.CONNECTION, + id : item.identifier + }); + + // If the item is a connection, generate a connection group identifier + if (item.isConnectionGroup) + return ClientIdentifier.toString({ + dataSource : item.dataSource, + type : ClientIdentifier.Types.CONNECTION_GROUP, + id : item.identifier + }); + + // Otherwise, no such identifier can exist + return null; + + } + + }; + + // Retrieve root groups and all descendants + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function rootGroupsRetrieved(rootConnectionGroups) { + $scope.rootConnectionGroups = rootConnectionGroups; }); }]); diff --git a/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js b/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js index 5dddd9717..a5d913a02 100644 --- a/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js +++ b/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js @@ -31,13 +31,15 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo scope: { /** - * The root connection group, and all visible descendants. - * Recent connections will only be shown if they exist within this - * hierarchy, regardless of their existence within the history. + * The root connection groups to display, and all visible + * descendants, as a map of data source identifier to the root + * connection group within that data source. Recent connections + * will only be shown if they exist within this hierarchy, + * regardless of their existence within the history. * - * @type ConnectionGroup + * @type Object. */ - rootGroup : '=' + rootGroups : '=' }, @@ -46,6 +48,7 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo // Required types var ActiveConnection = $injector.get('ActiveConnection'); + var ClientIdentifier = $injector.get('ClientIdentifier'); var RecentConnection = $injector.get('RecentConnection'); // Required services @@ -91,42 +94,62 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo /** * Adds the given connection to the internal set of visible * objects. - * + * + * @param {String} dataSource + * The identifier of the data source associated with the + * given connection group. + * * @param {Connection} connection * The connection to add to the internal set of visible objects. */ - var addVisibleConnection = function addVisibleConnection(connection) { + var addVisibleConnection = function addVisibleConnection(dataSource, connection) { // Add given connection to set of visible objects - visibleObjects['c/' + connection.identifier] = connection; + visibleObjects[ClientIdentifier.toString({ + dataSource : dataSource, + type : ClientIdentifier.Types.CONNECTION, + id : connection.identifier + })] = connection; }; /** * Adds the given connection group to the internal set of visible * objects, along with any descendants. - * + * + * @param {String} dataSource + * The identifier of the data source associated with the + * given connection group. + * * @param {ConnectionGroup} connectionGroup * The connection group to add to the internal set of visible * objects, along with any descendants. */ - var addVisibleConnectionGroup = function addVisibleConnectionGroup(connectionGroup) { + var addVisibleConnectionGroup = function addVisibleConnectionGroup(dataSource, connectionGroup) { // Add given connection group to set of visible objects - visibleObjects['g/' + connectionGroup.identifier] = connectionGroup; + visibleObjects[ClientIdentifier.toString({ + dataSource : dataSource, + type : ClientIdentifier.Types.CONNECTION_GROUP, + id : connectionGroup.identifier + })] = connectionGroup; // Add all child connections if (connectionGroup.childConnections) - connectionGroup.childConnections.forEach(addVisibleConnection); + connectionGroup.childConnections.forEach(function addChildConnection(child) { + addVisibleConnection(dataSource, child); + }); // Add all child connection groups if (connectionGroup.childConnectionGroups) - connectionGroup.childConnectionGroups.forEach(addVisibleConnectionGroup); + connectionGroup.childConnectionGroups.forEach(function addChildConnectionGroup(child) { + addVisibleConnectionGroup(dataSource, child); + }); }; - // Update visible objects when root group is set - $scope.$watch("rootGroup", function setRootGroup(rootGroup) { + // Update visible objects when root groups are set + $scope.$watch("rootGroups", function setRootGroups(rootGroups) { // Clear connection arrays $scope.activeConnections = []; @@ -134,8 +157,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo // Produce collection of visible objects visibleObjects = {}; - if (rootGroup) - addVisibleConnectionGroup(rootGroup); + if (rootGroups) { + angular.forEach(rootGroups, function addConnectionGroup(rootGroup, dataSource) { + addVisibleConnectionGroup(dataSource, rootGroup); + }); + } var managedClients = guacClientManager.getManagedClients(); diff --git a/guacamole/src/main/webapp/app/home/templates/connection.html b/guacamole/src/main/webapp/app/home/templates/connection.html index 33c5aec7e..87f101af7 100644 --- a/guacamole/src/main/webapp/app/home/templates/connection.html +++ b/guacamole/src/main/webapp/app/home/templates/connection.html @@ -1,4 +1,4 @@ - + - {{item.name}} + {{item.name}} {{item.name}} diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html index be41ada77..a2c362ac5 100644 --- a/guacamole/src/main/webapp/app/home/templates/home.html +++ b/guacamole/src/main/webapp/app/home/templates/home.html @@ -30,14 +30,15 @@
- +

{{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}

diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js index ad350a1ef..39808ea22 100644 --- a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js @@ -123,7 +123,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Management screen - .when('/settings/:tab', { + .when('/settings/:dataSource?/:tab', { title : 'APP.NAME', bodyClassName : 'settings', templateUrl : 'app/settings/templates/settings.html', @@ -132,7 +132,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Connection editor - .when('/manage/connections/:id?', { + .when('/manage/:dataSource/connections/:id?', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageConnection.html', @@ -141,7 +141,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Connection group editor - .when('/manage/connectionGroups/:id?', { + .when('/manage/:dataSource/connectionGroups/:id?', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageConnectionGroup.html', @@ -150,7 +150,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // User editor - .when('/manage/users/:id', { + .when('/manage/:dataSource/users/:id', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageUser.html', @@ -159,7 +159,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Client view - .when('/client/:type/:id/:params?', { + .when('/client/:id/:params?', { bodyClassName : 'client', templateUrl : 'app/client/templates/client.html', controller : 'clientController', diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js index 04e1066d4..1f1dc5ab5 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js @@ -55,7 +55,15 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i guacNotification.showStatus(false); } }; - + + /** + * The unique identifier of the data source containing the connection being + * edited. + * + * @type String + */ + var dataSource = $routeParams.dataSource; + /** * The identifier of the original connection from which this connection is * being cloned. Only valid if this is a new connection. @@ -178,20 +186,24 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }; // Pull connection attribute schema - schemaService.getConnectionAttributes().success(function attributesReceived(attributes) { + schemaService.getConnectionAttributes(dataSource) + .success(function attributesReceived(attributes) { $scope.attributes = attributes; }); // Pull connection group hierarchy - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.ADMINISTER]) + connectionGroupService.getConnectionGroupTree( + dataSource, + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.ADMINISTER] + ) .success(function connectionGroupReceived(rootGroup) { $scope.rootGroup = rootGroup; }); // Query the user's permissions for the current connection - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsReceived(permissions) { + permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername()) + .success(function permissionsReceived(permissions) { $scope.permissions = permissions; @@ -220,7 +232,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }); // Get protocol metadata - schemaService.getProtocols().success(function protocolsReceived(protocols) { + schemaService.getProtocols(dataSource) + .success(function protocolsReceived(protocols) { $scope.protocols = protocols; }); @@ -233,12 +246,14 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i if (identifier) { // Pull data from existing connection - connectionService.getConnection(identifier).success(function connectionRetrieved(connection) { + connectionService.getConnection(dataSource, identifier) + .success(function connectionRetrieved(connection) { $scope.connection = connection; }); // Pull connection history - connectionService.getConnectionHistory(identifier).success(function historyReceived(historyEntries) { + connectionService.getConnectionHistory(dataSource, identifier) + .success(function historyReceived(historyEntries) { // Wrap all history entries for sake of display $scope.historyEntryWrappers = []; @@ -249,7 +264,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }); // Pull connection parameters - connectionService.getConnectionParameters(identifier).success(function parametersReceived(parameters) { + connectionService.getConnectionParameters(dataSource, identifier) + .success(function parametersReceived(parameters) { $scope.parameters = parameters; }); } @@ -258,7 +274,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i else if (cloneSourceIdentifier) { // Pull data from cloned connection - connectionService.getConnection(cloneSourceIdentifier).success(function connectionRetrieved(connection) { + connectionService.getConnection(dataSource, cloneSourceIdentifier) + .success(function connectionRetrieved(connection) { $scope.connection = connection; // Clear the identifier field because this connection is new @@ -269,7 +286,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i $scope.historyEntryWrappers = []; // Pull connection parameters from cloned connection - connectionService.getConnectionParameters(cloneSourceIdentifier).success(function parametersReceived(parameters) { + connectionService.getConnectionParameters(dataSource, cloneSourceIdentifier) + .success(function parametersReceived(parameters) { $scope.parameters = parameters; }); } @@ -332,7 +350,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i * Cancels all pending edits, returning to the management page. */ $scope.cancel = function cancel() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }; /** @@ -340,7 +358,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i * which is prepopulated with the data from the connection currently being edited. */ $scope.cloneConnection = function cloneConnection() { - $location.path('/manage/connections').search('clone', identifier); + $location.path('/manage/' + encodeURIComponent(dataSource) + '/connections').search('clone', identifier); }; /** @@ -352,9 +370,9 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i $scope.connection.parameters = $scope.parameters; // Save the connection - connectionService.saveConnection($scope.connection) + connectionService.saveConnection(dataSource, $scope.connection) .success(function savedConnection() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }) // Notify of any errors @@ -402,9 +420,9 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i var deleteConnectionImmediately = function deleteConnectionImmediately() { // Delete the connection - connectionService.deleteConnection($scope.connection) + connectionService.deleteConnection(dataSource, $scope.connection) .success(function deletedConnection() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }) // Notify of any errors diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js index 64fb03b08..1de6c7127 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js @@ -51,6 +51,14 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' } }; + /** + * The unique identifier of the data source containing the connection group + * being edited. + * + * @type String + */ + var dataSource = $routeParams.dataSource; + /** * The identifier of the connection group being edited. If a new connection * group is being created, this will not be defined. @@ -123,13 +131,14 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' }; // Pull connection group attribute schema - schemaService.getConnectionGroupAttributes().success(function attributesReceived(attributes) { + schemaService.getConnectionGroupAttributes(dataSource) + .success(function attributesReceived(attributes) { $scope.attributes = attributes; }); // Query the user's permissions for the current connection group - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsReceived(permissions) { + permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername()) + .success(function permissionsReceived(permissions) { $scope.permissions = permissions; @@ -150,14 +159,19 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' // Pull connection group hierarchy - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) + connectionGroupService.getConnectionGroupTree( + dataSource, + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.ADMINISTER] + ) .success(function connectionGroupReceived(rootGroup) { $scope.rootGroup = rootGroup; }); // If we are editing an existing connection group, pull its data if (identifier) { - connectionGroupService.getConnectionGroup(identifier).success(function connectionGroupReceived(connectionGroup) { + connectionGroupService.getConnectionGroup(dataSource, identifier) + .success(function connectionGroupReceived(connectionGroup) { $scope.connectionGroup = connectionGroup; }); } @@ -187,7 +201,7 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' * Cancels all pending edits, returning to the management page. */ $scope.cancel = function cancel() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }; /** @@ -197,9 +211,9 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' $scope.saveConnectionGroup = function saveConnectionGroup() { // Save the connection - connectionGroupService.saveConnectionGroup($scope.connectionGroup) + connectionGroupService.saveConnectionGroup(dataSource, $scope.connectionGroup) .success(function savedConnectionGroup() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }) // Notify of any errors @@ -247,9 +261,9 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' var deleteConnectionGroupImmediately = function deleteConnectionGroupImmediately() { // Delete the connection group - connectionGroupService.deleteConnectionGroup($scope.connectionGroup) + connectionGroupService.deleteConnectionGroup(dataSource, $scope.connectionGroup) .success(function deletedConnectionGroup() { - $location.path('/settings/connections'); + $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections'); }) // Notify of any errors diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 00c09c5e1..6b927bdf6 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 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 @@ -28,18 +28,23 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); + var PageDefinition = $injector.get('PageDefinition'); var PermissionFlagSet = $injector.get('PermissionFlagSet'); var PermissionSet = $injector.get('PermissionSet'); + var User = $injector.get('User'); // Required services - var $location = $injector.get('$location'); - var $routeParams = $injector.get('$routeParams'); - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var guacNotification = $injector.get('guacNotification'); - var permissionService = $injector.get('permissionService'); - var schemaService = $injector.get('schemaService'); - var userService = $injector.get('userService'); + var $location = $injector.get('$location'); + var $routeParams = $injector.get('$routeParams'); + var $q = $injector.get('$q'); + var authenticationService = $injector.get('authenticationService'); + var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); + var guacNotification = $injector.get('guacNotification'); + var permissionService = $injector.get('permissionService'); + var schemaService = $injector.get('schemaService'); + var translationStringService = $injector.get('translationStringService'); + var userService = $injector.get('userService'); /** * An action to be provided along with the object sent to showStatus which @@ -53,6 +58,29 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto } }; + /** + * The identifiers of all data sources currently available to the + * authenticated user. + * + * @type String[] + */ + var dataSources = authenticationService.getAvailableDataSources(); + + /** + * The username of the current, authenticated user. + * + * @type String + */ + var currentUsername = authenticationService.getCurrentUsername(); + + /** + * The unique identifier of the data source containing the user being + * edited. + * + * @type String + */ + var dataSource = $routeParams.dataSource; + /** * The username of the user being edited. * @@ -60,6 +88,16 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ var username = $routeParams.id; + /** + * Whether the user being modified actually exists. If the user does not + * yet exist, a different REST service call must be made to create that + * user rather than update an existing user. If the user has not yet been + * loaded, this will be null. + * + * @type Boolean + */ + var userExists = null; + /** * The user being modified. * @@ -75,26 +113,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.permissionFlags = null; /** - * The root connection group of the connection group hierarchy. + * A map of data source identifiers to the root connection groups within + * thost data sources. As only one data source is applicable to any one + * user being edited/created, this will only contain a single key. * - * @type ConnectionGroup + * @type Object. */ - $scope.rootGroup = null; + $scope.rootGroups = null; - /** - * Whether the authenticated user has UPDATE permission for the user being edited. - * - * @type Boolean - */ - $scope.hasUpdatePermission = null; - - /** - * Whether the authenticated user has DELETE permission for the user being edited. - * - * @type Boolean - */ - $scope.hasDeletePermission = null; - /** * All permissions associated with the current user, or null if the user's * permissions have not yet been loaded. @@ -112,6 +138,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ $scope.attributes = null; + /** + * The pages associated with each user account having the given username. + * Each user account will be associated with a particular data source. + * + * @type PageDefinition[] + */ + $scope.accountPages = []; + /** * Returns whether critical data has completed being loaded. * @@ -123,54 +157,227 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return $scope.user !== null && $scope.permissionFlags !== null - && $scope.rootGroup !== null + && $scope.rootGroups !== null && $scope.permissions !== null - && $scope.attributes !== null - && $scope.canSaveUser !== null - && $scope.canDeleteUser !== null; + && $scope.attributes !== null; }; + /** + * Returns whether the current user can change attributes associated with + * the user being edited. + * + * @returns {Boolean} + * true if the current user can change attributes associated with the + * user being edited, false otherwise. + */ + $scope.canChangeAttributes = function canChangeAttributes() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Attributes can always be set if we are creating the user + if (!userExists) + return true; + + // The administrator can always change attributes + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, can change attributes if we have permission to update this user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.UPDATE, username); + + }; + + /** + * Returns whether the current user can change permissions of any kind + * which are associated with the user being edited. + * + * @returns {Boolean} + * true if the current user can grant or revoke permissions of any kind + * which are associated with the user being edited, false otherwise. + */ + $scope.canChangePermissions = function canChangePermissions() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Permissions can always be set if we are creating the user + if (!userExists) + return true; + + // The administrator can always modify permissions + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, can only modify permissions if we have explicit + // ADMINISTER permission + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.ADMINISTER, username); + + }; + + /** + * Returns whether the current user can change the system permissions + * granted to the user being edited. + * + * @returns {Boolean} + * true if the current user can grant or revoke system permissions to + * the user being edited, false otherwise. + */ + $scope.canChangeSystemPermissions = function canChangeSystemPermissions() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Only the administrator can modify system permissions + return PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER); + + }; + + /** + * Returns whether the current user can save the user being edited. Saving + * will create or update that user depending on whether the user already + * exists. + * + * @returns {Boolean} + * true if the current user can save changes to the user being edited, + * false otherwise. + */ + $scope.canSaveUser = function canSaveUser() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // The administrator can always save users + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // If user does not exist, can only save if we have permission to create users + if (!userExists) + return PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.CREATE_USER); + + // Otherwise, can only save if we have permission to update this user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.UPDATE, username); + + }; + + /** + * Returns whether the current user can delete the user being edited. + * + * @returns {Boolean} + * true if the current user can delete the user being edited, false + * otherwise. + */ + $scope.canDeleteUser = function canDeleteUser() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Can't delete what doesn't exist + if (!userExists) + return false; + + // The administrator can always delete users + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, require explicit DELETE permission on the user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.DELETE, username); + + }; + + /** + * Returns whether the user being edited is read-only, and thus cannot be + * modified by the current user. + * + * @returns {Boolean} + * true if the user being edited is actually read-only and cannot be + * edited at all, false otherwise. + */ + $scope.isReadOnly = function isReadOnly() { + return !$scope.canSaveUser(); + }; + // Pull user attribute schema - schemaService.getUserAttributes().success(function attributesReceived(attributes) { + schemaService.getUserAttributes(dataSource).success(function attributesReceived(attributes) { $scope.attributes = attributes; }); // Pull user data - userService.getUser(username).success(function userReceived(user) { - $scope.user = user; + dataSourceService.apply(userService.getUser, dataSources, username) + .then(function usersReceived(users) { + + // Get user for currently-selected data source + $scope.user = users[dataSource]; + + // Create skeleton user if user does not exist + if (!$scope.user) { + userExists = false; + $scope.user = new User({ + 'username' : username + }); + } + else + userExists = true; + + // Generate pages for each applicable data source + $scope.accountPages = []; + angular.forEach(dataSources, function addAccountPage(dataSource) { + + // Determine whether data source contains this user + var linked = dataSource in users; + + // Add page entry + $scope.accountPages.push(new PageDefinition({ + name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + url : '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), + className : linked ? 'linked' : 'unlinked' + })); + + }); + }); // Pull user permissions - permissionService.getPermissions(username).success(function gotPermissions(permissions) { + permissionService.getPermissions(dataSource, username).success(function gotPermissions(permissions) { $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); + }) + + // If permissions cannot be retrieved, use empty permissions + .error(function permissionRetrievalFailed() { + $scope.permissionFlags = new PermissionFlagSet(); }); // Retrieve all connections for which we have ADMINISTER permission - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) - .success(function connectionGroupReceived(rootGroup) { - $scope.rootGroup = rootGroup; + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + [dataSource], + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.ADMINISTER] + ) + .then(function connectionGroupReceived(rootGroups) { + $scope.rootGroups = rootGroups; }); - // Query the user's permissions for the current connection - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsReceived(permissions) { - + // Query the user's permissions for the current user + permissionService.getPermissions(dataSource, currentUsername) + .success(function permissionsReceived(permissions) { $scope.permissions = permissions; - - // Check if the user is new or if the user has UPDATE permission - $scope.canSaveUser = - !username - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, username); - - // Check if user is not new and the user has DELETE permission - $scope.canDeleteUser = - !!username && ( - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, username) - ); - }); /** @@ -507,12 +714,17 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return; } - // Save the user - userService.saveUser($scope.user) - .success(function savedUser() { + // Save or create the user, depending on whether the user exists + var saveUserPromise; + if (userExists) + saveUserPromise = userService.saveUser(dataSource, $scope.user); + else + saveUserPromise = userService.createUser(dataSource, $scope.user); + + saveUserPromise.success(function savedUser() { // Upon success, save any changed permissions - permissionService.patchPermissions($scope.user.username, permissionsAdded, permissionsRemoved) + permissionService.patchPermissions(dataSource, $scope.user.username, permissionsAdded, permissionsRemoved) .success(function patchedUserPermissions() { $location.path('/settings/users'); }) @@ -574,7 +786,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var deleteUserImmediately = function deleteUserImmediately() { // Delete the user - userService.deleteUser($scope.user) + userService.deleteUser(dataSource, $scope.user) .success(function deletedUser() { $location.path('/settings/users'); }) diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user.css b/guacamole/src/main/webapp/app/manage/styles/manage-user.css new file mode 100644 index 000000000..7d6f077db --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/styles/manage-user.css @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 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. + */ + +.manage-user .username.header { + margin-bottom: 0; +} + +.manage-user .username.header h2 { + text-transform: none; +} + +.manage-user .page-tabs .page-list li.unlinked a[href], +.manage-user .page-tabs .page-list li.linked a[href] { + padding-right: 2.5em; + position: relative; +} + +.manage-user .page-tabs .page-list li.unlinked a[href]:before, +.manage-user .page-tabs .page-list li.linked a[href]:before { + content: ' '; + position: absolute; + right: 0; + bottom: 0; + top: 0; + width: 2.5em; + background-size: 1.25em; + background-repeat: no-repeat; + background-position: center; +} + +.manage-user .page-tabs .page-list li.unlinked a[href]:before { + background-image: url('images/plus.png'); +} + +.manage-user .page-tabs .page-list li.unlinked a[href] { + opacity: 0.5; +} + +.manage-user .page-tabs .page-list li.unlinked a[href]:hover, +.manage-user .page-tabs .page-list li.unlinked a[href].current { + opacity: 1; +} + +.manage-user .page-tabs .page-list li.linked a[href]:before { + background-image: url('images/checkmark.png'); +} diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 4e0903f2d..71a6d93cf 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -1,5 +1,5 @@ -
+
- -
-

{{'MANAGE_USER.SECTION_HEADER_EDIT_USER' | translate}}

+ +
+

{{user.username}}

-
- - - - - - - - - - - - - - - - -
{{'MANAGE_USER.FIELD_HEADER_USERNAME' | translate}}{{user.username}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
+
+
- -
- + +
+

{{'MANAGE_USER.INFO_READ_ONLY' | translate}}

- -

{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

-
- - - - - - - - - -
{{systemPermissionType.label | translate}}
{{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}
-
- -

{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

-
- + +
+ + +
+ + + + + + + + + +
{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
+
+ + +
+ +
+ + +
+

{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

+
+ + + + + + + + + +
{{systemPermissionType.label | translate}}
{{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}
+
+
+ + +
+

{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

+
+ +
+
+
- + - +
diff --git a/guacamole/src/main/webapp/app/manage/types/ManageableUser.js b/guacamole/src/main/webapp/app/manage/types/ManageableUser.js new file mode 100644 index 000000000..8f2e43aef --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/types/ManageableUser.js @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 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. + */ + +/** + * A service for defining the ManageableUser class. + */ +angular.module('manage').factory('ManageableUser', [function defineManageableUser() { + + /** + * A pairing of an @link{User} with the identifier of its corresponding + * data source. + * + * @constructor + * @param {Object|ManageableUser} template + */ + var ManageableUser = function ManageableUser(template) { + + /** + * The unique identifier of the data source containing this user. + * + * @type String + */ + this.dataSource = template.dataSource; + + /** + * The @link{User} object represented by this ManageableUser and + * contained within the associated data source. + * + * @type User + */ + this.user = template.user; + + }; + + return ManageableUser; + +}]); diff --git a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js index bd5f909c5..ef2b05e61 100644 --- a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js +++ b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js @@ -33,7 +33,7 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() /** * The array of pages to display. * - * @type Page[] + * @type PageDefinition[] */ pages : '=' @@ -42,13 +42,119 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() templateUrl: 'app/navigation/templates/guacPageList.html', controller: ['$scope', '$injector', function guacPageListController($scope, $injector) { - // Get required services + // Required types + var PageDefinition = $injector.get('PageDefinition'); + + // Required services var $location = $injector.get('$location'); + /** + * The URL of the currently-displayed page. + * + * @type String + */ + var currentURL = $location.url(); + + /** + * The names associated with the current page, if the current page + * is known. The value of this property corresponds to the value of + * PageDefinition.name. Though PageDefinition.name may be a String, + * this will always be an Array. + * + * @type String[] + */ + var currentPageName = []; + + /** + * Array of each level of the page list, where a level is defined + * by a mapping of names (translation strings) to the + * PageDefinitions corresponding to those names. + * + * @type Object.[] + */ + $scope.levels = []; + + /** + * Returns the names associated with the given page, in + * hierarchical order. If the page is only associated with a single + * name, and that name is not stored as an array, it will be still + * be returned as an array containing a single item. + * + * @param {PageDefinition} page + * The page to return the names of. + * + * @return {String[]} + * An array of all names associated with the given page, in + * hierarchical order. + */ + var getPageNames = function getPageNames(page) { + + // If already an array, simply return the name + if (angular.isArray(page.name)) + return page.name; + + // Otherwise, transform into array + return [page.name]; + + }; + + /** + * Adds the given PageDefinition to the overall set of pages + * displayed by this guacPageList, automatically updating the + * available levels ($scope.levels) and the contents of those + * levels. + * + * @param {PageDefinition} page + * The PageDefinition to add. + * + * @param {Number} weight + * The sorting weight to use for the page if it does not + * already have an associated weight. + */ + var addPage = function addPage(page, weight) { + + // Pull all names for page + var names = getPageNames(page); + + // Copy the hierarchy of this page into the displayed levels + // as far as is relevant for the currently-displayed page + for (var i = 0; i < names.length; i++) { + + // Create current level, if it doesn't yet exist + var pages = $scope.levels[i]; + if (!pages) + pages = $scope.levels[i] = {}; + + // Get the name at the current level + var name = names[i]; + + // Determine whether this page definition is part of the + // hierarchy containing the current page + var isCurrentPage = (currentPageName[i] === name); + + // Store new page if it doesn't yet exist at this level + if (!pages[name]) { + pages[name] = new PageDefinition({ + name : name, + url : isCurrentPage ? currentURL : page.url, + className : page.className, + weight : page.weight || weight + }); + } + + // If the name at this level no longer matches the + // hierarchy of the current page, do not go any deeper + if (currentPageName[i] !== name) + break; + + } + + }; + /** * Navigate to the given page. * - * @param {Page} page + * @param {PageDefinition} page * The page to navigate to. */ $scope.navigateToPage = function navigateToPage(page) { @@ -58,16 +164,85 @@ angular.module('navigation').directive('guacPageList', [function guacPageList() /** * Tests whether the given page is the page currently being viewed. * - * @param {Page} page + * @param {PageDefinition} page * The page to test. * * @returns {Boolean} * true if the given page is the current page, false otherwise. */ $scope.isCurrentPage = function isCurrentPage(page) { - return $location.url() === page.url; + return currentURL === page.url; }; + /** + * Given an arbitrary map of PageDefinitions, returns an array of + * those PageDefinitions, sorted by weight. + * + * @param {Object.<*, PageDefinition>} level + * A map of PageDefinitions with arbitrary keys. The value of + * each key is ignored. + * + * @returns {PageDefinition[]} + * An array of all PageDefinitions in the given map, sorted by + * weight. + */ + $scope.getPages = function getPages(level) { + + var pages = []; + + // Convert contents of level to a flat array of pages + angular.forEach(level, function addPageFromLevel(page) { + pages.push(page); + }); + + // Sort page array by weight + pages.sort(function comparePages(a, b) { + return a.weight - b.weight; + }); + + return pages; + + }; + + // Update page levels whenever pages changes + $scope.$watch('pages', function setPages(pages) { + + // Determine current page name + currentPageName = []; + angular.forEach(pages, function findCurrentPageName(page) { + + // If page is current page, store its names + if ($scope.isCurrentPage(page)) + currentPageName = getPageNames(page); + + }); + + // Reset contents of levels + $scope.levels = []; + + // Add all page definitions + angular.forEach(pages, addPage); + + // Filter to only relevant levels + $scope.levels = $scope.levels.filter(function isRelevant(level) { + + // Determine relevancy by counting the number of pages + var pageCount = 0; + for (var name in level) { + + // Level is relevant if it has two or more pages + if (++pageCount === 2) + return true; + + } + + // Otherwise, the level is not relevant + return false; + + }); + + }); + }] // end controller }; diff --git a/guacamole/src/main/webapp/app/navigation/navigationModule.js b/guacamole/src/main/webapp/app/navigation/navigationModule.js index 06039e118..8e3964a4c 100644 --- a/guacamole/src/main/webapp/app/navigation/navigationModule.js +++ b/guacamole/src/main/webapp/app/navigation/navigationModule.js @@ -23,4 +23,8 @@ /** * Module for generating and implementing user navigation options. */ -angular.module('navigation', ['notification', 'rest']); +angular.module('navigation', [ + 'auth', + 'notification', + 'rest' +]); diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js index 9489581b0..27878ceb8 100644 --- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js +++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js @@ -27,91 +27,104 @@ angular.module('navigation').factory('userPageService', ['$injector', function userPageService($injector) { // Get required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - var PermissionSet = $injector.get('PermissionSet'); + var ClientIdentifier = $injector.get('ClientIdentifier'); + var ConnectionGroup = $injector.get('ConnectionGroup'); + var PageDefinition = $injector.get('PageDefinition'); + var PermissionSet = $injector.get('PermissionSet'); // Get required services - var $q = $injector.get('$q'); - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get("connectionGroupService"); - var permissionService = $injector.get("permissionService"); + var $q = $injector.get('$q'); + var authenticationService = $injector.get('authenticationService'); + var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); + var permissionService = $injector.get('permissionService'); + var translationStringService = $injector.get('translationStringService'); var service = {}; - /** - * Construct a new Page object with the given name and url. - * @constructor - * - * @param {String} name - * The i18n key for the name of the page. - * - * @param {String} url - * The url to the page. - * - * @returns {PageDefinition} - * The newly created PageDefinition object. - */ - var Page = function Page(name, url) { - this.name = name; - this.url = url; - }; - /** * The home page to assign to a user if they can navigate to more than one * page. * - * @type Page + * @type PageDefinition */ - var SYSTEM_HOME_PAGE = new Page( - 'USER_MENU.ACTION_NAVIGATE_HOME', - '/' - ); + var SYSTEM_HOME_PAGE = new PageDefinition({ + name : 'USER_MENU.ACTION_NAVIGATE_HOME', + url : '/' + }); /** * Returns an appropriate home page for the current user. * - * @param {ConnectionGroup} rootGroup - * The root of the connection group tree for the current user. + * @param {Object.} rootGroups + * A map of all root connection groups visible to the current user, + * where each key is the identifier of the corresponding data source. * - * @returns {Page} + * @returns {PageDefinition} * The user's home page. */ - var generateHomePage = function generateHomePage(rootGroup) { + var generateHomePage = function generateHomePage(rootGroups) { - // Get children - var connections = rootGroup.childConnections || []; - var connectionGroups = rootGroup.childConnectionGroups || []; + var homePage = null; - // Use main connection list screen as home if multiple connections - // are available - if (connections.length + connectionGroups.length === 1) { + // Determine whether a connection or balancing group should serve as + // the home page + for (var dataSource in rootGroups) { - var connection = connections[0]; - var connectionGroup = connectionGroups[0]; + // Get corresponding root group + var rootGroup = rootGroups[dataSource]; + + // Get children + var connections = rootGroup.childConnections || []; + var connectionGroups = rootGroup.childConnectionGroups || []; + + // If exactly one connection or balancing group is available, use + // that as the home page + if (homePage === null && connections.length + connectionGroups.length === 1) { + + var connection = connections[0]; + var connectionGroup = connectionGroups[0]; + + // Only one connection present, use as home page + if (connection) { + homePage = new PageDefinition({ + name : connection.name, + url : '/client/' + ClientIdentifier.toString({ + dataSource : dataSource, + type : ClientIdentifier.Types.CONNECTION, + id : connection.identifier + }) + }); + } + + // Only one balancing group present, use as home page + if (connectionGroup + && connectionGroup.type === ConnectionGroup.Type.BALANCING + && _.isEmpty(connectionGroup.childConnections) + && _.isEmpty(connectionGroup.childConnectionGroups)) { + homePage = new PageDefinition({ + name : connectionGroup.name, + url : '/client/' + ClientIdentifier.toString({ + dataSource : dataSource, + type : ClientIdentifier.Types.CONNECTION_GROUP, + id : connectionGroup.identifier + }) + }); + } - // Only one connection present, use as home page - if (connection) { - return new Page( - connection.name, - '/client/c/' + connection.identifier - ); } - // Only one connection present, use as home page - if (connectionGroup - && connectionGroup.type === ConnectionGroup.Type.BALANCING - && _.isEmpty(connectionGroup.childConnections) - && _.isEmpty(connectionGroup.childConnectionGroups)) { - return new Page( - connectionGroup.name, - '/client/g/' + connectionGroup.identifier - ); + // Otherwise, a connection or balancing group cannot serve as the + // home page + else { + homePage = null; + break; } - } + } // end for each data source - // Resolve promise with default home page - return SYSTEM_HOME_PAGE; + // Use default home page if no other is available + return homePage || SYSTEM_HOME_PAGE; }; @@ -126,10 +139,14 @@ angular.module('navigation').factory('userPageService', ['$injector', var deferred = $q.defer(); - // Resolve promise using home page derived from root connection group - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) - .success(function rootConnectionGroupRetrieved(rootGroup) { - deferred.resolve(generateHomePage(rootGroup)); + // Resolve promise using home page derived from root connection groups + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function rootConnectionGroupsRetrieved(rootGroups) { + deferred.resolve(generateHomePage(rootGroups)); }); return deferred.promise; @@ -140,94 +157,115 @@ angular.module('navigation').factory('userPageService', ['$injector', * Returns all settings pages that the current user can visit. This can * include any of the various manage pages. * - * @param {PermissionSet} permissions - * The permissions for the current user. + * @param {Object.} permissionSets + * A map of all permissions granted to the current user, where each + * key is the identifier of the corresponding data source. * * @returns {Page[]} * An array of all settings pages that the current user can visit. */ - var generateSettingsPages = function generateSettingsPages(permissions) { + var generateSettingsPages = function generateSettingsPages(permissionSets) { var pages = []; - permissions = angular.copy(permissions); + var canManageUsers = []; + var canManageConnections = []; + var canManageSessions = []; - // Ignore permission to update root group - PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); + // Inspect the contents of each provided permission set + angular.forEach(permissionSets, function inspectPermissions(permissions, dataSource) { - // Ignore permission to update self - PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, authenticationService.getCurrentUsername()); + permissions = angular.copy(permissions); - // Determine whether the current user needs access to the user management UI - var canManageUsers = + // Ignore permission to update root group + PermissionSet.removeConnectionGroupPermission(permissions, + PermissionSet.ObjectPermissionType.UPDATE, + ConnectionGroup.ROOT_IDENTIFIER); - // System permissions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) + // Ignore permission to update self + PermissionSet.removeUserPermission(permissions, + PermissionSet.ObjectPermissionType.UPDATE, + authenticationService.getCurrentUsername()); - // Permission to update users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + // Determine whether the current user needs access to the user management UI + if ( + // System permissions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) - // Permission to delete users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + // Permission to update users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - // Permission to administer users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); + // Permission to delete users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - // Determine whether the current user needs access to the connection management UI - var canManageConnections = + // Permission to administer users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + ) + canManageUsers.push(dataSource); - // System permissions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP) + // Determine whether the current user needs access to the connection management UI + if ( + // System permissions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP) - // Permission to update connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + // Permission to update connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - // Permission to delete connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + // Permission to delete connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - // Permission to administer connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); + // Permission to administer connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + ) + canManageConnections.push(dataSource); - var canManageSessions = + // Determine whether the current user needs access to the session management UI + if ( + // A user must be a system administrator to manage sessions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + ) + canManageSessions.push(dataSource); - // A user must be a system administrator to manage sessions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER); + }); // If user can manage sessions, add link to sessions management page - if (canManageSessions) { - pages.push(new Page( - 'USER_MENU.ACTION_MANAGE_SESSIONS', - '/settings/sessions' - )); + if (canManageSessions.length) { + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_SESSIONS', + url : '/settings/sessions' + })); } // If user can manage users, add link to user management page - if (canManageUsers) { - pages.push(new Page( - 'USER_MENU.ACTION_MANAGE_USERS', - '/settings/users' - )); + if (canManageUsers.length) { + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_USERS', + url : '/settings/users' + })); } - // If user can manage connections, add link to connections management page - if (canManageConnections) { - pages.push(new Page( - 'USER_MENU.ACTION_MANAGE_CONNECTIONS', - '/settings/connections' - )); - } + // If user can manage connections, add links for connection management pages + angular.forEach(canManageConnections, function addConnectionManagementLink(dataSource) { + pages.push(new PageDefinition({ + name : [ + 'USER_MENU.ACTION_MANAGE_CONNECTIONS', + translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME' + ], + url : '/settings/' + encodeURIComponent(dataSource) + '/connections' + })); + }); // Add link to user preferences (always accessible) - pages.push(new Page( - 'USER_MENU.ACTION_MANAGE_PREFERENCES', - '/settings/preferences' - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_PREFERENCES', + url : '/settings/preferences' + })); return pages; }; @@ -245,10 +283,15 @@ angular.module('navigation').factory('userPageService', ['$injector', var deferred = $q.defer(); - // Retrieve current permissions, resolving main pages if possible + // Retrieve current permissions + dataSourceService.apply( + permissionService.getPermissions, + authenticationService.getAvailableDataSources(), + authenticationService.getCurrentUsername() + ) + // Resolve promise using settings pages derived from permissions - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsRetrieved(permissions) { + .then(function permissionsRetrieved(permissions) { deferred.resolve(generateSettingsPages(permissions)); }); @@ -261,21 +304,23 @@ angular.module('navigation').factory('userPageService', ['$injector', * include the home page, manage pages, etc. In the case that there are no * applicable pages of this sort, it may return a client page. * - * @param {ConnectionGroup} rootGroup - * The root of the connection group tree for the current user. + * @param {Object.} rootGroups + * A map of all root connection groups visible to the current user, + * where each key is the identifier of the corresponding data source. * - * @param {PermissionSet} permissions - * The permissions for the current user. + * @param {Object.} permissions + * A map of all permissions granted to the current user, where each + * key is the identifier of the corresponding data source. * * @returns {Page[]} * An array of all main pages that the current user can visit. */ - var generateMainPages = function generateMainPages(rootGroup, permissions) { + var generateMainPages = function generateMainPages(rootGroups, permissions) { var pages = []; // Get home page and settings pages - var homePage = generateHomePage(rootGroup); + var homePage = generateHomePage(rootGroups); var settingsPages = generateSettingsPages(permissions); // Only include the home page in the list of main pages if the user @@ -285,10 +330,10 @@ angular.module('navigation').factory('userPageService', ['$injector', // Add generic link to the first-available settings page if (settingsPages.length) { - pages.push(new Page( - 'USER_MENU.ACTION_MANAGE_SETTINGS', - settingsPages[0].url - )); + pages.push(new PageDefinition({ + name : 'USER_MENU.ACTION_MANAGE_SETTINGS', + url : settingsPages[0].url + })); } return pages; @@ -308,7 +353,7 @@ angular.module('navigation').factory('userPageService', ['$injector', var deferred = $q.defer(); - var rootGroup = null; + var rootGroups = null; var permissions = null; /** @@ -316,20 +361,30 @@ angular.module('navigation').factory('userPageService', ['$injector', * insufficient data is available, this function does nothing. */ var resolveMainPages = function resolveMainPages() { - if (rootGroup && permissions) - deferred.resolve(generateMainPages(rootGroup, permissions)); + if (rootGroups && permissions) + deferred.resolve(generateMainPages(rootGroups, permissions)); }; // Retrieve root group, resolving main pages if possible - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) - .success(function rootConnectionGroupRetrieved(retrievedRootGroup) { - rootGroup = retrievedRootGroup; + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function rootConnectionGroupsRetrieved(retrievedRootGroups) { + rootGroups = retrievedRootGroups; resolveMainPages(); }); - // Retrieve current permissions, resolving main pages if possible - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsRetrieved(retrievedPermissions) { + // Retrieve current permissions + dataSourceService.apply( + permissionService.getPermissions, + authenticationService.getAvailableDataSources(), + authenticationService.getCurrentUsername() + ) + + // Resolving main pages if possible + .then(function permissionsRetrieved(retrievedPermissions) { permissions = retrievedPermissions; resolveMainPages(); }); diff --git a/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css b/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css new file mode 100644 index 000000000..28b4de263 --- /dev/null +++ b/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 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. + */ + +.page-tabs .page-list ul { + margin: 0; + padding: 0; + background: rgba(0, 0, 0, 0.0125); + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.page-tabs .page-list ul + ul { + font-size: 0.75em; +} + +.page-tabs .page-list li { + display: inline-block; + list-style: none; +} + +.page-tabs .page-list li a[href] { + display: block; + color: black; + text-decoration: none; + padding: 0.75em 1em; +} + +.page-tabs .page-list li a[href]:visited { + color: black; +} + +.page-tabs .page-list li a[href]:hover { + background-color: #CDA; +} + +.page-tabs .page-list li a[href].current, +.page-tabs .page-list li a[href].current:hover { + background: rgba(0,0,0,0.3); + cursor: default; +} diff --git a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html index d303c8efc..2f3b8aab8 100644 --- a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html +++ b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html @@ -1,4 +1,4 @@ - +
diff --git a/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js b/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js new file mode 100644 index 000000000..028a21fa4 --- /dev/null +++ b/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2015 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. + */ + +/** + * Provides the ClientIdentifier class definition. + */ +angular.module('client').factory('ClientIdentifier', ['$injector', + function defineClientIdentifier($injector) { + + // Required services + var authenticationService = $injector.get('authenticationService'); + var $window = $injector.get('$window'); + + /** + * Object which uniquely identifies a particular connection or connection + * group within Guacamole. This object can be converted to/from a string to + * generate a guaranteed-unique, deterministic identifier for client URLs. + * + * @constructor + * @param {ClientIdentifier|Object} [template={}] + * The object whose properties should be copied within the new + * ClientIdentifier. + */ + var ClientIdentifier = function ClientIdentifier(template) { + + // Use empty object by default + template = template || {}; + + /** + * The identifier of the data source associated with the object to + * which the client will connect. This identifier will be the + * identifier of an AuthenticationProvider within the Guacamole web + * application. + * + * @type String + */ + this.dataSource = template.dataSource; + + /** + * The type of object to which the client will connect. Possible values + * are defined within ClientIdentifier.Types. + * + * @type String + */ + this.type = template.type; + + /** + * The unique identifier of the object to which the client will + * connect. + * + * @type String + */ + this.id = template.id; + + }; + + /** + * All possible ClientIdentifier types. + * + * @type Object. + */ + ClientIdentifier.Types = { + + /** + * The type string for a Guacamole connection. + * + * @type String + */ + CONNECTION : 'c', + + /** + * The type string for a Guacamole connection group. + * + * @type String + */ + CONNECTION_GROUP : 'g' + + }; + + /** + * Converts the given ClientIdentifier or ClientIdentifier-like object to + * a String representation. Any object having the same properties as + * ClientIdentifier may be used, but only those properties will be taken + * into account when producing the resulting String. + * + * @param {ClientIdentifier|Object} id + * The ClientIdentifier or ClientIdentifier-like object to convert to + * a String representation. + * + * @returns {String} + * A deterministic String representation of the given ClientIdentifier + * or ClientIdentifier-like object. + */ + ClientIdentifier.toString = function toString(id) { + return $window.btoa([ + id.id, + id.type, + id.dataSource + ].join('\0')); + }; + + /** + * Converts the given String into the corresponding ClientIdentifier. If + * the provided String is not a valid identifier, it will be interpreted + * as the identifier of a connection within the data source that + * authenticated the current user. + * + * @param {String} str + * The String to convert to a ClientIdentifier. + * + * @returns {ClientIdentifier} + * The ClientIdentifier represented by the given String. + */ + ClientIdentifier.fromString = function fromString(str) { + + try { + var values = $window.atob(str).split('\0'); + return new ClientIdentifier({ + id : values[0], + type : values[1], + dataSource : values[2] + }); + } + + // If the provided string is invalid, transform into a reasonable guess + catch (e) { + return new ClientIdentifier({ + id : str, + type : ClientIdentifier.Types.CONNECTION, + dataSource : authenticationService.getDataSource() || 'default' + }); + } + + }; + + return ClientIdentifier; + +}]); diff --git a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js new file mode 100644 index 000000000..ba702b014 --- /dev/null +++ b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 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. + */ + +/** + * Provides the PageDefinition class definition. + */ +angular.module('navigation').factory('PageDefinition', [function definePageDefinition() { + + /** + * Creates a new PageDefinition object which pairs the URL of a page with + * an arbitrary, human-readable name. + * + * @constructor + * @param {PageDefinition|Object} template + * The object whose properties should be copied within the new + * PageDefinition. + */ + var PageDefinition = function PageDefinition(template) { + + /** + * The the name of the page, which should be a translation table key. + * Alternatively, this may also be a list of names, where the final + * name represents the page and earlier names represent categorization. + * Those categorical names may be rendered hierarchically as a system + * of menus, tabs, etc. + * + * @type String|String[] + */ + this.name = template.name; + + /** + * The URL of the page. + * + * @type String + */ + this.url = template.url; + + /** + * The CSS class name to associate with this page, if any. This will be + * an empty string by default. + * + * @type String + */ + this.className = template.className || ''; + + /** + * A numeric value denoting the relative sort order when compared to + * other sibling PageDefinitions. If unspecified, sort order is + * determined by the system using the PageDefinition. + * + * @type Number + */ + this.weight = template.weight; + + }; + + return PageDefinition; + +}]); diff --git a/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js b/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js index a96ebdf3f..99428a91c 100644 --- a/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js +++ b/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js @@ -23,8 +23,13 @@ /** * Service for operating on active connections via the REST API. */ -angular.module('rest').factory('activeConnectionService', ['$http', 'authenticationService', - function activeConnectionService($http, authenticationService) { +angular.module('rest').factory('activeConnectionService', ['$injector', + function activeConnectionService($injector) { + + // Required services + var $http = $injector.get('$http'); + var $q = $injector.get('$q'); + var authenticationService = $injector.get('authenticationService'); var service = {}; @@ -39,13 +44,12 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat * result. If null, no filtering will be performed. Valid values are * listed within PermissionSet.ObjectType. * - * @returns {Promise.>} * A promise which will resolve with a map of @link{ActiveConnection} * objects, where each key is the identifier of the corresponding * active connection. */ - service.getActiveConnections = function getActiveConnections(permissionTypes) { + service.getActiveConnections = function getActiveConnections(dataSource, permissionTypes) { // Build HTTP parameters set var httpParameters = { @@ -59,12 +63,74 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat // Retrieve tunnels return $http({ method : 'GET', - url : 'api/activeConnections', + url : 'api/data/' + encodeURIComponent(dataSource) + '/activeConnections', params : httpParameters }); }; + /** + * Returns a promise which resolves with all active connections accessible + * by the current user, as a map of @link{ActiveConnection} maps, as would + * be returned by getActiveConnections(), grouped by the identifier of + * their corresponding data source. All given data sources are queried. If + * an error occurs while retrieving any ActiveConnection map, the promise + * will be rejected. + * + * @param {String[]} dataSources + * The unique identifier of the data sources containing the active + * connections to be retrieved. These identifiers correspond to + * AuthenticationProviders within the Guacamole web application. + * + * @param {String[]} [permissionTypes] + * The set of permissions to filter with. A user must have one or more + * of these permissions for an active connection to appear in the + * result. If null, no filtering will be performed. Valid values are + * listed within PermissionSet.ObjectType. + * + * @returns {Promise.>>} + * A promise which resolves with all active connections available to + * the current user, as a map of ActiveConnection maps, as would be + * returned by getActiveConnections(), grouped by the identifier of + * their corresponding data source. + */ + service.getAllActiveConnections = function getAllActiveConnections(dataSources, permissionTypes) { + + var deferred = $q.defer(); + + var activeConnectionRequests = []; + var activeConnectionMaps = {}; + + // Retrieve all active connections from all data sources + angular.forEach(dataSources, function retrieveActiveConnections(dataSource) { + activeConnectionRequests.push( + service.getActiveConnections(dataSource, permissionTypes) + .success(function activeConnectionsRetrieved(activeConnections) { + activeConnectionMaps[dataSource] = activeConnections; + }) + ); + }); + + // Resolve when all requests are completed + $q.all(activeConnectionRequests) + .then( + + // All requests completed successfully + function allActiveConnectionsRetrieved() { + deferred.resolve(userArrays); + }, + + // At least one request failed + function activeConnectionRetrievalFailed(e) { + deferred.reject(e); + } + + ); + + return deferred.promise; + + }; + /** * Makes a request to the REST API to delete the active connections having * the given identifiers, effectively disconnecting them, returning a @@ -77,7 +143,7 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteActiveConnections = function deleteActiveConnections(identifiers) { + service.deleteActiveConnections = function deleteActiveConnections(dataSource, identifiers) { // Build HTTP parameters set var httpParameters = { @@ -96,7 +162,7 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat // Perform active connection deletion via PATCH return $http({ method : 'PATCH', - url : 'api/activeConnections', + url : 'api/data/' + encodeURIComponent(dataSource) + '/activeConnections', params : httpParameters, data : activeConnectionPatch }); diff --git a/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js b/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js index d43c58f52..1d48c160d 100644 --- a/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js +++ b/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 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 @@ -28,6 +28,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', // Required services var $http = $injector.get('$http'); + var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); @@ -57,7 +58,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', * A promise which will resolve with a @link{ConnectionGroup} upon * success. */ - service.getConnectionGroupTree = function getConnectionGroupTree(connectionGroupID, permissionTypes) { + service.getConnectionGroupTree = function getConnectionGroupTree(dataSource, connectionGroupID, permissionTypes) { // Use the root connection group ID if no ID is passed in connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; @@ -75,12 +76,12 @@ angular.module('rest').factory('connectionGroupService', ['$injector', return $http({ cache : cacheService.connections, method : 'GET', - url : 'api/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree', params : httpParameters }); }; - + /** * Makes a request to the REST API to get an individual connection group, * returning a promise that provides the corresponding @@ -94,7 +95,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', * A promise which will resolve with a @link{ConnectionGroup} upon * success. */ - service.getConnectionGroup = function getConnectionGroup(connectionGroupID) { + service.getConnectionGroup = function getConnectionGroup(dataSource, connectionGroupID) { // Use the root connection group ID if no ID is passed in connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; @@ -108,7 +109,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', return $http({ cache : cacheService.connections, method : 'GET', - url : 'api/connectionGroups/' + encodeURIComponent(connectionGroupID), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID), params : httpParameters }); @@ -127,7 +128,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ - service.saveConnectionGroup = function saveConnectionGroup(connectionGroup) { + service.saveConnectionGroup = function saveConnectionGroup(dataSource, connectionGroup) { // Build HTTP parameters set var httpParameters = { @@ -138,7 +139,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', if (!connectionGroup.identifier) { return $http({ method : 'POST', - url : 'api/connectionGroups', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups', params : httpParameters, data : connectionGroup }) @@ -154,7 +155,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', else { return $http({ method : 'PUT', - url : 'api/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), params : httpParameters, data : connectionGroup }) @@ -177,7 +178,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteConnectionGroup = function deleteConnectionGroup(connectionGroup) { + service.deleteConnectionGroup = function deleteConnectionGroup(dataSource, connectionGroup) { // Build HTTP parameters set var httpParameters = { @@ -187,7 +188,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector', // Delete connection group return $http({ method : 'DELETE', - url : 'api/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), params : httpParameters }) diff --git a/guacamole/src/main/webapp/app/rest/services/connectionService.js b/guacamole/src/main/webapp/app/rest/services/connectionService.js index 2afba2489..cbf40eb81 100644 --- a/guacamole/src/main/webapp/app/rest/services/connectionService.js +++ b/guacamole/src/main/webapp/app/rest/services/connectionService.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 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 @@ -48,7 +48,7 @@ angular.module('rest').factory('connectionService', ['$injector', * // Do something with the connection * }); */ - service.getConnection = function getConnection(id) { + service.getConnection = function getConnection(dataSource, id) { // Build HTTP parameters set var httpParameters = { @@ -59,7 +59,7 @@ angular.module('rest').factory('connectionService', ['$injector', return $http({ cache : cacheService.connections, method : 'GET', - url : 'api/connections/' + encodeURIComponent(id), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id), params : httpParameters }); @@ -77,7 +77,7 @@ angular.module('rest').factory('connectionService', ['$injector', * A promise which will resolve with an array of * @link{ConnectionHistoryEntry} objects upon success. */ - service.getConnectionHistory = function getConnectionHistory(id) { + service.getConnectionHistory = function getConnectionHistory(dataSource, id) { // Build HTTP parameters set var httpParameters = { @@ -87,7 +87,7 @@ angular.module('rest').factory('connectionService', ['$injector', // Retrieve connection history return $http({ method : 'GET', - url : 'api/connections/' + encodeURIComponent(id) + '/history', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/history', params : httpParameters }); @@ -105,7 +105,7 @@ angular.module('rest').factory('connectionService', ['$injector', * A promise which will resolve with an map of parameter name/value * pairs upon success. */ - service.getConnectionParameters = function getConnectionParameters(id) { + service.getConnectionParameters = function getConnectionParameters(dataSource, id) { // Build HTTP parameters set var httpParameters = { @@ -116,7 +116,7 @@ angular.module('rest').factory('connectionService', ['$injector', return $http({ cache : cacheService.connections, method : 'GET', - url : 'api/connections/' + encodeURIComponent(id) + '/parameters', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/parameters', params : httpParameters }); @@ -135,7 +135,7 @@ angular.module('rest').factory('connectionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ - service.saveConnection = function saveConnection(connection) { + service.saveConnection = function saveConnection(dataSource, connection) { // Build HTTP parameters set var httpParameters = { @@ -146,7 +146,7 @@ angular.module('rest').factory('connectionService', ['$injector', if (!connection.identifier) { return $http({ method : 'POST', - url : 'api/connections', + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections', params : httpParameters, data : connection }) @@ -162,7 +162,7 @@ angular.module('rest').factory('connectionService', ['$injector', else { return $http({ method : 'PUT', - url : 'api/connections/' + encodeURIComponent(connection.identifier), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier), params : httpParameters, data : connection }) @@ -185,7 +185,7 @@ angular.module('rest').factory('connectionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteConnection = function deleteConnection(connection) { + service.deleteConnection = function deleteConnection(dataSource, connection) { // Build HTTP parameters set var httpParameters = { @@ -195,7 +195,7 @@ angular.module('rest').factory('connectionService', ['$injector', // Delete connection return $http({ method : 'DELETE', - url : 'api/connections/' + encodeURIComponent(connection.identifier), + url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier), params : httpParameters }) diff --git a/guacamole/src/main/webapp/app/rest/services/dataSourceService.js b/guacamole/src/main/webapp/app/rest/services/dataSourceService.js new file mode 100644 index 000000000..70e7ad48d --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/services/dataSourceService.js @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 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. + */ + +/** + * Service which contains all REST API response caches. + */ +angular.module('rest').factory('dataSourceService', ['$injector', + function dataSourceService($injector) { + + // Required services + var $q = $injector.get('$q'); + + // Service containing all caches + var service = {}; + + /** + * Invokes the given function once for each of the given data sources, + * passing that data source as the first argument to each invocation, + * followed by any additional arguments passed to apply(). The results of + * each invocation are aggregated into a map by data source identifier, + * and handled through a single promise which is resolved or rejected + * depending on the success/failure of each resulting REST call. Any error + * results in rejection of the entire apply() operation, except 404 ("NOT + * FOUND") errors, which are ignored. + * + * @param {Function} fn + * The function to call for each of the given data sources. The data + * source identifier will be given as the first argument, followed by + * the rest of the arguments given to apply(), in order. The function + * must return a Promise which is resolved or rejected depending on the + * result of the REST call. + * + * @param {String[]} dataSources + * The array or data source identifiers against which the given + * function should be called. + * + * @param {...*} args + * Any additional arguments to pass to the given function each time it + * is called. + * + * @returns {Promise.>} + * A Promise which resolves with a map of data source identifier to + * corresponding result. The result will be the exact object or value + * provided as the resolution to the Promise returned by calls to the + * given function. + */ + service.apply = function apply(fn, dataSources) { + + var deferred = $q.defer(); + + var requests = []; + var results = {}; + + // Build array of arguments to pass to the given function + var args = []; + for (var i = 2; i < arguments.length; i++) + args.push(arguments[i]); + + // Retrieve the root group from all data sources + angular.forEach(dataSources, function invokeAgainstDataSource(dataSource) { + + // Add promise to list of pending requests + var deferredRequest = $q.defer(); + requests.push(deferredRequest.promise); + + // Retrieve root group from data source + fn.apply(this, [dataSource].concat(args)) + + // Store result on success + .then(function immediateRequestSucceeded(response) { + results[dataSource] = response.data; + deferredRequest.resolve(); + }, + + // Fail on any errors (except "NOT FOUND") + function immediateRequestFailed(response) { + + // Ignore "NOT FOUND" errors + if (response.status === 404) + deferredRequest.resolve(); + + // Explicitly abort for all other errors + else + deferredRequest.reject(response); + + }); + + }); + + // Resolve if all requests succeed + $q.all(requests).then(function requestsSucceeded() { + deferred.resolve(results); + }, + + // Reject if at least one request fails + function requestFailed(response) { + deferred.reject(response); + }); + + return deferred.promise; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/rest/services/permissionService.js b/guacamole/src/main/webapp/app/rest/services/permissionService.js index c60bfd2dc..3cef2b9bc 100644 --- a/guacamole/src/main/webapp/app/rest/services/permissionService.js +++ b/guacamole/src/main/webapp/app/rest/services/permissionService.js @@ -28,6 +28,7 @@ angular.module('rest').factory('permissionService', ['$injector', // Required services var $http = $injector.get('$http'); + var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); @@ -41,6 +42,11 @@ angular.module('rest').factory('permissionService', ['$injector', * given user, returning a promise that provides an array of * @link{Permission} objects if successful. * + * @param {String} dataSource + * The unique identifier of the data source containing the user whose + * permissions should be retrieved. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * * @param {String} userID * The ID of the user to retrieve the permissions for. * @@ -48,7 +54,7 @@ angular.module('rest').factory('permissionService', ['$injector', * A promise which will resolve with a @link{PermissionSet} upon * success. */ - service.getPermissions = function getPermissions(userID) { + service.getPermissions = function getPermissions(dataSource, userID) { // Build HTTP parameters set var httpParameters = { @@ -59,17 +65,22 @@ angular.module('rest').factory('permissionService', ['$injector', return $http({ cache : cacheService.users, method : 'GET', - url : 'api/users/' + encodeURIComponent(userID) + '/permissions', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(userID) + '/permissions', params : httpParameters }); }; - + /** * Makes a request to the REST API to add permissions for a given user, * returning a promise that can be used for processing the results of the * call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user whose + * permissions should be modified. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * * @param {String} userID * The ID of the user to modify the permissions of. * @@ -80,8 +91,8 @@ angular.module('rest').factory('permissionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * add operation is successful. */ - service.addPermissions = function addPermissions(userID, permissions) { - return service.patchPermissions(userID, permissions, null); + service.addPermissions = function addPermissions(dataSource, userID, permissions) { + return service.patchPermissions(dataSource, userID, permissions, null); }; /** @@ -89,6 +100,11 @@ angular.module('rest').factory('permissionService', ['$injector', * returning a promise that can be used for processing the results of the * call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user whose + * permissions should be modified. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * * @param {String} userID * The ID of the user to modify the permissions of. * @@ -99,8 +115,8 @@ angular.module('rest').factory('permissionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * remove operation is successful. */ - service.removePermissions = function removePermissions(userID, permissions) { - return service.patchPermissions(userID, null, permissions); + service.removePermissions = function removePermissions(dataSource, userID, permissions) { + return service.patchPermissions(dataSource, userID, null, permissions); }; /** @@ -186,6 +202,11 @@ angular.module('rest').factory('permissionService', ['$injector', * user, returning a promise that can be used for processing the results of * the call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user whose + * permissions should be modified. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * * @param {String} userID * The ID of the user to modify the permissions of. * @@ -199,7 +220,7 @@ angular.module('rest').factory('permissionService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * patch operation is successful. */ - service.patchPermissions = function patchPermissions(userID, permissionsToAdd, permissionsToRemove) { + service.patchPermissions = function patchPermissions(dataSource, userID, permissionsToAdd, permissionsToRemove) { var permissionPatch = []; @@ -217,7 +238,7 @@ angular.module('rest').factory('permissionService', ['$injector', // Patch user permissions return $http({ method : 'PATCH', - url : 'api/users/' + encodeURIComponent(userID) + '/permissions', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(userID) + '/permissions', params : httpParameters, data : permissionPatch }) diff --git a/guacamole/src/main/webapp/app/rest/services/schemaService.js b/guacamole/src/main/webapp/app/rest/services/schemaService.js index 0ca3596c5..2c3041f85 100644 --- a/guacamole/src/main/webapp/app/rest/services/schemaService.js +++ b/guacamole/src/main/webapp/app/rest/services/schemaService.js @@ -39,12 +39,18 @@ angular.module('rest').factory('schemaService', ['$injector', * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * + * @param {String} dataSource + * The unique identifier of the data source containing the users whose + * available attributes are to be retrieved. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getUserAttributes = function getUserAttributes() { + service.getUserAttributes = function getUserAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { @@ -55,7 +61,7 @@ angular.module('rest').factory('schemaService', ['$injector', return $http({ cache : cacheService.schema, method : 'GET', - url : 'api/schema/users/attributes', + url : 'api/schema/' + encodeURIComponent(dataSource) + '/users/attributes', params : httpParameters }); @@ -67,12 +73,18 @@ angular.module('rest').factory('schemaService', ['$injector', * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * + * @param {String} dataSource + * The unique identifier of the data source containing the connections + * whose available attributes are to be retrieved. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getConnectionAttributes = function getConnectionAttributes() { + service.getConnectionAttributes = function getConnectionAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { @@ -83,7 +95,7 @@ angular.module('rest').factory('schemaService', ['$injector', return $http({ cache : cacheService.schema, method : 'GET', - url : 'api/schema/connections/attributes', + url : 'api/schema/' + encodeURIComponent(dataSource) + '/connections/attributes', params : httpParameters }); @@ -95,12 +107,18 @@ angular.module('rest').factory('schemaService', ['$injector', * of @link{Form} objects if successful. Each element of the array * a logical grouping of possible attributes. * + * @param {String} dataSource + * The unique identifier of the data source containing the connection + * groups whose available attributes are to be retrieved. This + * identifier corresponds to an AuthenticationProvider within the + * Guacamole web application. + * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getConnectionGroupAttributes = function getConnectionGroupAttributes() { + service.getConnectionGroupAttributes = function getConnectionGroupAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { @@ -111,7 +129,7 @@ angular.module('rest').factory('schemaService', ['$injector', return $http({ cache : cacheService.schema, method : 'GET', - url : 'api/schema/connectionGroups/attributes', + url : 'api/schema/' + encodeURIComponent(dataSource) + '/connectionGroups/attributes', params : httpParameters }); @@ -122,11 +140,16 @@ angular.module('rest').factory('schemaService', ['$injector', * a promise that provides a map of @link{Protocol} objects by protocol * name if successful. * + * @param {String} dataSource + * The unique identifier of the data source defining available + * protocols. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @returns {Promise.>} * A promise which will resolve with a map of @link{Protocol} * objects by protocol name upon success. */ - service.getProtocols = function getProtocols() { + service.getProtocols = function getProtocols(dataSource) { // Build HTTP parameters set var httpParameters = { @@ -137,7 +160,7 @@ angular.module('rest').factory('schemaService', ['$injector', return $http({ cache : cacheService.schema, method : 'GET', - url : 'api/schema/protocols', + url : 'api/schema/' + encodeURIComponent(dataSource) + '/protocols', params : httpParameters }); diff --git a/guacamole/src/main/webapp/app/rest/services/userService.js b/guacamole/src/main/webapp/app/rest/services/userService.js index a81916e19..e7b3a359e 100644 --- a/guacamole/src/main/webapp/app/rest/services/userService.js +++ b/guacamole/src/main/webapp/app/rest/services/userService.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 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 @@ -28,6 +28,7 @@ angular.module('rest').factory('userService', ['$injector', // Required services var $http = $injector.get('$http'); + var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); @@ -41,6 +42,11 @@ angular.module('rest').factory('userService', ['$injector', * returning a promise that provides an array of @link{User} objects if * successful. * + * @param {String} dataSource + * The unique identifier of the data source containing the users to be + * retrieved. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {String[]} [permissionTypes] * The set of permissions to filter with. A user must have one or more * of these permissions for a user to appear in the result. @@ -51,7 +57,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise which will resolve with an array of @link{User} objects * upon success. */ - service.getUsers = function getUsers(permissionTypes) { + service.getUsers = function getUsers(dataSource, permissionTypes) { // Build HTTP parameters set var httpParameters = { @@ -66,7 +72,7 @@ angular.module('rest').factory('userService', ['$injector', return $http({ cache : cacheService.users, method : 'GET', - url : 'api/users', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users', params : httpParameters }); @@ -76,14 +82,19 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to get the user having the given * username, returning a promise that provides the corresponding * @link{User} if successful. - * + * + * @param {String} dataSource + * The unique identifier of the data source containing the user to be + * retrieved. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {String} username * The username of the user to retrieve. * * @returns {Promise.} * A promise which will resolve with a @link{User} upon success. */ - service.getUser = function getUser(username) { + service.getUser = function getUser(dataSource, username) { // Build HTTP parameters set var httpParameters = { @@ -94,7 +105,7 @@ angular.module('rest').factory('userService', ['$injector', return $http({ cache : cacheService.users, method : 'GET', - url : 'api/users/' + encodeURIComponent(username), + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), params : httpParameters }); @@ -104,6 +115,11 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to delete a user, returning a promise * that can be used for processing the results of the call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user to be + * deleted. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {User} user * The user to delete. * @@ -111,7 +127,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteUser = function deleteUser(user) { + service.deleteUser = function deleteUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { @@ -121,7 +137,7 @@ angular.module('rest').factory('userService', ['$injector', // Delete user return $http({ method : 'DELETE', - url : 'api/users/' + encodeURIComponent(user.username), + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username), params : httpParameters }) @@ -137,6 +153,11 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to create a user, returning a promise * that can be used for processing the results of the call. * + * @param {String} dataSource + * The unique identifier of the data source in which the user should be + * created. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {User} user * The user to create. * @@ -144,7 +165,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * create operation is successful. */ - service.createUser = function createUser(user) { + service.createUser = function createUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { @@ -154,7 +175,7 @@ angular.module('rest').factory('userService', ['$injector', // Create user return $http({ method : 'POST', - url : 'api/users', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users', params : httpParameters, data : user }) @@ -170,6 +191,11 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to save a user, returning a promise that * can be used for processing the results of the call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user to be + * updated. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {User} user * The user to update. * @@ -177,7 +203,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ - service.saveUser = function saveUser(user) { + service.saveUser = function saveUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { @@ -187,7 +213,7 @@ angular.module('rest').factory('userService', ['$injector', // Update user return $http({ method : 'PUT', - url : 'api/users/' + encodeURIComponent(user.username), + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username), params : httpParameters, data : user }) @@ -203,6 +229,11 @@ angular.module('rest').factory('userService', ['$injector', * Makes a request to the REST API to update the password for a user, * returning a promise that can be used for processing the results of the call. * + * @param {String} dataSource + * The unique identifier of the data source containing the user to be + * updated. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * * @param {String} username * The username of the user to update. * @@ -216,7 +247,7 @@ angular.module('rest').factory('userService', ['$injector', * A promise for the HTTP call which will succeed if and only if the * password update operation is successful. */ - service.updateUserPassword = function updateUserPassword(username, + service.updateUserPassword = function updateUserPassword(dataSource, username, oldPassword, newPassword) { // Build HTTP parameters set @@ -227,7 +258,7 @@ angular.module('rest').factory('userService', ['$injector', // Update user password return $http({ method : 'PUT', - url : 'api/users/' + encodeURIComponent(username) + '/password', + url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) + '/password', params : httpParameters, data : new UserPasswordUpdate({ oldPassword : oldPassword, diff --git a/guacamole/src/main/webapp/app/settings/controllers/settingsController.js b/guacamole/src/main/webapp/app/settings/controllers/settingsController.js index 7608f5f9a..ce1a26aef 100644 --- a/guacamole/src/main/webapp/app/settings/controllers/settingsController.js +++ b/guacamole/src/main/webapp/app/settings/controllers/settingsController.js @@ -46,17 +46,6 @@ angular.module('manage').controller('settingsController', ['$scope', '$injector' */ $scope.activeTab = $routeParams.tab; - /** - * Returns whether the list of all available settings tabs should be shown. - * - * @returns {Boolean} - * true if the list of available settings tabs should be shown, false - * otherwise. - */ - $scope.showAvailableTabs = function showAvailableTabs() { - return !!$scope.settingsPages && $scope.settingsPages.length > 1; - }; - // Retrieve settings pages userPageService.getSettingsPages() .then(function settingsPagesRetrieved(pages) { diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js index e1b77893f..fe0449fe2 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js @@ -41,13 +41,18 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe var PermissionSet = $injector.get('PermissionSet'); // Required services - var $location = $injector.get('$location'); + var $routeParams = $injector.get('$routeParams'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); - // Identifier of the current user + /** + * The identifier of the current user. + * + * @type String + */ var currentUsername = authenticationService.getCurrentUsername(); /** @@ -62,12 +67,19 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe } }; + /** + * The identifier of the currently-selected data source. + * + * @type String + */ + $scope.dataSource = $routeParams.dataSource; + /** * The root connection group of the connection group hierarchy. * - * @type ConnectionGroup + * @type Object. */ - $scope.rootGroup = null; + $scope.rootGroups = null; /** * Whether the current user can manage connections. If the current @@ -98,7 +110,7 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe * All permissions associated with the current user, or null if the * user's permissions have not yet been loaded. * - * @type PermissionSet + * @type Object. */ $scope.permissions = null; @@ -111,57 +123,142 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe */ $scope.isLoaded = function isLoaded() { - return $scope.rootGroup !== null - && $scope.permissions !== null - && $scope.canManageConnections !== null - && $scope.canCreateConnections !== null - && $scope.canCreateConnectionGroups !== null; + return $scope.rootGroup !== null + && $scope.permissions !== null; + + }; + + /** + * Returns whether the current user can create new connections + * within at least one data source. + * + * @return {Boolean} + * true if the current user can create new connections within + * at least one data source, false otherwise. + */ + $scope.canCreateConnections = function canCreateConnections() { + + // Abort if permissions have not yet loaded + if (!$scope.permissions) + return false; + + // For each data source + for (var dataSource in $scope.permissions) { + + // Retrieve corresponding permission set + var permissionSet = $scope.permissions[dataSource]; + + // Can create connections if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_CONNECTION)) + return true; + + } + + // No data sources allow connection creation + return false; + + }; + + /** + * Returns whether the current user can create new connection + * groups within at least one data source. + * + * @return {Boolean} + * true if the current user can create new connection groups + * within at least one data source, false otherwise. + */ + $scope.canCreateConnectionGroups = function canCreateConnectionGroups() { + + // Abort if permissions have not yet loaded + if (!$scope.permissions) + return false; + + // For each data source + for (var dataSource in $scope.permissions) { + + // Retrieve corresponding permission set + var permissionSet = $scope.permissions[dataSource]; + + // Can create connections groups if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP)) + return true; + + } + + // No data sources allow connection group creation + return false; + + }; + + /** + * Returns whether the current user can create new connections or + * connection groups or make changes to existing connections or + * connection groups within at least one data source. The + * connection management interface as a whole is useless if this + * function returns false. + * + * @return {Boolean} + * true if the current user can create new connections/groups + * or make changes to existing connections/groups within at + * least one data source, false otherwise. + */ + $scope.canManageConnections = function canManageConnections() { + + // Abort if permissions have not yet loaded + if (!$scope.permissions) + return false; + + // Creating connections/groups counts as management + if ($scope.canCreateConnections() || $scope.canCreateConnectionGroups()) + return true; + + // Ignore permission to update root group + PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); + + // For each data source + for (var dataSource in $scope.permissions) { + + // Retrieve corresponding permission set + var permissionSet = $scope.permissions[dataSource]; + + // Can manage connections if granted explicit update or delete + if (PermissionSet.hasConnectionPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasConnectionPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) + return true; + + // Can manage connections groups if granted explicit update or delete + if (PermissionSet.hasConnectionGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasConnectionGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) + return true; + + } + + // No data sources allow management of connections or groups + return false; }; // Retrieve current permissions - permissionService.getPermissions(currentUsername) - .success(function permissionsRetrieved(permissions) { - + dataSourceService.apply( + permissionService.getPermissions, + [$scope.dataSource], + currentUsername + ) + .then(function permissionsRetrieved(permissions) { $scope.permissions = permissions; - - // Ignore permission to update root group - PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); - - // Determine whether the current user can create new users - $scope.canCreateConnections = - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION); - - // Determine whether the current user can create new users - $scope.canCreateConnectionGroups = - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP); - - // Determine whether the current user can manage other connections or groups - $scope.canManageConnections = - - // Permission to manage connections - $scope.canCreateConnections - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - - // Permission to manage groups - || $scope.canCreateConnectionGroups - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE); - - // Return to home if there's nothing to do here - if (!$scope.canManageConnections) - $location.path('/'); - }); // Retrieve all connections for which we have UPDATE or DELETE permission - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE]) - .success(function connectionGroupReceived(rootGroup) { - $scope.rootGroup = rootGroup; + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + [$scope.dataSource], + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE] + ) + .then(function connectionGroupsReceived(rootGroups) { + $scope.rootGroups = rootGroups; }); }] diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js index 30a52e265..f13c04baa 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js @@ -66,6 +66,14 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe */ var username = authenticationService.getCurrentUsername(); + /** + * The identifier of the data source which authenticated the + * current user. + * + * @type String + */ + var dataSource = authenticationService.getDataSource(); + /** * All currently-set preferences, or their defaults if not yet set. * @@ -140,7 +148,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe } // Save the user with the new password - userService.updateUserPassword(username, $scope.oldPassword, $scope.newPassword) + userService.updateUserPassword(dataSource, username, $scope.oldPassword, $scope.newPassword) .success(function passwordUpdated() { // Clear the password fields @@ -174,7 +182,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe }); // Retrieve current permissions - permissionService.getPermissions(username) + permissionService.getPermissions(dataSource, username) .success(function permissionsRetrieved(permissions) { // Add action for changing password if permission is granted diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js index 3635d38cb..f40bfa11e 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js @@ -44,19 +44,20 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti // Required services var $filter = $injector.get('$filter'); var $translate = $injector.get('$translate'); + var $q = $injector.get('$q'); var activeConnectionService = $injector.get('activeConnectionService'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); - var permissionService = $injector.get('permissionService'); /** - * All permissions associated with the current user, or null if the - * user's permissions have not yet been loaded. + * The identifiers of all data sources accessible by the current + * user. * - * @type PermissionSet + * @type String[] */ - $scope.permissions = null; + var dataSources = authenticationService.getAvailableDataSources(); /** * The ActiveConnectionWrappers of all active sessions accessible @@ -93,20 +94,22 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti ]; /** - * All active connections, if known, or null if active connections - * have not yet been loaded. + * All active connections, if known, grouped by corresponding data + * source identifier, or null if active connections have not yet + * been loaded. * - * @type ActiveConnection + * @type Object.> */ - var activeConnections = null; + var allActiveConnections = null; /** - * Map of all visible connections by object identifier, or null if - * visible connections have not yet been loaded. + * Map of all visible connections by data source identifier and + * object identifier, or null if visible connections have not yet + * been loaded. * - * @type Object. + * @type Object.> */ - var connections = null; + var allConnections = null; /** * The date format for use for session-related dates. @@ -117,24 +120,28 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti /** * Map of all currently-selected active connection wrappers by - * identifier. + * data source and identifier. * - * @type Object. + * @type Object.> */ - var selectedWrappers = {}; + var allSelectedWrappers = {}; /** * Adds the given connection to the internal set of visible * connections. - * + * + * @param {String} dataSource + * The identifier of the data source associated with the given + * connection. + * * @param {Connection} connection * The connection to add to the internal set of visible * connections. */ - var addConnection = function addConnection(connection) { + var addConnection = function addConnection(dataSource, connection) { // Add given connection to set of visible connections - connections[connection.identifier] = connection; + allConnections[dataSource][connection.identifier] = connection; }; @@ -142,19 +149,25 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti * Adds all descendant connections of the given connection group to * the internal set of connections. * + * @param {String} dataSource + * The identifier of the data source associated with the given + * connection group. + * * @param {ConnectionGroup} connectionGroup * The connection group whose descendant connections should be * added to the internal set of connections. */ - var addDescendantConnections = function addDescendantConnections(connectionGroup) { + var addDescendantConnections = function addDescendantConnections(dataSource, connectionGroup) { // Add all child connections - if (connectionGroup.childConnections) - connectionGroup.childConnections.forEach(addConnection); + angular.forEach(connectionGroup.childConnections, function addConnectionForDataSource(connection) { + addConnection(dataSource, connection); + }); // Add all child connection groups - if (connectionGroup.childConnectionGroups) - connectionGroup.childConnectionGroups.forEach(addDescendantConnections); + angular.forEach(connectionGroup.childConnectionGroups, function addConnectionGroupForDataSource(connectionGroup) { + addDescendantConnections(dataSource, connectionGroup); + }); }; @@ -163,56 +176,66 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti * within the scope. If required data has not yet finished loading, * this function has no effect. */ - var wrapActiveConnections = function wrapActiveConnections() { + var wrapAllActiveConnections = function wrapAllActiveConnections() { // Abort if not all required data is available - if (!activeConnections || !connections || !sessionDateFormat) + if (!allActiveConnections || !allConnections || !sessionDateFormat) return; // Wrap all active connections for sake of display $scope.wrappers = []; - for (var identifier in activeConnections) { + angular.forEach(allActiveConnections, function wrapActiveConnections(activeConnections, dataSource) { + angular.forEach(activeConnections, function wrapActiveConnection(activeConnection, identifier) { - var activeConnection = activeConnections[identifier]; - var connection = connections[activeConnection.connectionIdentifier]; + // Retrieve corresponding connection + var connection = allConnections[dataSource][activeConnection.connectionIdentifier]; - $scope.wrappers.push(new ActiveConnectionWrapper( - connection.name, - $filter('date')(activeConnection.startDate, sessionDateFormat), - activeConnection - )); + // Add wrapper + $scope.wrappers.push(new ActiveConnectionWrapper({ + dataSource : dataSource, + name : connection.name, + startDate : $filter('date')(activeConnection.startDate, sessionDateFormat), + activeConnection : activeConnection + })); - } + }); + }); }; - // Query the user's permissions - permissionService.getPermissions(authenticationService.getCurrentUsername()) - .success(function permissionsReceived(retrievedPermissions) { - $scope.permissions = retrievedPermissions; - }); - // Retrieve all connections - connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) - .success(function connectionGroupReceived(retrievedRootGroup) { + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + dataSources, + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function connectionGroupsReceived(rootGroups) { - // Load connections from retrieved group tree - connections = {}; - addDescendantConnections(retrievedRootGroup); + allConnections = {}; + + // Load connections from each received root group + angular.forEach(rootGroups, function connectionGroupReceived(rootGroup, dataSource) { + allConnections[dataSource] = {}; + addDescendantConnections(dataSource, rootGroup); + }); // Attempt to produce wrapped list of active connections - wrapActiveConnections(); + wrapAllActiveConnections(); }); // Query active sessions - activeConnectionService.getActiveConnections().success(function sessionsRetrieved(retrievedActiveConnections) { + dataSourceService.apply( + activeConnectionService.getActiveConnections, + dataSources + ) + .then(function sessionsRetrieved(retrievedActiveConnections) { - // Store received list - activeConnections = retrievedActiveConnections; + // Store received map of active connections + allActiveConnections = retrievedActiveConnections; // Attempt to produce wrapped list of active connections - wrapActiveConnections(); + wrapAllActiveConnections(); }); @@ -223,7 +246,7 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti sessionDateFormat = retrievedSessionDateFormat; // Attempt to produce wrapped list of active connections - wrapActiveConnections(); + wrapAllActiveConnections(); }); @@ -235,11 +258,7 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti * to be useful, false otherwise. */ $scope.isLoaded = function isLoaded() { - - return $scope.wrappers !== null - && $scope.sessionDateFormat !== null - && $scope.permissions !== null; - + return $scope.wrappers !== null; }; /** @@ -276,7 +295,7 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti className : "danger", // Handle action callback : function deleteCallback() { - deleteSessionsImmediately(); + deleteAllSessionsImmediately(); guacNotification.showStatus(false); } }; @@ -285,24 +304,36 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti * Immediately deletes the selected sessions, without prompting the * user for confirmation. */ - var deleteSessionsImmediately = function deleteSessionsImmediately() { + var deleteAllSessionsImmediately = function deleteAllSessionsImmediately() { - // Perform deletion - activeConnectionService.deleteActiveConnections(Object.keys(selectedWrappers)) - .success(function activeConnectionsDeleted() { + var deletionRequests = []; + + // Perform deletion for each relevant data source + angular.forEach(allSelectedWrappers, function deleteSessionsImmediately(selectedWrappers, dataSource) { + + // Delete sessions, if any are selected + var identifiers = Object.keys(selectedWrappers); + if (identifiers.length) + deletionRequests.push(activeConnectionService.deleteActiveConnections(dataSource, identifiers)); + + }); + + // Update interface + $q.all(deletionRequests) + .then(function activeConnectionsDeleted() { // Remove deleted connections from wrapper array $scope.wrappers = $scope.wrappers.filter(function activeConnectionStillExists(wrapper) { - return !(wrapper.activeConnection.identifier in selectedWrappers); + return !(wrapper.activeConnection.identifier in (allSelectedWrappers[wrapper.dataSource] || {})); }); // Clear selection - selectedWrappers = {}; + allSelectedWrappers = {}; - }) + }, // Notify of any errors - .error(function activeConnectionDeletionFailed(error) { + function activeConnectionDeletionFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_ERROR', @@ -335,8 +366,10 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti $scope.canDeleteSessions = function canDeleteSessions() { // We can delete sessions if at least one is selected - for (var identifier in selectedWrappers) - return true; + for (var dataSource in allSelectedWrappers) { + for (var identifier in allSelectedWrappers[dataSource]) + return true; + } return false; @@ -351,6 +384,11 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti */ $scope.wrapperSelectionChange = function wrapperSelectionChange(wrapper) { + // Get selection map for associated data source, creating if necessary + var selectedWrappers = allSelectedWrappers[wrapper.dataSource]; + if (!selectedWrappers) + selectedWrappers = allSelectedWrappers[wrapper.dataSource] = {}; + // Add wrapper to map if selected if (wrapper.checked) selectedWrappers[wrapper.activeConnection.identifier] = wrapper; diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js index 1d0f411fb..af52ee3bf 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js @@ -37,12 +37,13 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings controller: ['$scope', '$injector', function settingsUsersController($scope, $injector) { // Required types + var ManageableUser = $injector.get('ManageableUser'); var PermissionSet = $injector.get('PermissionSet'); - var User = $injector.get('User'); // Required services var $location = $injector.get('$location'); var authenticationService = $injector.get('authenticationService'); + var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var userService = $injector.get('userService'); @@ -63,27 +64,19 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings }; /** - * All visible users. + * The identifiers of all data sources accessible by the current + * user. * - * @type User[] + * @type String[] */ - $scope.users = null; + var dataSources = authenticationService.getAvailableDataSources(); /** - * Whether the current user can manage users. If the current - * permissions have not yet been loaded, this will be null. + * All visible users, along with their corresponding data sources. * - * @type Boolean + * @type ManageableUser[] */ - $scope.canManageUsers = null; - - /** - * Whether the current user can create new users. If the current - * permissions have not yet been loaded, this will be null. - * - * @type Boolean - */ - $scope.canCreateUsers = null; + $scope.manageableUsers = null; /** * The name of the new user to create, if any, when user creation @@ -94,10 +87,11 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings $scope.newUsername = ""; /** - * All permissions associated with the current user, or null if the + * Map of data source identifiers to all permissions associated + * with the current user within that data source, or null if the * user's permissions have not yet been loaded. * - * @type PermissionSet + * @type Object. */ $scope.permissions = null; @@ -110,80 +104,152 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings */ $scope.isLoaded = function isLoaded() { - return $scope.users !== null - && $scope.permissions !== null - && $scope.canManageUsers !== null - && $scope.canCreateUsers !== null; + return $scope.manageableUsers !== null + && $scope.permissions !== null; + + }; + + /** + * Returns the identifier of the data source that should be used by + * default when creating a new user. + * + * @return {String} + * The identifier of the data source that should be used by + * default when creating a new user, or null if user creation + * is not allowed. + */ + var getDefaultDataSource = function getDefaultDataSource() { + + // Abort if permissions have not yet loaded + if (!$scope.permissions) + return null; + + // For each data source + for (var dataSource in $scope.permissions) { + + // Retrieve corresponding permission set + var permissionSet = $scope.permissions[dataSource]; + + // Can create users if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_USER)) + return dataSource; + + } + + // No data sources allow user creation + return null; + + }; + + /** + * Returns whether the current user can create new users within at + * least one data source. + * + * @return {Boolean} + * true if the current user can create new users within at + * least one data source, false otherwise. + */ + $scope.canCreateUsers = function canCreateUsers() { + return getDefaultDataSource() !== null; + }; + + /** + * Returns whether the current user can create new users or make + * changes to existing users within at least one data source. The + * user management interface as a whole is useless if this function + * returns false. + * + * @return {Boolean} + * true if the current user can create new users or make + * changes to existing users within at least one data source, + * false otherwise. + */ + var canManageUsers = function canManageUsers() { + + // Abort if permissions have not yet loaded + if (!$scope.permissions) + return false; + + // Creating users counts as management + if ($scope.canCreateUsers()) + return true; + + // For each data source + for (var dataSource in $scope.permissions) { + + // Retrieve corresponding permission set + var permissionSet = $scope.permissions[dataSource]; + + // Can manage users if granted explicit update or delete + if (PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) + return true; + + } + + // No data sources allow management of users + return false; }; // Retrieve current permissions - permissionService.getPermissions(currentUsername) - .success(function permissionsRetrieved(permissions) { + dataSourceService.apply(permissionService.getPermissions, dataSources, currentUsername) + .then(function permissionsRetrieved(permissions) { + // Store retrieved permissions $scope.permissions = permissions; - // Determine whether the current user can create new users - $scope.canCreateUsers = - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER); - - // Determine whether the current user can manage other users - $scope.canManageUsers = - $scope.canCreateUsers - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE); - // Return to home if there's nothing to do here - if (!$scope.canManageUsers) + if (!canManageUsers()) $location.path('/'); - - }); - // Retrieve all users for whom we have UPDATE or DELETE permission - userService.getUsers([PermissionSet.ObjectPermissionType.UPDATE, - PermissionSet.ObjectPermissionType.DELETE]) - .success(function usersReceived(users) { + var userPromise; + + // If users can be created, list all readable users + if ($scope.canCreateUsers()) + userPromise = dataSourceService.apply(userService.getUsers, dataSources); + + // Otherwise, list only updateable/deletable users + else + userPromise = dataSourceService.apply(userService.getUsers, dataSources, [ + PermissionSet.ObjectPermissionType.UPDATE, + PermissionSet.ObjectPermissionType.DELETE + ]); + + userPromise.then(function usersReceived(userArrays) { + + var addedUsers = {}; + $scope.manageableUsers = []; + + // For each user in each data source + angular.forEach(dataSources, function addUserList(dataSource) { + angular.forEach(userArrays[dataSource], function addUser(user) { + + // Do not add the same user twice + if (addedUsers[user.username]) + return; + + // Add user to overall list + addedUsers[user.username] = user; + $scope.manageableUsers.push(new ManageableUser ({ + 'dataSource' : dataSource, + 'user' : user + })); + + }); + }); - // Display only other users, not self - $scope.users = users.filter(function isNotSelf(user) { - return user.username !== currentUsername; }); }); /** - * Creates a new user having the username specified in the user - * creation interface. + * Navigates to an interface for creating a new user having the + * username specified. */ $scope.newUser = function newUser() { - - // Create user skeleton - var user = new User({ - username: $scope.newUsername || '' - }); - - // Create specified user - userService.createUser(user) - - // Add user to visible list upon success - .success(function userCreated() { - $scope.users.push(user); - }) - - // Notify of any errors - .error(function userCreationFailed(error) { - guacNotification.showStatus({ - 'className' : 'error', - 'title' : 'SETTINGS_USERS.DIALOG_HEADER_ERROR', - 'text' : error.message, - 'actions' : [ ACKNOWLEDGE_ACTION ] - }); - }); - - // Reset username - $scope.newUsername = ""; - + $location.url('/manage/' + encodeURIComponent(getDefaultDataSource()) + '/users/' + encodeURIComponent($scope.newUsername)); }; }] diff --git a/guacamole/src/main/webapp/app/settings/styles/settings.css b/guacamole/src/main/webapp/app/settings/styles/settings.css index 67453a266..deb4b8cc1 100644 --- a/guacamole/src/main/webapp/app/settings/styles/settings.css +++ b/guacamole/src/main/webapp/app/settings/styles/settings.css @@ -34,36 +34,3 @@ text-align: center; margin: 1em 0; } - -.settings-tabs .page-list { - margin: 0; - padding: 0; - background: rgba(0, 0, 0, 0.0125); - border-bottom: 1px solid rgba(0, 0, 0, 0.05); -} - -.settings-tabs .page-list li { - display: inline-block; - list-style: none; -} - -.settings-tabs .page-list li a[href] { - display: block; - color: black; - text-decoration: none; - padding: 0.75em 1em; -} - -.settings-tabs .page-list li a[href]:visited { - color: black; -} - -.settings-tabs .page-list li a[href]:hover { - background-color: #CDA; -} - -.settings-tabs .page-list li a[href].current, -.settings-tabs .page-list li a[href].current:hover { - background: rgba(0,0,0,0.3); - cursor: default; -} diff --git a/guacamole/src/main/webapp/app/settings/templates/connection.html b/guacamole/src/main/webapp/app/settings/templates/connection.html index f2d1a5550..7514ade8e 100644 --- a/guacamole/src/main/webapp/app/settings/templates/connection.html +++ b/guacamole/src/main/webapp/app/settings/templates/connection.html @@ -1,4 +1,4 @@ - + - @@ -41,7 +41,7 @@
diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html b/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html index 79290cb07..e1932bfb4 100644 --- a/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html +++ b/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html @@ -25,24 +25,25 @@

{{'SETTINGS_USERS.HELP_USERS' | translate}}

-
+
\ No newline at end of file diff --git a/guacamole/src/main/webapp/app/settings/types/ActiveConnectionWrapper.js b/guacamole/src/main/webapp/app/settings/types/ActiveConnectionWrapper.js index 85ffe02e7..abc1af297 100644 --- a/guacamole/src/main/webapp/app/settings/types/ActiveConnectionWrapper.js +++ b/guacamole/src/main/webapp/app/settings/types/ActiveConnectionWrapper.js @@ -31,44 +31,47 @@ angular.module('settings').factory('ActiveConnectionWrapper', [ * properties, such as a checked option. * * @constructor - * @param {String} name - * The display name of the active connection. - * - * @param {String} startDate - * The date and time this session began, pre-formatted for display. - * - * @param {ActiveConnection} activeConnection - * The ActiveConnection to wrap. + * @param {ActiveConnectionWrapper|Object} template + * The object whose properties should be copied within the new + * ActiveConnectionWrapper. */ - var ActiveConnectionWrapper = function ActiveConnectionWrapper(name, startDate, activeConnection) { + var ActiveConnectionWrapper = function ActiveConnectionWrapper(template) { + + /** + * The identifier of the data source associated with the + * ActiveConnection wrapped by this ActiveConnectionWrapper. + * + * @type String + */ + this.dataSource = template.dataSource; /** * The display name of this connection. * * @type String */ - this.name = name; + this.name = template.name; /** * The date and time this session began, pre-formatted for display. * * @type String */ - this.startDate = startDate; + this.startDate = template.startDate; /** * The wrapped ActiveConnection. * * @type ActiveConnection */ - this.activeConnection = activeConnection; + this.activeConnection = template.activeConnection; /** * A flag indicating that the active connection has been selected. * * @type Boolean */ - this.checked = false; + this.checked = template.checked || false; }; diff --git a/guacamole/src/main/webapp/images/checkmark.png b/guacamole/src/main/webapp/images/checkmark.png new file mode 100644 index 000000000..c54feb265 Binary files /dev/null and b/guacamole/src/main/webapp/images/checkmark.png differ diff --git a/guacamole/src/main/webapp/images/plus.png b/guacamole/src/main/webapp/images/plus.png new file mode 100644 index 000000000..14bedbe48 Binary files /dev/null and b/guacamole/src/main/webapp/images/plus.png differ diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index 8e24f5269..270db30f7 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -125,6 +125,10 @@ }, + "DATA_SOURCE_DEFAULT" : { + "NAME" : "Default (XML)" + }, + "FORM" : { "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD", @@ -240,7 +244,9 @@ "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Username:", - + + "INFO_READ_ONLY" : "Sorry, but this user account cannot be edited.", + "SECTION_HEADER_CONNECTIONS" : "Connections", "SECTION_HEADER_EDIT_USER" : "Edit User", "SECTION_HEADER_PERMISSIONS" : "Permissions",