Merge pull request #252 from glyptodon/decouple-auth

GUAC-586: Support multiple auth providers within web UI
This commit is contained in:
James Muehlner
2015-09-02 23:06:11 -07:00
72 changed files with 3106 additions and 986 deletions

View File

@@ -123,6 +123,7 @@ public class ConnectionService extends ModeledGroupedDirectoryObjectService<Mode
connection.setParentIdentifier(object.getParentIdentifier());
connection.setName(object.getName());
connection.setConfiguration(object.getConfiguration());
connection.setAttributes(object.getAttributes());
return model;

View File

@@ -106,6 +106,7 @@ public class ConnectionGroupService extends ModeledGroupedDirectoryObjectService
connectionGroup.setParentIdentifier(object.getParentIdentifier());
connectionGroup.setName(object.getName());
connectionGroup.setType(object.getType());
connectionGroup.setAttributes(object.getAttributes());
return model;

View File

@@ -162,6 +162,7 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
// Set model contents through ModeledUser, copying the provided user
user.setIdentifier(object.getIdentifier());
user.setPassword(object.getPassword());
user.setAttributes(object.getAttributes());
return model;

View File

@@ -33,6 +33,14 @@
},
"DATA_SOURCE_MYSQL" : {
"NAME" : "MySQL"
},
"DATA_SOURCE_POSTGRESQL" : {
"NAME" : "PostgreSQL"
},
"USER_ATTRIBUTES" : {
"FIELD_HEADER_DISABLED" : "Login disabled:",

View File

@@ -7,6 +7,10 @@
"authProviders" : [
"net.sourceforge.guacamole.net.auth.ldap.LDAPAuthenticationProvider"
],
"translations" : [
"translations/en.json"
]
}

View File

@@ -0,0 +1,7 @@
{
"DATA_SOURCE_LDAP" : {
"NAME" : "LDAP"
}
}

View File

@@ -7,6 +7,10 @@
"authProviders" : [
"net.sourceforge.guacamole.net.auth.noauth.NoAuthenticationProvider"
],
"translations" : [
"translations/en.json"
]
}

View File

@@ -0,0 +1,7 @@
{
"DATA_SOURCE_NOAUTH" : {
"NAME" : "NoAuth"
}
}

View File

@@ -202,7 +202,7 @@ public abstract class SimpleAuthenticationProvider
throws GuacamoleException {
// Pull cached configurations, if any
if (authenticatedUser instanceof SimpleAuthenticatedUser)
if (authenticatedUser instanceof SimpleAuthenticatedUser && authenticatedUser.getAuthenticationProvider() == this)
return ((SimpleAuthenticatedUser) authenticatedUser).getAuthorizedConfigurations();
// Otherwise, pull using credentials

View File

@@ -115,40 +115,6 @@ public class GuacamoleSession {
this.authenticatedUser = authenticatedUser;
}
/**
* Returns the UserContext associated with this session.
*
* @return The UserContext associated with this session.
*/
public UserContext getUserContext() {
// Warn of deprecation
logger.debug(
"\n****************************************************************"
+ "\n"
+ "\n !!!! PLEASE DO NOT USE getUserContext() !!!!"
+ "\n"
+ "\n getUserContext() has been replaced by getUserContexts(), which"
+ "\n properly handles multiple authentication providers. All use of"
+ "\n the old getUserContext() must be removed before GUAC-586 can"
+ "\n be considered complete."
+ "\n"
+ "\n****************************************************************"
);
// Return the UserContext associated with the AuthenticationProvider
// that authenticated the current user.
String authProviderIdentifier = authenticatedUser.getAuthenticationProvider().getIdentifier();
for (UserContext userContext : userContexts) {
if (userContext.getAuthenticationProvider().getIdentifier().equals(authProviderIdentifier))
return userContext;
}
// If not found, return null
return null;
}
/**
* Returns a list of all UserContexts associated with this session. Each
* AuthenticationProvider currently loaded by Guacamole may provide its own

View File

@@ -31,7 +31,7 @@ import javax.servlet.http.HttpServletRequest;
*
* @author Michael Jumper
*/
public class HTTPTunnelRequest implements TunnelRequest {
public class HTTPTunnelRequest extends TunnelRequest {
/**
* The wrapped HttpServletRequest.

View File

@@ -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
@@ -23,68 +23,105 @@
package org.glyptodon.guacamole.net.basic;
import java.util.List;
import org.glyptodon.guacamole.GuacamoleClientException;
import org.glyptodon.guacamole.GuacamoleException;
/**
* Request interface which provides only the functions absolutely required
* to retrieve and connect to a tunnel.
* A request object which provides only the functions absolutely required to
* retrieve and connect to a tunnel.
*
* @author Michael Jumper
*/
public interface TunnelRequest {
public abstract class TunnelRequest {
/**
* All supported identifier types.
* The name of the request parameter containing the user's authentication
* token.
*/
public static enum IdentifierType {
public static final String AUTH_TOKEN_PARAMETER = "token";
/**
* The unique identifier of a connection.
* The name of the parameter containing the identifier of the
* AuthenticationProvider associated with the UserContext containing the
* object to which a tunnel is being requested.
*/
CONNECTION("c/"),
public static final String AUTH_PROVIDER_IDENTIFIER_PARAMETER = "GUAC_DATA_SOURCE";
/**
* The unique identifier of a connection group.
* The name of the parameter specifying the type of object to which a
* tunnel is being requested. Currently, this may be "c" for a Guacamole
* connection, or "g" for a Guacamole connection group.
*/
CONNECTION_GROUP("g/");
public static final String TYPE_PARAMETER = "GUAC_TYPE";
/**
* The prefix which precedes an identifier of this type.
* The name of the parameter containing the unique identifier of the object
* to which a tunnel is being requested.
*/
final String PREFIX;
public static final String IDENTIFIER_PARAMETER = "GUAC_ID";
/**
* Defines an IdentifierType having the given prefix.
* @param prefix The prefix which will precede any identifier of this
* type, thus differentiating it from other identifier
* types.
* The name of the parameter containing the desired display width, in
* pixels.
*/
IdentifierType(String prefix) {
PREFIX = prefix;
}
public static final String WIDTH_PARAMETER = "GUAC_WIDTH";
/**
* Given an identifier, determines the corresponding identifier type.
* The name of the parameter containing the desired display height, in
* pixels.
*/
public static final String HEIGHT_PARAMETER = "GUAC_HEIGHT";
/**
* The name of the parameter containing the desired display resolution, in
* DPI.
*/
public static final String DPI_PARAMETER = "GUAC_DPI";
/**
* The name of the parameter specifying one supported audio mimetype. This
* will normally appear multiple times within a single tunnel request -
* once for each mimetype.
*/
public static final String AUDIO_PARAMETER = "GUAC_AUDIO";
/**
* The name of the parameter specifying one supported video mimetype. This
* will normally appear multiple times within a single tunnel request -
* once for each mimetype.
*/
public static final String VIDEO_PARAMETER = "GUAC_VIDEO";
/**
* All supported object types that can be used as the destination of a
* tunnel.
*/
public static enum Type {
/**
* A Guacamole connection.
*/
CONNECTION("c"),
/**
* A Guacamole connection group.
*/
CONNECTION_GROUP("g");
/**
* The parameter value which denotes a destination object of this type.
*/
final String PARAMETER_VALUE;
/**
* Defines a Type having the given corresponding parameter value.
*
* @param identifier The identifier whose type should be identified.
* @return The identified identifier type.
* @param value
* The parameter value which denotes a destination object of this
* type.
*/
static IdentifierType getType(String identifier) {
// If null, no known identifier
if (identifier == null)
return null;
// Connection identifiers
if (identifier.startsWith(CONNECTION.PREFIX))
return CONNECTION;
// Connection group identifiers
if (identifier.startsWith(CONNECTION_GROUP.PREFIX))
return CONNECTION_GROUP;
// Otherwise, unknown
return null;
Type(String value) {
PARAMETER_VALUE = value;
}
};
@@ -92,19 +129,225 @@ public interface TunnelRequest {
/**
* Returns the value of the parameter having the given name.
*
* @param name The name of the parameter to return.
* @return The value of the parameter having the given name, or null
* if no such parameter was specified.
* @param name
* The name of the parameter to return.
*
* @return
* The value of the parameter having the given name, or null if no such
* parameter was specified.
*/
public String getParameter(String name);
public abstract String getParameter(String name);
/**
* Returns a list of all values specified for the given parameter.
*
* @param name The name of the parameter to return.
* @return All values of the parameter having the given name , or null
* if no such parameter was specified.
* @param name
* The name of the parameter to return.
*
* @return
* All values of the parameter having the given name , or null if no
* such parameter was specified.
*/
public List<String> getParameterValues(String name);
public abstract List<String> 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<String> 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<String> getVideoMimetypes() {
return getParameterValues(VIDEO_PARAMETER);
}
}

View File

@@ -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<String> audio_mimetypes = request.getParameterValues("audio");
if (audio_mimetypes != null)
info.getAudioMimetypes().addAll(audio_mimetypes);
List<String> audioMimetypes = request.getAudioMimetypes();
if (audioMimetypes != null)
info.getAudioMimetypes().addAll(audioMimetypes);
// Add video mimetypes
List<String> video_mimetypes = request.getParameterValues("video");
if (video_mimetypes != null)
info.getVideoMimetypes().addAll(video_mimetypes);
List<String> 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);
}

View File

@@ -334,6 +334,9 @@ public class ExtensionModule extends ServletModule {
return;
}
// Sort files lexicographically
Arrays.sort(extensionFiles);
// Load each extension within the extension directory
for (File extensionFile : extensionFiles) {

View File

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

View File

@@ -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<String, APIActiveConnection> getActiveConnections(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@QueryParam("permission") List<ObjectPermission.Type> 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<APIPatch<String>> patches) throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken);
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the directory
Directory<ActiveConnection> activeConnectionDirectory = userContext.getActiveConnectionDirectory();

View File

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

View File

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

View File

@@ -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<String, String> 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<APIConnectionRecord> getConnectionHistory(@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
Connection connection = retrievalService.retrieveConnection(userContext, connectionID);
Connection connection = retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID);
// Retrieve the requested connection's history
List<APIConnectionRecord> apiRecords = new ArrayList<APIConnectionRecord>();
@@ -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<Connection> 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)

View File

@@ -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.
*
@@ -93,12 +98,14 @@ public class ConnectionGroupRESTService {
@Path("/{connectionGroupID}")
@AuthProviderRESTExposure
public APIConnectionGroup getConnectionGroup(@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);
// 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<ObjectPermission.Type> 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<ConnectionGroup> 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)

View File

@@ -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<Form> getUserAttributes(@QueryParam("token") String authToken) throws GuacamoleException {
public Collection<Form> 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<Form> getConnectionAttributes(@QueryParam("token") String authToken) throws GuacamoleException {
public Collection<Form> 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<Form> getConnectionGroupAttributes(@QueryParam("token") String authToken) throws GuacamoleException {
public Collection<Form> 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<String, ProtocolInfo> getProtocols(@QueryParam("token") String authToken) throws GuacamoleException {
public Map<String, ProtocolInfo> 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();

View File

@@ -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 {
@@ -122,13 +125,17 @@ public class UserRESTService {
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.
@@ -145,17 +152,15 @@ public class UserRESTService {
@GET
@AuthProviderRESTExposure
public List<APIUser> getUsers(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@QueryParam("permission") List<ObjectPermission.Type> 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<String> 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,33 +206,48 @@ 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<User> 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,10 +287,12 @@ 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<User> 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,11 +351,13 @@ public class UserRESTService {
@Path("/{username}/password")
@AuthProviderRESTExposure
public void updatePassword(@QueryParam("token") String authToken,
@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();
@@ -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,10 +416,12 @@ 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<User> 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;
@@ -490,6 +538,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 modify the permissions of.
*
@@ -503,10 +555,12 @@ public class UserRESTService {
@Path("/{username}/permissions")
@AuthProviderRESTExposure
public void patchPermissions(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username,
List<APIPatch<String>> 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);

View File

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

View File

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

View File

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

View File

@@ -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);
}
};
@@ -220,12 +220,12 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$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 = {};

View File

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

View File

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

View File

@@ -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.<String, ConnectionGroup>
*/
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.<String, Number>
* @type Object.<String, Object.<String, Number>>
*/
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,
// If connection groups are given, add them to the interface
if (connectionGroups) {
// 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, wrap that group as the child of a fake root group
// If root group is to be shown, add it as a root item
if ($scope.showRootGroup)
$scope.rootItem = new GroupListItem({
isConnectionGroup : true,
isBalancing : false,
children : [ rootItem ]
$scope.rootItems.push(rootItem);
// Otherwise, add its children as root items
else {
angular.forEach(rootItem.children, function addRootItem(child) {
$scope.rootItems.push(child);
});
}
});
// If not wrapped, only the descendants of the root will be shown
// 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
$scope.rootItem = rootItem;
connectionCount[dataSource][identifier] = 1;
});
});
});
}
else
$scope.rootItem = null;
});

View File

@@ -57,7 +57,7 @@
</div>
<!-- Pager for connections / groups -->
<guac-pager page="childrenPage" items="rootItem.children | orderBy : 'name'"
<guac-pager page="childrenPage" items="rootItems | orderBy : 'name'"
page-size="pageSize"></guac-pager>
</div>

View File

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

View File

@@ -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.<String, ConnectionGroup>
*/
$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;
});
}]);

View File

@@ -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.<String, ConnectionGroup>
*/
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
@@ -92,13 +95,21 @@ 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;
};
@@ -106,27 +117,39 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
* 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();

View File

@@ -1,4 +1,4 @@
<a ng-href="#/client/c/{{item.identifier}}">
<a ng-href="#/client/{{context.getClientIdentifier(item)}}">
<!--
Copyright (C) 2014 Glyptodon LLC

View File

@@ -21,6 +21,6 @@
THE SOFTWARE.
-->
<a ng-show="item.isBalancing" ng-href="#/client/g/{{item.identifier}}">{{item.name}}</a>
<a ng-show="item.isBalancing" ng-href="#/client/{{context.getClientIdentifier(item)}}">{{item.name}}</a>
<span ng-show="!item.isBalancing">{{item.name}}</span>
</span>

View File

@@ -30,14 +30,15 @@
<guac-user-menu></guac-user-menu>
</div>
<div class="recent-connections">
<guac-recent-connections root-group="rootConnectionGroup"></guac-recent-connections>
<guac-recent-connections root-groups="rootConnectionGroups"></guac-recent-connections>
</div>
<!-- All connections for this user -->
<h2 class="header">{{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}</h2>
<div class="all-connections">
<guac-group-list
connection-group="rootConnectionGroup"
context="context"
connection-groups="rootConnectionGroups"
connection-template="'app/home/templates/connection.html'"
connection-group-template="'app/home/templates/connectionGroup.html'"
page-size="20"></guac-group-list>

View File

@@ -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',

View File

@@ -56,6 +56,14 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
}
};
/**
* 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,19 +186,23 @@ 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())
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

View File

@@ -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,12 +131,13 @@ 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())
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

View File

@@ -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,17 +28,22 @@ 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 $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');
/**
@@ -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,25 +113,13 @@ 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.<String, ConnectionGroup>
*/
$scope.rootGroup = 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;
$scope.rootGroups = null;
/**
* All permissions associated with the current user, or null if the user's
@@ -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())
// 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');
})

View File

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

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2014 Glyptodon LLC.
Copyright 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
@@ -20,43 +20,51 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<div class="view" ng-class="{loading: !isLoaded()}">
<div class="manage-user view" ng-class="{loading: !isLoaded()}">
<!-- Main property editor -->
<div class="header">
<h2>{{'MANAGE_USER.SECTION_HEADER_EDIT_USER' | translate}}</h2>
<!-- User header and data source tabs -->
<div class="username header">
<h2>{{user.username}}</h2>
<guac-user-menu></guac-user-menu>
</div>
<div class="page-tabs">
<guac-page-list pages="accountPages"></guac-page-list>
</div>
<!-- Warn if user is read-only -->
<div class="section" ng-show="isReadOnly()">
<p class="notice read-only">{{'MANAGE_USER.INFO_READ_ONLY' | translate}}</p>
</div>
<!-- Sections applicable to non-read-only users -->
<div ng-show="!isReadOnly()">
<!-- User password section -->
<div class="section">
<table class="properties">
<tr>
<th>{{'MANAGE_USER.FIELD_HEADER_USERNAME' | translate}}</th>
<td>{{user.username}}</td>
</tr>
<tr>
<th>{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}</th>
<td><input ng-model="user.password" type="password" /></td>
</tr>
<tr>
<th>{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}</th>
<td><input ng-model="passwordMatch" type="password" /></td>
</tr>
</table>
</div>
<!-- User attributes section -->
<div class="attributes">
<div class="attributes" ng-show="canChangeAttributes()">
<guac-form namespace="'USER_ATTRIBUTES'" content="attributes" model="user.attributes"></guac-form>
</div>
<!-- System permissions section -->
<div class="system-permissions" ng-show="canChangePermissions()">
<h2 class="header">{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}</h2>
<div class="section">
<table class="properties">
<tr ng-repeat="systemPermissionType in systemPermissionTypes">
<tr ng-repeat="systemPermissionType in systemPermissionTypes"
ng-show="canChangeSystemPermissions()">
<th>{{systemPermissionType.label | translate}}</th>
<td><input type="checkbox" ng-model="permissionFlags.systemPermissions[systemPermissionType.value]"
ng-change="systemPermissionChanged(systemPermissionType.value)"/></td>
@@ -68,22 +76,28 @@ THE SOFTWARE.
</tr>
</table>
</div>
</div>
<!-- Connection permissions section -->
<div class="connection-permissions" ng-show="canChangePermissions()">
<h2 class="header">{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}</h2>
<div class="section" ng-class="{loading: !rootGroup}">
<div class="section">
<guac-group-list
context="groupListContext"
connection-group="rootGroup"
connection-groups="rootGroups"
connection-template="'app/manage/templates/connectionPermission.html'"
connection-group-template="'app/manage/templates/connectionGroupPermission.html'"
page-size="20"/>
</div>
</div>
</div>
<!-- Form action buttons -->
<div class="action-buttons">
<button ng-show="canSaveUser" ng-click="saveUser()">{{'MANAGE_USER.ACTION_SAVE' | translate}}</button>
<button ng-show="canSaveUser()" ng-click="saveUser()">{{'MANAGE_USER.ACTION_SAVE' | translate}}</button>
<button ng-click="cancel()">{{'MANAGE_USER.ACTION_CANCEL' | translate}}</button>
<button ng-show="canDeleteUser" ng-click="deleteUser()" class="danger">{{'MANAGE_USER.ACTION_DELETE' | translate}}</button>
<button ng-show="canDeleteUser()" ng-click="deleteUser()" class="danger">{{'MANAGE_USER.ACTION_DELETE' | translate}}</button>
</div>
</div>

View File

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

View File

@@ -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.<String, PageDefinition>[]
*/
$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
};

View File

@@ -23,4 +23,8 @@
/**
* Module for generating and implementing user navigation options.
*/
angular.module('navigation', ['notification', 'rest']);
angular.module('navigation', [
'auth',
'notification',
'rest'
]);

View File

@@ -27,91 +27,104 @@ angular.module('navigation').factory('userPageService', ['$injector',
function userPageService($injector) {
// Get required types
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 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.<String, ConnectionGroup>} 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) {
var homePage = null;
// Determine whether a connection or balancing group should serve as
// the home page
for (var dataSource in rootGroups) {
// Get corresponding root group
var rootGroup = rootGroups[dataSource];
// Get children
var connections = rootGroup.childConnections || [];
var connectionGroups = rootGroup.childConnectionGroups || [];
// Use main connection list screen as home if multiple connections
// are available
if (connections.length + connectionGroups.length === 1) {
// 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) {
return new Page(
connection.name,
'/client/c/' + connection.identifier
);
homePage = new PageDefinition({
name : connection.name,
url : '/client/' + ClientIdentifier.toString({
dataSource : dataSource,
type : ClientIdentifier.Types.CONNECTION,
id : connection.identifier
})
});
}
// Only one connection present, use as home page
// Only one balancing group 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
);
homePage = new PageDefinition({
name : connectionGroup.name,
url : '/client/' + ClientIdentifier.toString({
dataSource : dataSource,
type : ClientIdentifier.Types.CONNECTION_GROUP,
id : connectionGroup.identifier
})
});
}
}
// Resolve promise with default home page
return SYSTEM_HOME_PAGE;
// Otherwise, a connection or balancing group cannot serve as the
// home page
else {
homePage = null;
break;
}
} // end for each data source
// 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,27 +157,38 @@ 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.<String, PermissionSet>} 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 = [];
var canManageUsers = [];
var canManageConnections = [];
var canManageSessions = [];
// Inspect the contents of each provided permission set
angular.forEach(permissionSets, function inspectPermissions(permissions, dataSource) {
permissions = angular.copy(permissions);
// Ignore permission to update root group
PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER);
PermissionSet.removeConnectionGroupPermission(permissions,
PermissionSet.ObjectPermissionType.UPDATE,
ConnectionGroup.ROOT_IDENTIFIER);
// Ignore permission to update self
PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, authenticationService.getCurrentUsername());
PermissionSet.removeUserPermission(permissions,
PermissionSet.ObjectPermissionType.UPDATE,
authenticationService.getCurrentUsername());
// Determine whether the current user needs access to the user management UI
var canManageUsers =
if (
// System permissions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER)
@@ -172,11 +200,12 @@ angular.module('navigation').factory('userPageService', ['$injector',
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
// Permission to administer users
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER);
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER)
)
canManageUsers.push(dataSource);
// Determine whether the current user needs access to the connection management UI
var canManageConnections =
if (
// System permissions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION)
@@ -192,42 +221,51 @@ angular.module('navigation').factory('userPageService', ['$injector',
// Permission to administer connections or connection groups
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER)
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER);
var canManageSessions =
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER)
)
canManageConnections.push(dataSource);
// 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);
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
)
canManageSessions.push(dataSource);
});
// 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(
// 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',
'/settings/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.<String, ConnectionGroup>} 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.<String, PermissionSet>} 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();
});

View File

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

View File

@@ -1,4 +1,4 @@
<ul class="page-list">
<div class="page-list" ng-show="levels.length">
<!--
Copyright (C) 2015 Glyptodon LLC
@@ -22,11 +22,13 @@
-->
<!-- Navigation links -->
<li ng-repeat="page in pages">
<ul class="page-list-level" ng-repeat="level in levels track by $index">
<li ng-repeat="page in getPages(level)" class="{{page.className}}">
<a class="home" ng-click="navigateToPage(page)"
ng-class="{current: isCurrentPage(page)}" href="#{{page.url}}">
{{page.name | translate}}
</a>
</li>
</ul>
</ul>
</div>

View File

@@ -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.<String, String>
*/
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;
}]);

View File

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

View File

@@ -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.<Object.<String, ActiveConnection>>}
* 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.<Object.<String, Object.<String, ActiveConnection>>>}
* 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
});

View File

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

View File

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

View File

@@ -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.<Object.<String, *>>}
* 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;
}]);

View File

@@ -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,7 +65,7 @@ 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
});
@@ -70,6 +76,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.
*
@@ -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
})

View File

@@ -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.<Form[]>}
* 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.<Form[]>}
* 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.<Form[]>}
* 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.<Object.<String, Protocol>>}
* 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
});

View File

@@ -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
});
@@ -77,13 +83,18 @@ angular.module('rest').factory('userService', ['$injector',
* 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.<User>}
* 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,

View File

@@ -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) {

View File

@@ -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.<String, ConnectionGroup>
*/
$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.<String, PermissionSet>
*/
$scope.permissions = null;
@@ -112,56 +124,141 @@ 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;
&& $scope.permissions !== null;
};
// Retrieve current permissions
permissionService.getPermissions(currentUsername)
.success(function permissionsRetrieved(permissions) {
/**
* 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() {
$scope.permissions = permissions;
// 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);
// Determine whether the current user can create new users
$scope.canCreateConnections =
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION);
// For each data source
for (var dataSource in $scope.permissions) {
// 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);
// Retrieve corresponding permission set
var permissionSet = $scope.permissions[dataSource];
// Determine whether the current user can manage other connections or groups
$scope.canManageConnections =
// Can manage connections if granted explicit update or delete
if (PermissionSet.hasConnectionPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE)
|| PermissionSet.hasConnectionPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE))
return true;
// Permission to manage connections
$scope.canCreateConnections
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
// 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;
// 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('/');
// No data sources allow management of connections or groups
return false;
};
// Retrieve current permissions
dataSourceService.apply(
permissionService.getPermissions,
[$scope.dataSource],
currentUsername
)
.then(function permissionsRetrieved(permissions) {
$scope.permissions = permissions;
});
// 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;
});
}]

View File

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

View File

@@ -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.<String, Object.<String, ActiveConnection>>
*/
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.<String, Connection>
* @type Object.<String, Object.<String, Connection>>
*/
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.<String, ActiveConnectionWrapper>
* @type Object.<String, Object.<String, ActiveConnectionWrapper>>
*/
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
dataSourceService.apply(
connectionGroupService.getConnectionGroupTree,
dataSources,
ConnectionGroup.ROOT_IDENTIFIER
)
.then(function connectionGroupsReceived(rootGroups) {
allConnections = {};
// Load connections from each received root group
angular.forEach(rootGroups, function connectionGroupReceived(rootGroup, dataSource) {
allConnections[dataSource] = {};
addDescendantConnections(dataSource, rootGroup);
});
// Retrieve all connections
connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER)
.success(function connectionGroupReceived(retrievedRootGroup) {
// Load connections from retrieved group tree
connections = {};
addDescendantConnections(retrievedRootGroup);
// 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)
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;

View File

@@ -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.<String, PermissionSet>
*/
$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('/');
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
}));
});
});
// Retrieve all users for whom we have UPDATE or DELETE permission
userService.getUsers([PermissionSet.ObjectPermissionType.UPDATE,
PermissionSet.ObjectPermissionType.DELETE])
.success(function usersReceived(users) {
// 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));
};
}]

View File

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

View File

@@ -1,4 +1,4 @@
<a ng-href="#/manage/connections/{{item.identifier}}">
<a ng-href="#/manage/{{item.dataSource}}/connections/{{item.identifier}}">
<!--
Copyright (C) 2014 Glyptodon LLC

View File

@@ -1,4 +1,4 @@
<a ng-href="#/manage/connectionGroups/{{item.identifier}}">
<a ng-href="#/manage/{{item.dataSource}}/connectionGroups/{{item.identifier}}">
<!--
Copyright (C) 2014 Glyptodon LLC

View File

@@ -28,8 +28,8 @@ THE SOFTWARE.
</div>
<!-- Available tabs -->
<div class="settings-tabs">
<guac-page-list pages="settingsPages" ng-show="showAvailableTabs()"></guac-page-list>
<div class="page-tabs">
<guac-page-list pages="settingsPages"></guac-page-list>
</div>
<!-- Selected tab -->

View File

@@ -29,11 +29,11 @@
<a class="add-connection button"
ng-show="canCreateConnections"
href="#/manage/connections/">{{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | translate}}</a>
href="#/manage/{{dataSource}}/connections/">{{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | translate}}</a>
<a class="add-connection-group button"
ng-show="canCreateConnectionGroups"
href="#/manage/connectionGroups/">{{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION_GROUP' | translate}}</a>
href="#/manage/{{dataSource}}/connectionGroups/">{{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION_GROUP' | translate}}</a>
</div>
@@ -41,7 +41,7 @@
<div class="connection-list">
<guac-group-list
page-size="25"
connection-group="rootGroup"
connection-groups="rootGroups"
connection-template="'app/settings/templates/connection.html'"
connection-group-template="'app/settings/templates/connectionGroup.html'"/>
</div>

View File

@@ -25,24 +25,25 @@
<p>{{'SETTINGS_USERS.HELP_USERS' | translate}}</p>
<!-- Form action buttons -->
<div class="action-buttons" ng-show="canCreateUsers">
<div class="action-buttons" ng-show="canCreateUsers()">
<input type="text" ng-model="newUsername" class="name username" autocorrect="off" autocapitalize="off"/>
<button class="add-user" ng-click="newUser()">{{'SETTINGS_USERS.ACTION_NEW_USER' | translate}}</button>
</div>
<!-- List of users this user has access to -->
<div class="user-list">
<div ng-repeat="user in userPage" class="user list-item">
<a ng-href="#/manage/users/{{user.username}}">
<div ng-repeat="manageableUser in manageableUserPage" class="user list-item">
<a ng-href="#/manage/{{manageableUser.dataSource}}/users/{{manageableUser.user.username}}">
<div class="caption">
<div class="icon user"></div>
<span class="name">{{user.username}}</span>
<span class="name">{{manageableUser.user.username}}</span>
</div>
</a>
</div>
</div>
<!-- Pager controls for user list -->
<guac-pager page="userPage" page-size="25" items="users | orderBy : 'username'"></guac-pager>
<guac-pager page="manageableUserPage" page-size="25"
items="manageableUsers | orderBy : 'user.username'"></guac-pager>
</div>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -125,6 +125,10 @@
},
"DATA_SOURCE_DEFAULT" : {
"NAME" : "Default (XML)"
},
"FORM" : {
"FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD",
@@ -241,6 +245,8 @@
"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",