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.setParentIdentifier(object.getParentIdentifier());
connection.setName(object.getName()); connection.setName(object.getName());
connection.setConfiguration(object.getConfiguration()); connection.setConfiguration(object.getConfiguration());
connection.setAttributes(object.getAttributes());
return model; return model;

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,10 @@
"authProviders" : [ "authProviders" : [
"net.sourceforge.guacamole.net.auth.ldap.LDAPAuthenticationProvider" "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" : [ "authProviders" : [
"net.sourceforge.guacamole.net.auth.noauth.NoAuthenticationProvider" "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 { throws GuacamoleException {
// Pull cached configurations, if any // Pull cached configurations, if any
if (authenticatedUser instanceof SimpleAuthenticatedUser) if (authenticatedUser instanceof SimpleAuthenticatedUser && authenticatedUser.getAuthenticationProvider() == this)
return ((SimpleAuthenticatedUser) authenticatedUser).getAuthorizedConfigurations(); return ((SimpleAuthenticatedUser) authenticatedUser).getAuthorizedConfigurations();
// Otherwise, pull using credentials // Otherwise, pull using credentials

View File

@@ -115,40 +115,6 @@ public class GuacamoleSession {
this.authenticatedUser = authenticatedUser; 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 * Returns a list of all UserContexts associated with this session. Each
* AuthenticationProvider currently loaded by Guacamole may provide its own * AuthenticationProvider currently loaded by Guacamole may provide its own

View File

@@ -31,7 +31,7 @@ import javax.servlet.http.HttpServletRequest;
* *
* @author Michael Jumper * @author Michael Jumper
*/ */
public class HTTPTunnelRequest implements TunnelRequest { public class HTTPTunnelRequest extends TunnelRequest {
/** /**
* The wrapped HttpServletRequest. * 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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -23,88 +23,331 @@
package org.glyptodon.guacamole.net.basic; package org.glyptodon.guacamole.net.basic;
import java.util.List; import java.util.List;
import org.glyptodon.guacamole.GuacamoleClientException;
import org.glyptodon.guacamole.GuacamoleException;
/** /**
* Request interface which provides only the functions absolutely required * A request object which provides only the functions absolutely required to
* to retrieve and connect to a tunnel. * retrieve and connect to a tunnel.
* *
* @author Michael Jumper * @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 name of the parameter containing the identifier of the
* AuthenticationProvider associated with the UserContext containing the
* object to which a tunnel is being requested.
*/
public static final String AUTH_PROVIDER_IDENTIFIER_PARAMETER = "GUAC_DATA_SOURCE";
/**
* 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.
*/
public static final String TYPE_PARAMETER = "GUAC_TYPE";
/**
* The name of the parameter containing the unique identifier of the object
* to which a tunnel is being requested.
*/
public static final String IDENTIFIER_PARAMETER = "GUAC_ID";
/**
* The name of the parameter containing the desired display width, in
* pixels.
*/
public static final String WIDTH_PARAMETER = "GUAC_WIDTH";
/**
* 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 {
/** /**
* The unique identifier of a connection. * A Guacamole connection.
*/ */
CONNECTION("c/"), CONNECTION("c"),
/** /**
* The unique identifier of a connection group. * A Guacamole connection group.
*/ */
CONNECTION_GROUP("g/"); CONNECTION_GROUP("g");
/**
* The parameter value which denotes a destination object of this type.
*/
final String PARAMETER_VALUE;
/** /**
* The prefix which precedes an identifier of this type. * Defines a Type having the given corresponding parameter value.
*
* @param value
* The parameter value which denotes a destination object of this
* type.
*/ */
final String PREFIX; Type(String value) {
PARAMETER_VALUE = value;
/**
* 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.
*/
IdentifierType(String prefix) {
PREFIX = prefix;
} }
/**
* Given an identifier, determines the corresponding identifier type.
*
* @param identifier The identifier whose type should be identified.
* @return The identified identifier 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;
}
}; };
/** /**
* Returns the value of the parameter having the given name. * Returns the value of the parameter having the given name.
* *
* @param name The name of the parameter to return. * @param name
* @return The value of the parameter having the given name, or null * The name of the parameter to return.
* if no such parameter was specified. *
* @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. * Returns a list of all values specified for the given parameter.
* *
* @param name The name of the parameter to return. * @param name
* @return All values of the parameter having the given name , or null * The name of the parameter to return.
* if no such parameter was specified. *
* @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.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.util.List; import java.util.List;
import org.glyptodon.guacamole.GuacamoleClientException;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.GuacamoleSecurityException;
import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel; import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel;
import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.GuacamoleTunnel;
import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.Connection;
import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.UserContext; 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.net.basic.rest.auth.AuthenticationService;
import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -53,12 +52,6 @@ import org.slf4j.LoggerFactory;
@Singleton @Singleton
public class TunnelRequestService { public class TunnelRequestService {
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/** /**
* Logger for this class. * Logger for this class.
*/ */
@@ -70,6 +63,12 @@ public class TunnelRequestService {
@Inject @Inject
private AuthenticationService authenticationService; private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/** /**
* Reads and returns the client information provided within the given * Reads and returns the client information provided within the given
* request. * request.
@@ -80,35 +79,40 @@ public class TunnelRequestService {
* @return GuacamoleClientInformation * @return GuacamoleClientInformation
* An object containing information about the client sending the tunnel * An object containing information about the client sending the tunnel
* request. * 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 // Get client information
GuacamoleClientInformation info = new GuacamoleClientInformation(); GuacamoleClientInformation info = new GuacamoleClientInformation();
// Set width if provided // Set width if provided
String width = request.getParameter("width"); Integer width = request.getWidth();
if (width != null) if (width != null)
info.setOptimalScreenWidth(Integer.parseInt(width)); info.setOptimalScreenWidth(width);
// Set height if provided // Set height if provided
String height = request.getParameter("height"); Integer height = request.getHeight();
if (height != null) if (height != null)
info.setOptimalScreenHeight(Integer.parseInt(height)); info.setOptimalScreenHeight(height);
// Set resolution if provided // Set resolution if provided
String dpi = request.getParameter("dpi"); Integer dpi = request.getDPI();
if (dpi != null) if (dpi != null)
info.setOptimalResolution(Integer.parseInt(dpi)); info.setOptimalResolution(dpi);
// Add audio mimetypes // Add audio mimetypes
List<String> audio_mimetypes = request.getParameterValues("audio"); List<String> audioMimetypes = request.getAudioMimetypes();
if (audio_mimetypes != null) if (audioMimetypes != null)
info.getAudioMimetypes().addAll(audio_mimetypes); info.getAudioMimetypes().addAll(audioMimetypes);
// Add video mimetypes // Add video mimetypes
List<String> video_mimetypes = request.getParameterValues("video"); List<String> videoMimetypes = request.getVideoMimetypes();
if (video_mimetypes != null) if (videoMimetypes != null)
info.getVideoMimetypes().addAll(video_mimetypes); info.getVideoMimetypes().addAll(videoMimetypes);
return info; return info;
} }
@@ -122,7 +126,7 @@ public class TunnelRequestService {
* The UserContext associated with the user for whom the tunnel is * The UserContext associated with the user for whom the tunnel is
* being created. * being created.
* *
* @param idType * @param type
* The type of object being connected to (connection or group). * The type of object being connected to (connection or group).
* *
* @param id * @param id
@@ -138,13 +142,13 @@ public class TunnelRequestService {
* If an error occurs while creating the tunnel. * If an error occurs while creating the tunnel.
*/ */
protected GuacamoleTunnel createConnectedTunnel(UserContext context, protected GuacamoleTunnel createConnectedTunnel(UserContext context,
final TunnelRequest.IdentifierType idType, String id, final TunnelRequest.Type type, String id,
GuacamoleClientInformation info) GuacamoleClientInformation info)
throws GuacamoleException { throws GuacamoleException {
// Create connected tunnel from identifier // Create connected tunnel from identifier
GuacamoleTunnel tunnel = null; GuacamoleTunnel tunnel = null;
switch (idType) { switch (type) {
// Connection identifiers // Connection identifiers
case CONNECTION: { case CONNECTION: {
@@ -205,7 +209,7 @@ public class TunnelRequestService {
* @param session * @param session
* The Guacamole session to associate the tunnel with. * The Guacamole session to associate the tunnel with.
* *
* @param idType * @param type
* The type of object being connected to (connection or group). * The type of object being connected to (connection or group).
* *
* @param id * @param id
@@ -220,7 +224,7 @@ public class TunnelRequestService {
* If an error occurs while obtaining the tunnel. * If an error occurs while obtaining the tunnel.
*/ */
protected GuacamoleTunnel createAssociatedTunnel(final GuacamoleSession session, protected GuacamoleTunnel createAssociatedTunnel(final GuacamoleSession session,
GuacamoleTunnel tunnel, final TunnelRequest.IdentifierType idType, GuacamoleTunnel tunnel, final TunnelRequest.Type type,
final String id) throws GuacamoleException { final String id) throws GuacamoleException {
// Monitor tunnel closure and data // Monitor tunnel closure and data
@@ -239,7 +243,7 @@ public class TunnelRequestService {
long duration = connectionEndTime - connectionStartTime; long duration = connectionEndTime - connectionStartTime;
// Log closure // Log closure
switch (idType) { switch (type) {
// Connection identifiers // Connection identifiers
case CONNECTION: case CONNECTION:
@@ -289,27 +293,20 @@ public class TunnelRequestService {
public GuacamoleTunnel createTunnel(TunnelRequest request) public GuacamoleTunnel createTunnel(TunnelRequest request)
throws GuacamoleException { throws GuacamoleException {
// Get auth token and session // Parse request parameters
final String authToken = request.getParameter("authToken"); String authToken = request.getAuthenticationToken();
final GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); String id = request.getIdentifier();
TunnelRequest.Type type = request.getType();
// Get client information and connection ID from request String authProviderIdentifier = request.getAuthenticationProviderIdentifier();
String id = request.getParameter("id"); GuacamoleClientInformation info = getClientInformation(request);
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());
// Create connected tunnel using provided connection ID and client information // 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 // Associate tunnel with session
return createAssociatedTunnel(session, tunnel, idType, id); return createAssociatedTunnel(session, tunnel, type, id);
} }

View File

@@ -333,7 +333,10 @@ public class ExtensionModule extends ServletModule {
logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read."); logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read.");
return; return;
} }
// Sort files lexicographically
Arrays.sort(extensionFiles);
// Load each extension within the extension directory // Load each extension within the extension directory
for (File extensionFile : extensionFiles) { for (File extensionFile : extensionFiles) {

View File

@@ -25,6 +25,7 @@ package org.glyptodon.guacamole.net.basic.rest;
import java.util.List; import java.util.List;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; 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.Connection;
import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.Directory;
@@ -48,9 +49,9 @@ public class ObjectRetrievalService {
* @param session * @param session
* The GuacamoleSession to retrieve the UserContext from. * The GuacamoleSession to retrieve the UserContext from.
* *
* @param identifier * @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider that created the * 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. * AuthenticationProvider can exist.
* *
* @return * @return
@@ -62,7 +63,7 @@ public class ObjectRetrievalService {
* UserContext does not exist. * UserContext does not exist.
*/ */
public UserContext retrieveUserContext(GuacamoleSession session, public UserContext retrieveUserContext(GuacamoleSession session,
String identifier) throws GuacamoleException { String authProviderIdentifier) throws GuacamoleException {
// Get list of UserContexts // Get list of UserContexts
List<UserContext> userContexts = session.getUserContexts(); List<UserContext> userContexts = session.getUserContexts();
@@ -70,11 +71,17 @@ public class ObjectRetrievalService {
// Locate and return the UserContext associated with the // Locate and return the UserContext associated with the
// AuthenticationProvider having the given identifier, if any // AuthenticationProvider having the given identifier, if any
for (UserContext userContext : userContexts) { 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; 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. * 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 * Retrieves a single connection group from the given user context. If
* the given identifier the REST API root identifier, the root connection * 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.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType; 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.ObjectPermissionSet;
import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission;
import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; 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.APIPatch;
import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; 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.PATCH;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -56,7 +59,7 @@ import org.slf4j.LoggerFactory;
* *
* @author Michael Jumper * @author Michael Jumper
*/ */
@Path("/activeConnections") @Path("/data/{dataSource}/activeConnections")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public class ActiveConnectionRESTService { public class ActiveConnectionRESTService {
@@ -72,6 +75,12 @@ public class ActiveConnectionRESTService {
@Inject @Inject
private AuthenticationService authenticationService; 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 * Gets a list of active connections in the system, filtering the returned
* list by the given permissions, if specified. * list by the given permissions, if specified.
@@ -80,6 +89,10 @@ public class ActiveConnectionRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the active connections to be retrieved.
*
* @param permissions * @param permissions
* The set of permissions to filter with. A user must have one or more * The set of permissions to filter with. A user must have one or more
* of these permissions for a user to appear in the result. * of these permissions for a user to appear in the result.
@@ -96,10 +109,12 @@ public class ActiveConnectionRESTService {
@GET @GET
@AuthProviderRESTExposure @AuthProviderRESTExposure
public Map<String, APIActiveConnection> getActiveConnections(@QueryParam("token") String authToken, public Map<String, APIActiveConnection> getActiveConnections(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@QueryParam("permission") List<ObjectPermission.Type> permissions) @QueryParam("permission") List<ObjectPermission.Type> permissions)
throws GuacamoleException { throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
User self = userContext.self(); User self = userContext.self();
// Do not filter on permissions if no permissions are specified // 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 * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the active connections to be deleted.
*
* @param patches * @param patches
* The active connection patches to apply for this request. * The active connection patches to apply for this request.
* *
@@ -149,9 +168,11 @@ public class ActiveConnectionRESTService {
@PATCH @PATCH
@AuthProviderRESTExposure @AuthProviderRESTExposure
public void patchTunnels(@QueryParam("token") String authToken, public void patchTunnels(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
List<APIPatch<String>> patches) throws GuacamoleException { 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 // Get the directory
Directory<ActiveConnection> activeConnectionDirectory = userContext.getActiveConnectionDirectory(); 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 * Returns all UserContexts associated with a given auth token, if the auth
* token represents a currently logged in user. Throws an unauthorized * 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.ObjectPermissionSet;
import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission;
import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; 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.AuthProviderRESTExposure;
import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
@@ -60,7 +61,7 @@ import org.slf4j.LoggerFactory;
* *
* @author James Muehlner * @author James Muehlner
*/ */
@Path("/connections") @Path("/data/{dataSource}/connections")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public class ConnectionRESTService { public class ConnectionRESTService {
@@ -89,6 +90,10 @@ public class ConnectionRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection to be retrieved.
*
* @param connectionID * @param connectionID
* The identifier of the connection to retrieve. * The identifier of the connection to retrieve.
* *
@@ -102,12 +107,14 @@ public class ConnectionRESTService {
@Path("/{connectionID}") @Path("/{connectionID}")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public APIConnection getConnection(@QueryParam("token") String authToken, 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 // 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 * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param connectionID
* The identifier of the connection. * The identifier of the connection.
* *
@@ -131,9 +143,12 @@ public class ConnectionRESTService {
@Path("/{connectionID}/parameters") @Path("/{connectionID}/parameters")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public Map<String, String> getConnectionParameters(@QueryParam("token") String authToken, 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(); User self = userContext.self();
// Retrieve permission sets // Retrieve permission sets
@@ -163,6 +178,11 @@ public class ConnectionRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param connectionID
* The identifier of the connection. * The identifier of the connection.
* *
@@ -177,12 +197,14 @@ public class ConnectionRESTService {
@Path("/{connectionID}/history") @Path("/{connectionID}/history")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public List<APIConnectionRecord> getConnectionHistory(@QueryParam("token") String authToken, public List<APIConnectionRecord> getConnectionHistory(@QueryParam("token") String authToken,
@PathParam("connectionID") String connectionID) throws GuacamoleException { @PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionID") String connectionID)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = authenticationService.getUserContext(authToken);
// Retrieve the requested connection // Retrieve the requested connection
Connection connection = retrievalService.retrieveConnection(userContext, connectionID); Connection connection = retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID);
// Retrieve the requested connection's history // Retrieve the requested connection's history
List<APIConnectionRecord> apiRecords = new ArrayList<APIConnectionRecord>(); List<APIConnectionRecord> apiRecords = new ArrayList<APIConnectionRecord>();
@@ -201,6 +223,10 @@ public class ConnectionRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection to be deleted.
*
* @param connectionID * @param connectionID
* The identifier of the connection to delete. * The identifier of the connection to delete.
* *
@@ -210,10 +236,13 @@ public class ConnectionRESTService {
@DELETE @DELETE
@Path("/{connectionID}") @Path("/{connectionID}")
@AuthProviderRESTExposure @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 { throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the connection directory // Get the connection directory
Directory<Connection> connectionDirectory = userContext.getConnectionDirectory(); Directory<Connection> connectionDirectory = userContext.getConnectionDirectory();
@@ -231,6 +260,10 @@ public class ConnectionRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param connection
* The connection to create. * The connection to create.
* *
@@ -244,9 +277,11 @@ public class ConnectionRESTService {
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
@AuthProviderRESTExposure @AuthProviderRESTExposure
public String createConnection(@QueryParam("token") String authToken, public String createConnection(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
APIConnection connection) throws GuacamoleException { 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 // Validate that connection data was provided
if (connection == null) if (connection == null)
@@ -270,6 +305,10 @@ public class ConnectionRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection to be updated.
*
* @param connectionID * @param connectionID
* The identifier of the connection to update. * The identifier of the connection to update.
* *
@@ -283,9 +322,12 @@ public class ConnectionRESTService {
@Path("/{connectionID}") @Path("/{connectionID}")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public void updateConnection(@QueryParam("token") String authToken, 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 // Validate that connection data was provided
if (connection == null) 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.Directory;
import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; 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.AuthProviderRESTExposure;
import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
@@ -51,7 +52,7 @@ import org.slf4j.LoggerFactory;
* *
* @author James Muehlner * @author James Muehlner
*/ */
@Path("/connectionGroups") @Path("/data/{dataSource}/connectionGroups")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public class ConnectionGroupRESTService { public class ConnectionGroupRESTService {
@@ -80,6 +81,10 @@ public class ConnectionGroupRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection group to be retrieved.
*
* @param connectionGroupID * @param connectionGroupID
* The ID of the connection group to retrieve. * The ID of the connection group to retrieve.
* *
@@ -92,13 +97,15 @@ public class ConnectionGroupRESTService {
@GET @GET
@Path("/{connectionGroupID}") @Path("/{connectionGroupID}")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken, 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 // 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 * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection group to be retrieved.
*
* @param connectionGroupID * @param connectionGroupID
* The ID of the connection group to retrieve. * The ID of the connection group to retrieve.
* *
@@ -129,11 +140,13 @@ public class ConnectionGroupRESTService {
@Path("/{connectionGroupID}/tree") @Path("/{connectionGroupID}/tree")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken, public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionGroupID") String connectionGroupID, @PathParam("connectionGroupID") String connectionGroupID,
@QueryParam("permission") List<ObjectPermission.Type> permissions) @QueryParam("permission") List<ObjectPermission.Type> permissions)
throws GuacamoleException { 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 // Retrieve the requested tree, filtering by the given permissions
ConnectionGroup treeRoot = retrievalService.retrieveConnectionGroup(userContext, connectionGroupID); ConnectionGroup treeRoot = retrievalService.retrieveConnectionGroup(userContext, connectionGroupID);
@@ -151,6 +164,10 @@ public class ConnectionGroupRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection group to be deleted.
*
* @param connectionGroupID * @param connectionGroupID
* The identifier of the connection group to delete. * The identifier of the connection group to delete.
* *
@@ -161,9 +178,12 @@ public class ConnectionGroupRESTService {
@Path("/{connectionGroupID}") @Path("/{connectionGroupID}")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public void deleteConnectionGroup(@QueryParam("token") String authToken, 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 // Get the connection group directory
Directory<ConnectionGroup> connectionGroupDirectory = userContext.getConnectionGroupDirectory(); Directory<ConnectionGroup> connectionGroupDirectory = userContext.getConnectionGroupDirectory();
@@ -183,6 +203,10 @@ public class ConnectionGroupRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param connectionGroup
* The connection group to create. * The connection group to create.
* *
@@ -196,9 +220,11 @@ public class ConnectionGroupRESTService {
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
@AuthProviderRESTExposure @AuthProviderRESTExposure
public String createConnectionGroup(@QueryParam("token") String authToken, public String createConnectionGroup(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
APIConnectionGroup connectionGroup) throws GuacamoleException { 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 // Validate that connection group data was provided
if (connectionGroup == null) if (connectionGroup == null)
@@ -222,6 +248,10 @@ public class ConnectionGroupRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection group to be updated.
*
* @param connectionGroupID * @param connectionGroupID
* The identifier of the existing connection group to update. * The identifier of the existing connection group to update.
* *
@@ -235,10 +265,13 @@ public class ConnectionGroupRESTService {
@Path("/{connectionGroupID}") @Path("/{connectionGroupID}")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public void updateConnectionGroup(@QueryParam("token") String authToken, 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 { 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 // Validate that connection group data was provided
if (connectionGroup == null) if (connectionGroup == null)

View File

@@ -28,6 +28,7 @@ import java.util.Map;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType; 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.environment.LocalEnvironment;
import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.form.Form;
import org.glyptodon.guacamole.net.auth.UserContext; 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.AuthProviderRESTExposure;
import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
import org.glyptodon.guacamole.protocols.ProtocolInfo; import org.glyptodon.guacamole.protocols.ProtocolInfo;
@@ -46,7 +49,7 @@ import org.glyptodon.guacamole.protocols.ProtocolInfo;
* *
* @author Michael Jumper * @author Michael Jumper
*/ */
@Path("/schema") @Path("/schema/{dataSource}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public class SchemaRESTService { public class SchemaRESTService {
@@ -57,6 +60,12 @@ public class SchemaRESTService {
@Inject @Inject
private AuthenticationService authenticationService; private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/** /**
* Retrieves the possible attributes of a user object. * 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 * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext dictating the available user attributes.
*
* @return * @return
* A collection of forms which describe the possible attributes of a * A collection of forms which describe the possible attributes of a
* user object. * user object.
@@ -74,10 +87,13 @@ public class SchemaRESTService {
@GET @GET
@Path("/users/attributes") @Path("/users/attributes")
@AuthProviderRESTExposure @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 // Retrieve all possible user attributes
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
return userContext.getUserAttributes(); return userContext.getUserAttributes();
} }
@@ -89,6 +105,10 @@ public class SchemaRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext dictating the available connection attributes.
*
* @return * @return
* A collection of forms which describe the possible attributes of a * A collection of forms which describe the possible attributes of a
* connection object. * connection object.
@@ -99,10 +119,13 @@ public class SchemaRESTService {
@GET @GET
@Path("/connections/attributes") @Path("/connections/attributes")
@AuthProviderRESTExposure @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 // Retrieve all possible connection attributes
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
return userContext.getConnectionAttributes(); return userContext.getConnectionAttributes();
} }
@@ -114,6 +137,11 @@ public class SchemaRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * performing the operation.
* *
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext dictating the available connection group
* attributes.
*
* @return * @return
* A collection of forms which describe the possible attributes of a * A collection of forms which describe the possible attributes of a
* connection group object. * connection group object.
@@ -124,10 +152,13 @@ public class SchemaRESTService {
@GET @GET
@Path("/connectionGroups/attributes") @Path("/connectionGroups/attributes")
@AuthProviderRESTExposure @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 // 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(); return userContext.getConnectionGroupAttributes();
} }
@@ -139,6 +170,13 @@ public class SchemaRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @return
* A map of protocol information, where each key is the unique name * A map of protocol information, where each key is the unique name
* associated with that protocol. * associated with that protocol.
@@ -149,10 +187,13 @@ public class SchemaRESTService {
@GET @GET
@Path("/protocols") @Path("/protocols")
@AuthProviderRESTExposure @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 // Verify the given auth token and auth provider identifier are valid
authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get and return a map of all protocols. // Get and return a map of all protocols.
Environment env = new LocalEnvironment(); Environment env = new LocalEnvironment();

View File

@@ -41,6 +41,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; 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.Credentials;
import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.User; 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.Permission;
import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission;
import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; 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.APIError;
import org.glyptodon.guacamole.net.basic.rest.APIPatch; import org.glyptodon.guacamole.net.basic.rest.APIPatch;
import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.add; 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. * A REST Service for handling user CRUD operations.
* *
* @author James Muehlner * @author James Muehlner
* @author Michael Jumper
*/ */
@Path("/users") @Path("/data/{dataSource}/users")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public class UserRESTService { public class UserRESTService {
@@ -120,42 +123,44 @@ public class UserRESTService {
*/ */
@Inject @Inject
private ObjectRetrievalService retrievalService; private ObjectRetrievalService retrievalService;
/** /**
* Gets a list of users in the system, filtering the returned list by the * Gets a list of users in the given data source (UserContext), filtering
* given permission, if specified. * the returned list by the given permission, if specified.
* *
* @param authToken * @param authToken
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param permissions
* The set of permissions to filter with. A user must have one or more * The set of permissions to filter with. A user must have one or more
* of these permissions for a user to appear in the result. * of these permissions for a user to appear in the result.
* If null, no filtering will be performed. * If null, no filtering will be performed.
* *
* @return * @return
* A list of all visible users. If a permission was specified, this * A list of all visible users. If a permission was specified, this
* list will contain only those users for whom the current user has * list will contain only those users for whom the current user has
* that permission. * that permission.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If an error is encountered while retrieving users. * If an error is encountered while retrieving users.
*/ */
@GET @GET
@AuthProviderRESTExposure @AuthProviderRESTExposure
public List<APIUser> getUsers(@QueryParam("token") String authToken, public List<APIUser> getUsers(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@QueryParam("permission") List<ObjectPermission.Type> permissions) @QueryParam("permission") List<ObjectPermission.Type> permissions)
throws GuacamoleException { throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
User self = userContext.self(); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Do not filter on permissions if no permissions are specified
if (permissions != null && permissions.isEmpty())
permissions = null;
// An admin user has access to any user // An admin user has access to any user
User self = userContext.self();
SystemPermissionSet systemPermissions = self.getSystemPermissions(); SystemPermissionSet systemPermissions = self.getSystemPermissions();
boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER); boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER);
@@ -164,7 +169,7 @@ public class UserRESTService {
// Filter users, if requested // Filter users, if requested
Collection<String> userIdentifiers = userDirectory.getIdentifiers(); Collection<String> userIdentifiers = userDirectory.getIdentifiers();
if (!isAdmin && permissions != null) { if (!isAdmin && permissions != null && !permissions.isEmpty()) {
ObjectPermissionSet userPermissions = self.getUserPermissions(); ObjectPermissionSet userPermissions = self.getUserPermissions();
userIdentifiers = userPermissions.getAccessibleObjects(permissions, userIdentifiers); userIdentifiers = userPermissions.getAccessibleObjects(permissions, userIdentifiers);
} }
@@ -174,7 +179,6 @@ public class UserRESTService {
for (User user : userDirectory.getAll(userIdentifiers)) for (User user : userDirectory.getAll(userIdentifiers))
apiUsers.add(new APIUser(user)); apiUsers.add(new APIUser(user));
// Return the converted user list
return apiUsers; return apiUsers;
} }
@@ -186,6 +190,10 @@ public class UserRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param username
* The username of the user to retrieve. * The username of the user to retrieve.
* *
@@ -198,34 +206,49 @@ public class UserRESTService {
@GET @GET
@Path("/{username}") @Path("/{username}")
@AuthProviderRESTExposure @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 { throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
// Retrieve the requested user // Retrieve the requested user
User user = retrievalService.retrieveUser(userContext, username); User user = retrievalService.retrieveUser(session, authProviderIdentifier, username);
return new APIUser(user); return new APIUser(user);
} }
/** /**
* Creates a new user and returns the username. * Creates a new user and returns the username.
* @param authToken The authentication token that is used to authenticate *
* the user performing the operation. * @param authToken
* @param user The new user to create. * The authentication token that is used to authenticate the user
* @throws GuacamoleException If a problem is encountered while creating the user. * performing the operation.
* *
* @return The username of the newly created user. * @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 @POST
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
@AuthProviderRESTExposure @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 { throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the directory // Get the directory
Directory<User> userDirectory = userContext.getUserDirectory(); Directory<User> userDirectory = userContext.getUserDirectory();
@@ -247,6 +270,10 @@ public class UserRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param username
* The username of the user to update. * The username of the user to update.
* *
@@ -260,11 +287,13 @@ public class UserRESTService {
@Path("/{username}") @Path("/{username}")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public void updateUser(@QueryParam("token") String authToken, public void updateUser(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username, APIUser user) @PathParam("username") String username, APIUser user)
throws GuacamoleException { throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the directory // Get the directory
Directory<User> userDirectory = userContext.getUserDirectory(); Directory<User> userDirectory = userContext.getUserDirectory();
@@ -301,6 +330,10 @@ public class UserRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param username
* The username of the user to update. * The username of the user to update.
* *
@@ -318,12 +351,14 @@ public class UserRESTService {
@Path("/{username}/password") @Path("/{username}/password")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public void updatePassword(@QueryParam("token") String authToken, public void updatePassword(@QueryParam("token") String authToken,
@PathParam("username") String username, @PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username,
APIUserPasswordUpdate userPasswordUpdate, APIUserPasswordUpdate userPasswordUpdate,
@Context HttpServletRequest request) throws GuacamoleException { @Context HttpServletRequest request) throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Build credentials // Build credentials
Credentials credentials = new Credentials(); Credentials credentials = new Credentials();
credentials.setUsername(username); credentials.setUsername(username);
@@ -333,7 +368,8 @@ public class UserRESTService {
// Verify that the old password was correct // Verify that the old password was correct
try { try {
if (userContext.getAuthenticationProvider().authenticateUser(credentials) == null) { AuthenticationProvider authProvider = userContext.getAuthenticationProvider();
if (authProvider.authenticateUser(credentials) == null) {
throw new APIException(APIError.Type.PERMISSION_DENIED, throw new APIException(APIError.Type.PERMISSION_DENIED,
"Permission denied."); "Permission denied.");
} }
@@ -366,6 +402,10 @@ public class UserRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param username
* The username of the user to delete. * The username of the user to delete.
* *
@@ -376,11 +416,13 @@ public class UserRESTService {
@Path("/{username}") @Path("/{username}")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public void deleteUser(@QueryParam("token") String authToken, public void deleteUser(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username) @PathParam("username") String username)
throws GuacamoleException { throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the directory // Get the directory
Directory<User> userDirectory = userContext.getUserDirectory(); Directory<User> userDirectory = userContext.getUserDirectory();
@@ -401,6 +443,10 @@ public class UserRESTService {
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param username
* The username of the user to retrieve permissions for. * The username of the user to retrieve permissions for.
* *
@@ -414,10 +460,12 @@ public class UserRESTService {
@Path("/{username}/permissions") @Path("/{username}/permissions")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public APIPermissionSet getPermissions(@QueryParam("token") String authToken, public APIPermissionSet getPermissions(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username) @PathParam("username") String username)
throws GuacamoleException { throws GuacamoleException {
UserContext userContext = authenticationService.getUserContext(authToken); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
User user; User user;
@@ -489,7 +537,11 @@ public class UserRESTService {
* @param authToken * @param authToken
* The authentication token that is used to authenticate the user * The authentication token that is used to authenticate the user
* performing the operation. * 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 * @param username
* The username of the user to modify the permissions of. * The username of the user to modify the permissions of.
* *
@@ -503,11 +555,13 @@ public class UserRESTService {
@Path("/{username}/permissions") @Path("/{username}/permissions")
@AuthProviderRESTExposure @AuthProviderRESTExposure
public void patchPermissions(@QueryParam("token") String authToken, public void patchPermissions(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username, @PathParam("username") String username,
List<APIPatch<String>> patches) throws GuacamoleException { 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 // Get the user
User user = userContext.getUserDirectory().get(username); User user = userContext.getUserDirectory().get(username);
if (user == null) if (user == null)

View File

@@ -32,7 +32,7 @@ import org.glyptodon.guacamole.net.basic.TunnelRequest;
* *
* @author Michael Jumper * @author Michael Jumper
*/ */
public class WebSocketTunnelRequest implements TunnelRequest { public class WebSocketTunnelRequest extends TunnelRequest {
/** /**
* All parameters passed via HTTP to the WebSocket handshake. * 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 * @author Michael Jumper
*/ */
public class WebSocketTunnelRequest implements TunnelRequest { public class WebSocketTunnelRequest extends TunnelRequest {
/** /**
* All parameters passed via HTTP to the WebSocket handshake. * All parameters passed via HTTP to the WebSocket handshake.

View File

@@ -99,9 +99,10 @@ angular.module('auth').factory('authenticationService', ['$injector',
// Store auth data // Store auth data
$cookieStore.put(AUTH_COOKIE_ID, { $cookieStore.put(AUTH_COOKIE_ID, {
authToken : data.authToken, 'authToken' : data.authToken,
username : data.username, 'username' : data.username,
dataSource : data.dataSource 'dataSource' : data.dataSource,
'availableDataSources' : data.availableDataSources
}); });
// Process is complete // Process is complete
@@ -320,7 +321,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
* *
* @returns {String[]} * @returns {String[]}
* The identifiers of all data sources availble to the current user, * 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() { service.getAvailableDataSources = function getAvailableDataSources() {
@@ -330,7 +331,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
return authData.availableDataSources; return authData.availableDataSources;
// No auth data present // No auth data present
return null; return [];
}; };

View File

@@ -158,7 +158,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
var RECONNECT_ACTION = { var RECONNECT_ACTION = {
name : "CLIENT.ACTION_RECONNECT", name : "CLIENT.ACTION_RECONNECT",
callback : function reconnectCallback() { callback : function reconnectCallback() {
$scope.client = guacClientManager.replaceManagedClient(uniqueId, $routeParams.params); $scope.client = guacClientManager.replaceManagedClient($routeParams.id, $routeParams.params);
guacNotification.showStatus(false); guacNotification.showStatus(false);
} }
}; };
@@ -219,13 +219,13 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) { $scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) {
$scope.clipboardData = clipboardData; $scope.clipboardData = clipboardData;
}); });
/* /**
* Parse the type, name, and id out of the url paramteres, * The client which should be attached to the client UI.
* as well as any extra parameters if set. *
* @type ManagedClient
*/ */
var uniqueId = $routeParams.type + '/' + $routeParams.id; $scope.client = guacClientManager.getManagedClient($routeParams.id, $routeParams.params);
$scope.client = guacClientManager.getManagedClient(uniqueId, $routeParams.params);
var keysCurrentlyPressed = {}; var keysCurrentlyPressed = {};

View File

@@ -28,6 +28,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Required types // Required types
var ClientProperties = $injector.get('ClientProperties'); var ClientProperties = $injector.get('ClientProperties');
var ClientIdentifier = $injector.get('ClientIdentifier');
var ManagedClientState = $injector.get('ManagedClientState'); var ManagedClientState = $injector.get('ManagedClientState');
var ManagedDisplay = $injector.get('ManagedDisplay'); var ManagedDisplay = $injector.get('ManagedDisplay');
var ManagedFileDownload = $injector.get('ManagedFileDownload'); 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 * desired connection ID, display resolution, and supported audio/video
* codecs. * codecs.
* *
* @param {String} id * @param {ClientIdentifier} identifier
* The ID of the connection or group to connect to. * The identifier representing the connection or group to connect to.
* *
* @param {String} [connectionParameters] * @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. * 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 * The string of connection parameters to be passed to the Guacamole
* client. * client.
*/ */
var getConnectString = function getConnectString(id, connectionParameters) { var getConnectString = function getConnectString(identifier, connectionParameters) {
// Calculate optimal width/height for display // Calculate optimal width/height for display
var pixel_density = $window.devicePixelRatio || 1; var pixel_density = $window.devicePixelRatio || 1;
@@ -173,21 +174,23 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Build base connect string // Build base connect string
var connectString = var connectString =
"id=" + encodeURIComponent(id) "token=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&authToken=" + encodeURIComponent(authenticationService.getCurrentToken()) + "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource)
+ "&width=" + Math.floor(optimal_width) + "&GUAC_ID=" + encodeURIComponent(identifier.id)
+ "&height=" + Math.floor(optimal_height) + "&GUAC_TYPE=" + encodeURIComponent(identifier.type)
+ "&dpi=" + Math.floor(optimal_dpi) + "&GUAC_WIDTH=" + Math.floor(optimal_width)
+ "&GUAC_HEIGHT=" + Math.floor(optimal_height)
+ "&GUAC_DPI=" + Math.floor(optimal_dpi)
+ (connectionParameters ? '&' + connectionParameters : ''); + (connectionParameters ? '&' + connectionParameters : '');
// Add audio mimetypes to connect_string // Add audio mimetypes to connect_string
guacAudio.supported.forEach(function(mimetype) { guacAudio.supported.forEach(function(mimetype) {
connectString += "&audio=" + encodeURIComponent(mimetype); connectString += "&GUAC_AUDIO=" + encodeURIComponent(mimetype);
}); });
// Add video mimetypes to connect_string // Add video mimetypes to connect_string
guacVideo.supported.forEach(function(mimetype) { guacVideo.supported.forEach(function(mimetype) {
connectString += "&video=" + encodeURIComponent(mimetype); connectString += "&GUAC_VIDEO=" + encodeURIComponent(mimetype);
}); });
return connectString; return connectString;
@@ -238,7 +241,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
* or group. * or group.
* *
* @param {String} id * @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] * @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. * Any additional HTTP parameters to pass while connecting.
@@ -402,23 +407,23 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
// Manage the client display // Manage the client display
managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay());
// Connect the Guacamole client // Parse connection details from ID
client.connect(getConnectString(id, connectionParameters)); var clientIdentifier = ClientIdentifier.fromString(id);
// Determine type of connection // Connect the Guacamole client
var typePrefix = id.substring(0, 2); client.connect(getConnectString(clientIdentifier, connectionParameters));
// If using a connection, pull connection name // If using a connection, pull connection name
if (typePrefix === 'c/') { if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
connectionService.getConnection(id.substring(2)) connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id)
.success(function connectionRetrieved(connection) { .success(function connectionRetrieved(connection) {
managedClient.name = connection.name; managedClient.name = connection.name;
}); });
} }
// If using a connection group, pull connection name // If using a connection group, pull connection name
else if (typePrefix === 'g/') { else if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION_GROUP) {
connectionGroupService.getConnectionGroup(id.substring(2)) connectionGroupService.getConnectionGroup(clientIdentifier.dataSource, clientIdentifier.id)
.success(function connectionGroupRetrieved(group) { .success(function connectionGroupRetrieved(group) {
managedClient.name = group.name; managedClient.name = group.name;
}); });

View File

@@ -697,15 +697,15 @@ angular.module('form').controller('timeZoneFieldController', ['$scope', '$inject
*/ */
$scope.region = ''; $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 // Ensure corresponding region is selected
$scope.$watch('model', function setModel(model) { $scope.$watch('model', function setModel(model) {
$scope.region = timeZoneRegions[model] || ''; $scope.region = timeZoneRegions[model] || '';
selectedTimeZone[$scope.region] = 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: { 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 * Arbitrary object which shall be made available to the connection
@@ -92,43 +93,38 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
// Required services // Required services
var activeConnectionService = $injector.get('activeConnectionService'); var activeConnectionService = $injector.get('activeConnectionService');
var dataSourceService = $injector.get('dataSourceService');
// Required types // Required types
var GroupListItem = $injector.get('GroupListItem'); var GroupListItem = $injector.get('GroupListItem');
/** /**
* The number of active connections associated with a given * Map of data source identifier to the number of active
* connection identifier. If this information is unknown, or there * connections associated with a given connection identifier.
* are no active connections for a given identifier, no number will * If this information is unknown, or there are no active
* be stored. * connections for a given identifier, no number will be stored.
* *
* @type Object.<String, Number> * @type Object.<String, Object.<String, Number>>
*/ */
var connectionCount = {}; var connectionCount = {};
// Count active connections by connection identifier /**
activeConnectionService.getActiveConnections() * A list of all items which should appear at the root level. As
.success(function activeConnectionsRetrieved(activeConnections) { * connections and connection groups from multiple data sources may
* be included in a guacGroupList, there may be multiple root
// Count each active connection by identifier * items, even if the root connection group is shown.
angular.forEach(activeConnections, function addActiveConnection(activeConnection) { *
* @type GroupListItem[]
// If counter already exists, increment */
var identifier = activeConnection.connectionIdentifier; $scope.rootItems = [];
if (connectionCount[identifier])
connectionCount[identifier]++;
// Otherwise, initialize counter to 1
else
connectionCount[identifier] = 1;
});
});
/** /**
* Returns the number of active usages of a given connection. * 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 * @param {Connection} connection
* The connection whose active connections should be counted. * 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 * The number of currently-active usages of the given
* connection. * connection.
*/ */
var countActiveConnections = function countActiveConnections(connection) { var countActiveConnections = function countActiveConnections(dataSource, connection) {
return connectionCount[connection.identifier]; 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 // 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 // If connection groups are given, add them to the interface
var rootItem = GroupListItem.fromConnectionGroup(connectionGroup, if (connectionGroups) {
!!$scope.connectionTemplate, countActiveConnections);
// If root group is to be shown, wrap that group as the child of a fake root group // Add each provided connection group
if ($scope.showRootGroup) angular.forEach(connectionGroups, function addConnectionGroup(connectionGroup, dataSource) {
$scope.rootItem = new GroupListItem({
isConnectionGroup : true, // Prepare data source for active connection counting
isBalancing : false, dataSources.push(dataSource);
children : [ rootItem ] connectionCount[dataSource] = {};
// Create root item for current connection group
var rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup,
!!$scope.connectionTemplate, countActiveConnections);
// If root group is to be shown, add it as a root item
if ($scope.showRootGroup)
$scope.rootItems.push(rootItem);
// Otherwise, add its children as root items
else {
angular.forEach(rootItem.children, function addRootItem(child) {
$scope.rootItems.push(child);
});
}
});
// Count active connections by connection identifier
dataSourceService.apply(
activeConnectionService.getActiveConnections,
dataSources
)
.then(function activeConnectionsRetrieved(activeConnectionMap) {
// Within each data source, count each active connection by identifier
angular.forEach(activeConnectionMap, function addActiveConnections(activeConnections, dataSource) {
angular.forEach(activeConnections, function addActiveConnection(activeConnection) {
// If counter already exists, increment
var identifier = activeConnection.connectionIdentifier;
if (connectionCount[dataSource][identifier])
connectionCount[dataSource][identifier]++;
// Otherwise, initialize counter to 1
else
connectionCount[dataSource][identifier] = 1;
});
}); });
// If not wrapped, only the descendants of the root will be shown });
else
$scope.rootItem = rootItem;
} }
else
$scope.rootItem = null;
}); });

View File

@@ -57,7 +57,7 @@
</div> </div>
<!-- Pager for connections / groups --> <!-- 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> page-size="pageSize"></guac-pager>
</div> </div>

View File

@@ -39,6 +39,14 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
// Use empty object by default // Use empty object by default
template = template || {}; 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 * The unique identifier associated with the connection or connection
* group this item represents. * 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. * 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 * @param {ConnectionGroup} connection
* The connection whose contents should be represented by the new * The connection whose contents should be represented by the new
* GroupListItem. * GroupListItem.
@@ -131,12 +143,15 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
* @param {Function} [countActiveConnections] * @param {Function} [countActiveConnections]
* A getter which returns the current number of active connections for * A getter which returns the current number of active connections for
* the given connection. If omitted, the number of active connections * 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} * @returns {GroupListItem}
* A new GroupListItem which represents the given connection. * 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 item representing the given connection
return new GroupListItem({ return new GroupListItem({
@@ -145,6 +160,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
name : connection.name, name : connection.name,
identifier : connection.identifier, identifier : connection.identifier,
protocol : connection.protocol, protocol : connection.protocol,
dataSource : dataSource,
// Type information // Type information
isConnection : true, isConnection : true,
@@ -155,7 +171,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
// Use getter, if provided // Use getter, if provided
if (countActiveConnections) if (countActiveConnections)
return countActiveConnections(connection); return countActiveConnections(dataSource, connection);
return connection.activeConnections; 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 * Creates a new GroupListItem using the contents and descendants of the
* given connection group. * given connection group.
* *
* @param {String} dataSource
* The identifier of the data source containing the given connection
* group.
*
* @param {ConnectionGroup} connectionGroup * @param {ConnectionGroup} connectionGroup
* The connection group whose contents and descendants should be * The connection group whose contents and descendants should be
* represented by the new GroupListItem and its descendants. * represented by the new GroupListItem and its descendants.
@@ -183,34 +203,41 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
* @param {Function} [countActiveConnections] * @param {Function} [countActiveConnections]
* A getter which returns the current number of active connections for * A getter which returns the current number of active connections for
* the given connection. If omitted, the number of active connections * 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] * @param {Function} [countActiveConnectionGroups]
* A getter which returns the current number of active connections for * A getter which returns the current number of active connections for
* the given connection group. If omitted, the number of active * the given connection group. If omitted, the number of active
* connections known at the time this function was called is used * 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} * @returns {GroupListItem}
* A new GroupListItem which represents the given connection group, * A new GroupListItem which represents the given connection group,
* including all descendants. * including all descendants.
*/ */
GroupListItem.fromConnectionGroup = function fromConnectionGroup(connectionGroup, GroupListItem.fromConnectionGroup = function fromConnectionGroup(dataSource,
includeConnections, countActiveConnections, countActiveConnectionGroups) { connectionGroup, includeConnections, countActiveConnections,
countActiveConnectionGroups) {
var children = []; var children = [];
// Add any child connections // Add any child connections
if (connectionGroup.childConnections && includeConnections !== false) { if (connectionGroup.childConnections && includeConnections !== false) {
connectionGroup.childConnections.forEach(function addChildConnection(child) { connectionGroup.childConnections.forEach(function addChildConnection(child) {
children.push(GroupListItem.fromConnection(child, countActiveConnections)); children.push(GroupListItem.fromConnection(dataSource, child,
countActiveConnections));
}); });
} }
// Add any child groups // Add any child groups
if (connectionGroup.childConnectionGroups) { if (connectionGroup.childConnectionGroups) {
connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { 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 // Identifying information
name : connectionGroup.name, name : connectionGroup.name,
identifier : connectionGroup.identifier, identifier : connectionGroup.identifier,
dataSource : dataSource,
// Type information // Type information
isConnection : false, isConnection : false,
@@ -234,7 +262,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
// Use getter, if provided // Use getter, if provided
if (countActiveConnectionGroups) if (countActiveConnectionGroups)
return countActiveConnectionGroups(connectionGroup); return countActiveConnectionGroups(dataSource, connectionGroup);
return connectionGroup.activeConnections; return connectionGroup.activeConnections;

View File

@@ -27,19 +27,22 @@ angular.module('home').controller('homeController', ['$scope', '$injector',
function homeController($scope, $injector) { function homeController($scope, $injector) {
// Get required types // Get required types
var ConnectionGroup = $injector.get("ConnectionGroup"); var ConnectionGroup = $injector.get('ConnectionGroup');
var ClientIdentifier = $injector.get('ClientIdentifier');
// Get required services // Get required services
var authenticationService = $injector.get("authenticationService"); var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get("connectionGroupService"); var connectionGroupService = $injector.get('connectionGroupService');
var dataSourceService = $injector.get('dataSourceService');
/** /**
* The root connection group, or null if the connection group hierarchy has * Map of data source identifier to the root connection group of that data
* not yet been loaded. * 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. * 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) * Object passed to the guacGroupList directive, providing context-specific
.success(function rootGroupRetrieved(rootConnectionGroup) { * functions or data.
$scope.rootConnectionGroup = rootConnectionGroup; */
$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: { scope: {
/** /**
* The root connection group, and all visible descendants. * The root connection groups to display, and all visible
* Recent connections will only be shown if they exist within this * descendants, as a map of data source identifier to the root
* hierarchy, regardless of their existence within the history. * 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 // Required types
var ActiveConnection = $injector.get('ActiveConnection'); var ActiveConnection = $injector.get('ActiveConnection');
var ClientIdentifier = $injector.get('ClientIdentifier');
var RecentConnection = $injector.get('RecentConnection'); var RecentConnection = $injector.get('RecentConnection');
// Required services // Required services
@@ -91,42 +94,62 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
/** /**
* Adds the given connection to the internal set of visible * Adds the given connection to the internal set of visible
* objects. * objects.
* *
* @param {String} dataSource
* The identifier of the data source associated with the
* given connection group.
*
* @param {Connection} connection * @param {Connection} connection
* The connection to add to the internal set of visible objects. * 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 // Add given connection to set of visible objects
visibleObjects['c/' + connection.identifier] = connection; visibleObjects[ClientIdentifier.toString({
dataSource : dataSource,
type : ClientIdentifier.Types.CONNECTION,
id : connection.identifier
})] = connection;
}; };
/** /**
* Adds the given connection group to the internal set of visible * Adds the given connection group to the internal set of visible
* objects, along with any descendants. * objects, along with any descendants.
* *
* @param {String} dataSource
* The identifier of the data source associated with the
* given connection group.
*
* @param {ConnectionGroup} connectionGroup * @param {ConnectionGroup} connectionGroup
* The connection group to add to the internal set of visible * The connection group to add to the internal set of visible
* objects, along with any descendants. * 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 // 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 // Add all child connections
if (connectionGroup.childConnections) if (connectionGroup.childConnections)
connectionGroup.childConnections.forEach(addVisibleConnection); connectionGroup.childConnections.forEach(function addChildConnection(child) {
addVisibleConnection(dataSource, child);
});
// Add all child connection groups // Add all child connection groups
if (connectionGroup.childConnectionGroups) if (connectionGroup.childConnectionGroups)
connectionGroup.childConnectionGroups.forEach(addVisibleConnectionGroup); connectionGroup.childConnectionGroups.forEach(function addChildConnectionGroup(child) {
addVisibleConnectionGroup(dataSource, child);
});
}; };
// Update visible objects when root group is set // Update visible objects when root groups are set
$scope.$watch("rootGroup", function setRootGroup(rootGroup) { $scope.$watch("rootGroups", function setRootGroups(rootGroups) {
// Clear connection arrays // Clear connection arrays
$scope.activeConnections = []; $scope.activeConnections = [];
@@ -134,8 +157,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
// Produce collection of visible objects // Produce collection of visible objects
visibleObjects = {}; visibleObjects = {};
if (rootGroup) if (rootGroups) {
addVisibleConnectionGroup(rootGroup); angular.forEach(rootGroups, function addConnectionGroup(rootGroup, dataSource) {
addVisibleConnectionGroup(dataSource, rootGroup);
});
}
var managedClients = guacClientManager.getManagedClients(); 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 Copyright (C) 2014 Glyptodon LLC

View File

@@ -21,6 +21,6 @@
THE SOFTWARE. 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 ng-show="!item.isBalancing">{{item.name}}</span>
</span> </span>

View File

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

View File

@@ -123,7 +123,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
}) })
// Management screen // Management screen
.when('/settings/:tab', { .when('/settings/:dataSource?/:tab', {
title : 'APP.NAME', title : 'APP.NAME',
bodyClassName : 'settings', bodyClassName : 'settings',
templateUrl : 'app/settings/templates/settings.html', templateUrl : 'app/settings/templates/settings.html',
@@ -132,7 +132,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
}) })
// Connection editor // Connection editor
.when('/manage/connections/:id?', { .when('/manage/:dataSource/connections/:id?', {
title : 'APP.NAME', title : 'APP.NAME',
bodyClassName : 'manage', bodyClassName : 'manage',
templateUrl : 'app/manage/templates/manageConnection.html', templateUrl : 'app/manage/templates/manageConnection.html',
@@ -141,7 +141,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
}) })
// Connection group editor // Connection group editor
.when('/manage/connectionGroups/:id?', { .when('/manage/:dataSource/connectionGroups/:id?', {
title : 'APP.NAME', title : 'APP.NAME',
bodyClassName : 'manage', bodyClassName : 'manage',
templateUrl : 'app/manage/templates/manageConnectionGroup.html', templateUrl : 'app/manage/templates/manageConnectionGroup.html',
@@ -150,7 +150,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
}) })
// User editor // User editor
.when('/manage/users/:id', { .when('/manage/:dataSource/users/:id', {
title : 'APP.NAME', title : 'APP.NAME',
bodyClassName : 'manage', bodyClassName : 'manage',
templateUrl : 'app/manage/templates/manageUser.html', templateUrl : 'app/manage/templates/manageUser.html',
@@ -159,7 +159,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
}) })
// Client view // Client view
.when('/client/:type/:id/:params?', { .when('/client/:id/:params?', {
bodyClassName : 'client', bodyClassName : 'client',
templateUrl : 'app/client/templates/client.html', templateUrl : 'app/client/templates/client.html',
controller : 'clientController', controller : 'clientController',

View File

@@ -55,7 +55,15 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
guacNotification.showStatus(false); guacNotification.showStatus(false);
} }
}; };
/**
* The unique identifier of the data source containing the connection being
* edited.
*
* @type String
*/
var dataSource = $routeParams.dataSource;
/** /**
* The identifier of the original connection from which this connection is * The identifier of the original connection from which this connection is
* being cloned. Only valid if this is a new connection. * being cloned. Only valid if this is a new connection.
@@ -178,20 +186,24 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
}; };
// Pull connection attribute schema // Pull connection attribute schema
schemaService.getConnectionAttributes().success(function attributesReceived(attributes) { schemaService.getConnectionAttributes(dataSource)
.success(function attributesReceived(attributes) {
$scope.attributes = attributes; $scope.attributes = attributes;
}); });
// Pull connection group hierarchy // Pull connection group hierarchy
connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, connectionGroupService.getConnectionGroupTree(
[PermissionSet.ObjectPermissionType.ADMINISTER]) dataSource,
ConnectionGroup.ROOT_IDENTIFIER,
[PermissionSet.ObjectPermissionType.ADMINISTER]
)
.success(function connectionGroupReceived(rootGroup) { .success(function connectionGroupReceived(rootGroup) {
$scope.rootGroup = rootGroup; $scope.rootGroup = rootGroup;
}); });
// Query the user's permissions for the current connection // Query the user's permissions for the current connection
permissionService.getPermissions(authenticationService.getCurrentUsername()) permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername())
.success(function permissionsReceived(permissions) { .success(function permissionsReceived(permissions) {
$scope.permissions = permissions; $scope.permissions = permissions;
@@ -220,7 +232,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
}); });
// Get protocol metadata // Get protocol metadata
schemaService.getProtocols().success(function protocolsReceived(protocols) { schemaService.getProtocols(dataSource)
.success(function protocolsReceived(protocols) {
$scope.protocols = protocols; $scope.protocols = protocols;
}); });
@@ -233,12 +246,14 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
if (identifier) { if (identifier) {
// Pull data from existing connection // Pull data from existing connection
connectionService.getConnection(identifier).success(function connectionRetrieved(connection) { connectionService.getConnection(dataSource, identifier)
.success(function connectionRetrieved(connection) {
$scope.connection = connection; $scope.connection = connection;
}); });
// Pull connection history // 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 // Wrap all history entries for sake of display
$scope.historyEntryWrappers = []; $scope.historyEntryWrappers = [];
@@ -249,7 +264,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
}); });
// Pull connection parameters // Pull connection parameters
connectionService.getConnectionParameters(identifier).success(function parametersReceived(parameters) { connectionService.getConnectionParameters(dataSource, identifier)
.success(function parametersReceived(parameters) {
$scope.parameters = parameters; $scope.parameters = parameters;
}); });
} }
@@ -258,7 +274,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
else if (cloneSourceIdentifier) { else if (cloneSourceIdentifier) {
// Pull data from cloned connection // Pull data from cloned connection
connectionService.getConnection(cloneSourceIdentifier).success(function connectionRetrieved(connection) { connectionService.getConnection(dataSource, cloneSourceIdentifier)
.success(function connectionRetrieved(connection) {
$scope.connection = connection; $scope.connection = connection;
// Clear the identifier field because this connection is new // Clear the identifier field because this connection is new
@@ -269,7 +286,8 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
$scope.historyEntryWrappers = []; $scope.historyEntryWrappers = [];
// Pull connection parameters from cloned connection // Pull connection parameters from cloned connection
connectionService.getConnectionParameters(cloneSourceIdentifier).success(function parametersReceived(parameters) { connectionService.getConnectionParameters(dataSource, cloneSourceIdentifier)
.success(function parametersReceived(parameters) {
$scope.parameters = parameters; $scope.parameters = parameters;
}); });
} }
@@ -332,7 +350,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
* Cancels all pending edits, returning to the management page. * Cancels all pending edits, returning to the management page.
*/ */
$scope.cancel = function cancel() { $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. * which is prepopulated with the data from the connection currently being edited.
*/ */
$scope.cloneConnection = function cloneConnection() { $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; $scope.connection.parameters = $scope.parameters;
// Save the connection // Save the connection
connectionService.saveConnection($scope.connection) connectionService.saveConnection(dataSource, $scope.connection)
.success(function savedConnection() { .success(function savedConnection() {
$location.path('/settings/connections'); $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections');
}) })
// Notify of any errors // Notify of any errors
@@ -402,9 +420,9 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
var deleteConnectionImmediately = function deleteConnectionImmediately() { var deleteConnectionImmediately = function deleteConnectionImmediately() {
// Delete the connection // Delete the connection
connectionService.deleteConnection($scope.connection) connectionService.deleteConnection(dataSource, $scope.connection)
.success(function deletedConnection() { .success(function deletedConnection() {
$location.path('/settings/connections'); $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections');
}) })
// Notify of any errors // 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 * The identifier of the connection group being edited. If a new connection
* group is being created, this will not be defined. * group is being created, this will not be defined.
@@ -123,13 +131,14 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope'
}; };
// Pull connection group attribute schema // Pull connection group attribute schema
schemaService.getConnectionGroupAttributes().success(function attributesReceived(attributes) { schemaService.getConnectionGroupAttributes(dataSource)
.success(function attributesReceived(attributes) {
$scope.attributes = attributes; $scope.attributes = attributes;
}); });
// Query the user's permissions for the current connection group // Query the user's permissions for the current connection group
permissionService.getPermissions(authenticationService.getCurrentUsername()) permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername())
.success(function permissionsReceived(permissions) { .success(function permissionsReceived(permissions) {
$scope.permissions = permissions; $scope.permissions = permissions;
@@ -150,14 +159,19 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope'
// Pull connection group hierarchy // 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) { .success(function connectionGroupReceived(rootGroup) {
$scope.rootGroup = rootGroup; $scope.rootGroup = rootGroup;
}); });
// If we are editing an existing connection group, pull its data // If we are editing an existing connection group, pull its data
if (identifier) { if (identifier) {
connectionGroupService.getConnectionGroup(identifier).success(function connectionGroupReceived(connectionGroup) { connectionGroupService.getConnectionGroup(dataSource, identifier)
.success(function connectionGroupReceived(connectionGroup) {
$scope.connectionGroup = connectionGroup; $scope.connectionGroup = connectionGroup;
}); });
} }
@@ -187,7 +201,7 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope'
* Cancels all pending edits, returning to the management page. * Cancels all pending edits, returning to the management page.
*/ */
$scope.cancel = function cancel() { $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() { $scope.saveConnectionGroup = function saveConnectionGroup() {
// Save the connection // Save the connection
connectionGroupService.saveConnectionGroup($scope.connectionGroup) connectionGroupService.saveConnectionGroup(dataSource, $scope.connectionGroup)
.success(function savedConnectionGroup() { .success(function savedConnectionGroup() {
$location.path('/settings/connections'); $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections');
}) })
// Notify of any errors // Notify of any errors
@@ -247,9 +261,9 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope'
var deleteConnectionGroupImmediately = function deleteConnectionGroupImmediately() { var deleteConnectionGroupImmediately = function deleteConnectionGroupImmediately() {
// Delete the connection group // Delete the connection group
connectionGroupService.deleteConnectionGroup($scope.connectionGroup) connectionGroupService.deleteConnectionGroup(dataSource, $scope.connectionGroup)
.success(function deletedConnectionGroup() { .success(function deletedConnectionGroup() {
$location.path('/settings/connections'); $location.path('/settings/' + encodeURIComponent(dataSource) + '/connections');
}) })
// Notify of any errors // 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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -28,18 +28,23 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
// Required types // Required types
var ConnectionGroup = $injector.get('ConnectionGroup'); var ConnectionGroup = $injector.get('ConnectionGroup');
var PageDefinition = $injector.get('PageDefinition');
var PermissionFlagSet = $injector.get('PermissionFlagSet'); var PermissionFlagSet = $injector.get('PermissionFlagSet');
var PermissionSet = $injector.get('PermissionSet'); var PermissionSet = $injector.get('PermissionSet');
var User = $injector.get('User');
// Required services // Required services
var $location = $injector.get('$location'); var $location = $injector.get('$location');
var $routeParams = $injector.get('$routeParams'); var $routeParams = $injector.get('$routeParams');
var authenticationService = $injector.get('authenticationService'); var $q = $injector.get('$q');
var connectionGroupService = $injector.get('connectionGroupService'); var authenticationService = $injector.get('authenticationService');
var guacNotification = $injector.get('guacNotification'); var connectionGroupService = $injector.get('connectionGroupService');
var permissionService = $injector.get('permissionService'); var dataSourceService = $injector.get('dataSourceService');
var schemaService = $injector.get('schemaService'); var guacNotification = $injector.get('guacNotification');
var userService = $injector.get('userService'); var permissionService = $injector.get('permissionService');
var schemaService = $injector.get('schemaService');
var translationStringService = $injector.get('translationStringService');
var userService = $injector.get('userService');
/** /**
* An action to be provided along with the object sent to showStatus which * An action to be provided along with the object sent to showStatus which
@@ -53,6 +58,29 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
} }
}; };
/**
* The identifiers of all data sources currently available to the
* authenticated user.
*
* @type String[]
*/
var dataSources = authenticationService.getAvailableDataSources();
/**
* The username of the current, authenticated user.
*
* @type String
*/
var currentUsername = authenticationService.getCurrentUsername();
/**
* The unique identifier of the data source containing the user being
* edited.
*
* @type String
*/
var dataSource = $routeParams.dataSource;
/** /**
* The username of the user being edited. * The username of the user being edited.
* *
@@ -60,6 +88,16 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
*/ */
var username = $routeParams.id; 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. * The user being modified.
* *
@@ -75,26 +113,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
$scope.permissionFlags = null; $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; $scope.rootGroups = null;
/**
* Whether the authenticated user has UPDATE permission for the user being edited.
*
* @type Boolean
*/
$scope.hasUpdatePermission = null;
/**
* Whether the authenticated user has DELETE permission for the user being edited.
*
* @type Boolean
*/
$scope.hasDeletePermission = null;
/** /**
* All permissions associated with the current user, or null if the user's * All permissions associated with the current user, or null if the user's
* permissions have not yet been loaded. * permissions have not yet been loaded.
@@ -112,6 +138,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
*/ */
$scope.attributes = null; $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. * Returns whether critical data has completed being loaded.
* *
@@ -123,54 +157,227 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
return $scope.user !== null return $scope.user !== null
&& $scope.permissionFlags !== null && $scope.permissionFlags !== null
&& $scope.rootGroup !== null && $scope.rootGroups !== null
&& $scope.permissions !== null && $scope.permissions !== null
&& $scope.attributes !== null && $scope.attributes !== null;
&& $scope.canSaveUser !== null
&& $scope.canDeleteUser !== 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 // Pull user attribute schema
schemaService.getUserAttributes().success(function attributesReceived(attributes) { schemaService.getUserAttributes(dataSource).success(function attributesReceived(attributes) {
$scope.attributes = attributes; $scope.attributes = attributes;
}); });
// Pull user data // Pull user data
userService.getUser(username).success(function userReceived(user) { dataSourceService.apply(userService.getUser, dataSources, username)
$scope.user = user; .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 // Pull user permissions
permissionService.getPermissions(username).success(function gotPermissions(permissions) { permissionService.getPermissions(dataSource, username).success(function gotPermissions(permissions) {
$scope.permissionFlags = PermissionFlagSet.fromPermissionSet(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 // Retrieve all connections for which we have ADMINISTER permission
connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) dataSourceService.apply(
.success(function connectionGroupReceived(rootGroup) { connectionGroupService.getConnectionGroupTree,
$scope.rootGroup = rootGroup; [dataSource],
ConnectionGroup.ROOT_IDENTIFIER,
[PermissionSet.ObjectPermissionType.ADMINISTER]
)
.then(function connectionGroupReceived(rootGroups) {
$scope.rootGroups = rootGroups;
}); });
// Query the user's permissions for the current connection // Query the user's permissions for the current user
permissionService.getPermissions(authenticationService.getCurrentUsername()) permissionService.getPermissions(dataSource, currentUsername)
.success(function permissionsReceived(permissions) { .success(function permissionsReceived(permissions) {
$scope.permissions = 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; return;
} }
// Save the user // Save or create the user, depending on whether the user exists
userService.saveUser($scope.user) var saveUserPromise;
.success(function savedUser() { 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 // Upon success, save any changed permissions
permissionService.patchPermissions($scope.user.username, permissionsAdded, permissionsRemoved) permissionService.patchPermissions(dataSource, $scope.user.username, permissionsAdded, permissionsRemoved)
.success(function patchedUserPermissions() { .success(function patchedUserPermissions() {
$location.path('/settings/users'); $location.path('/settings/users');
}) })
@@ -574,7 +786,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
var deleteUserImmediately = function deleteUserImmediately() { var deleteUserImmediately = function deleteUserImmediately() {
// Delete the user // Delete the user
userService.deleteUser($scope.user) userService.deleteUser(dataSource, $scope.user)
.success(function deletedUser() { .success(function deletedUser() {
$location.path('/settings/users'); $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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -20,70 +20,84 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
--> -->
<div class="view" ng-class="{loading: !isLoaded()}"> <div class="manage-user view" ng-class="{loading: !isLoaded()}">
<!-- Main property editor --> <!-- User header and data source tabs -->
<div class="header"> <div class="username header">
<h2>{{'MANAGE_USER.SECTION_HEADER_EDIT_USER' | translate}}</h2> <h2>{{user.username}}</h2>
<guac-user-menu></guac-user-menu> <guac-user-menu></guac-user-menu>
</div> </div>
<div class="section"> <div class="page-tabs">
<table class="properties"> <guac-page-list pages="accountPages"></guac-page-list>
<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> </div>
<!-- User attributes section --> <!-- Warn if user is read-only -->
<div class="attributes"> <div class="section" ng-show="isReadOnly()">
<guac-form namespace="'USER_ATTRIBUTES'" content="attributes" model="user.attributes"></guac-form> <p class="notice read-only">{{'MANAGE_USER.INFO_READ_ONLY' | translate}}</p>
</div> </div>
<!-- System permissions section --> <!-- Sections applicable to non-read-only users -->
<h2 class="header">{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}</h2> <div ng-show="!isReadOnly()">
<div class="section">
<table class="properties"> <!-- User password section -->
<tr ng-repeat="systemPermissionType in systemPermissionTypes"> <div class="section">
<th>{{systemPermissionType.label | translate}}</th> <table class="properties">
<td><input type="checkbox" ng-model="permissionFlags.systemPermissions[systemPermissionType.value]" <tr>
ng-change="systemPermissionChanged(systemPermissionType.value)"/></td> <th>{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}</th>
</tr> <td><input ng-model="user.password" type="password" /></td>
<tr> </tr>
<th>{{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}</th> <tr>
<td><input type="checkbox" ng-model="permissionFlags.userPermissions.UPDATE[user.username]" <th>{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}</th>
ng-change="userPermissionChanged('UPDATE', user.username)"/></td> <td><input ng-model="passwordMatch" type="password" /></td>
</tr> </tr>
</table> </table>
</div> </div>
<h2 class="header">{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}</h2> <!-- User attributes section -->
<div class="section" ng-class="{loading: !rootGroup}"> <div class="attributes" ng-show="canChangeAttributes()">
<guac-group-list <guac-form namespace="'USER_ATTRIBUTES'" content="attributes" model="user.attributes"></guac-form>
context="groupListContext" </div>
connection-group="rootGroup"
connection-template="'app/manage/templates/connectionPermission.html'" <!-- System permissions section -->
connection-group-template="'app/manage/templates/connectionGroupPermission.html'" <div class="system-permissions" ng-show="canChangePermissions()">
page-size="20"/> <h2 class="header">{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}</h2>
<div class="section">
<table class="properties">
<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>
</tr>
<tr>
<th>{{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}</th>
<td><input type="checkbox" ng-model="permissionFlags.userPermissions.UPDATE[user.username]"
ng-change="userPermissionChanged('UPDATE', user.username)"/></td>
</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">
<guac-group-list
context="groupListContext"
connection-groups="rootGroups"
connection-template="'app/manage/templates/connectionPermission.html'"
connection-group-template="'app/manage/templates/connectionGroupPermission.html'"
page-size="20"/>
</div>
</div>
</div> </div>
<!-- Form action buttons --> <!-- Form action buttons -->
<div class="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-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>
</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. * The array of pages to display.
* *
* @type Page[] * @type PageDefinition[]
*/ */
pages : '=' pages : '='
@@ -42,13 +42,119 @@ angular.module('navigation').directive('guacPageList', [function guacPageList()
templateUrl: 'app/navigation/templates/guacPageList.html', templateUrl: 'app/navigation/templates/guacPageList.html',
controller: ['$scope', '$injector', function guacPageListController($scope, $injector) { controller: ['$scope', '$injector', function guacPageListController($scope, $injector) {
// Get required services // Required types
var PageDefinition = $injector.get('PageDefinition');
// Required services
var $location = $injector.get('$location'); 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. * Navigate to the given page.
* *
* @param {Page} page * @param {PageDefinition} page
* The page to navigate to. * The page to navigate to.
*/ */
$scope.navigateToPage = function navigateToPage(page) { $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. * Tests whether the given page is the page currently being viewed.
* *
* @param {Page} page * @param {PageDefinition} page
* The page to test. * The page to test.
* *
* @returns {Boolean} * @returns {Boolean}
* true if the given page is the current page, false otherwise. * true if the given page is the current page, false otherwise.
*/ */
$scope.isCurrentPage = function isCurrentPage(page) { $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 }] // end controller
}; };

View File

@@ -23,4 +23,8 @@
/** /**
* Module for generating and implementing user navigation options. * 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) { function userPageService($injector) {
// Get required types // Get required types
var ConnectionGroup = $injector.get('ConnectionGroup'); var ClientIdentifier = $injector.get('ClientIdentifier');
var PermissionSet = $injector.get('PermissionSet'); var ConnectionGroup = $injector.get('ConnectionGroup');
var PageDefinition = $injector.get('PageDefinition');
var PermissionSet = $injector.get('PermissionSet');
// Get required services // Get required services
var $q = $injector.get('$q'); var $q = $injector.get('$q');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get("connectionGroupService"); var connectionGroupService = $injector.get('connectionGroupService');
var permissionService = $injector.get("permissionService"); var dataSourceService = $injector.get('dataSourceService');
var permissionService = $injector.get('permissionService');
var translationStringService = $injector.get('translationStringService');
var service = {}; 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 * The home page to assign to a user if they can navigate to more than one
* page. * page.
* *
* @type Page * @type PageDefinition
*/ */
var SYSTEM_HOME_PAGE = new Page( var SYSTEM_HOME_PAGE = new PageDefinition({
'USER_MENU.ACTION_NAVIGATE_HOME', name : 'USER_MENU.ACTION_NAVIGATE_HOME',
'/' url : '/'
); });
/** /**
* Returns an appropriate home page for the current user. * Returns an appropriate home page for the current user.
* *
* @param {ConnectionGroup} rootGroup * @param {Object.<String, ConnectionGroup>} rootGroups
* The root of the connection group tree for the current user. * 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. * The user's home page.
*/ */
var generateHomePage = function generateHomePage(rootGroup) { var generateHomePage = function generateHomePage(rootGroups) {
// Get children var homePage = null;
var connections = rootGroup.childConnections || [];
var connectionGroups = rootGroup.childConnectionGroups || [];
// Use main connection list screen as home if multiple connections // Determine whether a connection or balancing group should serve as
// are available // the home page
if (connections.length + connectionGroups.length === 1) { for (var dataSource in rootGroups) {
var connection = connections[0]; // Get corresponding root group
var connectionGroup = connectionGroups[0]; var rootGroup = rootGroups[dataSource];
// Get children
var connections = rootGroup.childConnections || [];
var connectionGroups = rootGroup.childConnectionGroups || [];
// If exactly one connection or balancing group is available, use
// that as the home page
if (homePage === null && connections.length + connectionGroups.length === 1) {
var connection = connections[0];
var connectionGroup = connectionGroups[0];
// Only one connection present, use as home page
if (connection) {
homePage = new PageDefinition({
name : connection.name,
url : '/client/' + ClientIdentifier.toString({
dataSource : dataSource,
type : ClientIdentifier.Types.CONNECTION,
id : connection.identifier
})
});
}
// Only one balancing group present, use as home page
if (connectionGroup
&& connectionGroup.type === ConnectionGroup.Type.BALANCING
&& _.isEmpty(connectionGroup.childConnections)
&& _.isEmpty(connectionGroup.childConnectionGroups)) {
homePage = new PageDefinition({
name : connectionGroup.name,
url : '/client/' + ClientIdentifier.toString({
dataSource : dataSource,
type : ClientIdentifier.Types.CONNECTION_GROUP,
id : connectionGroup.identifier
})
});
}
// Only one connection present, use as home page
if (connection) {
return new Page(
connection.name,
'/client/c/' + connection.identifier
);
} }
// Only one connection present, use as home page // Otherwise, a connection or balancing group cannot serve as the
if (connectionGroup // home page
&& connectionGroup.type === ConnectionGroup.Type.BALANCING else {
&& _.isEmpty(connectionGroup.childConnections) homePage = null;
&& _.isEmpty(connectionGroup.childConnectionGroups)) { break;
return new Page(
connectionGroup.name,
'/client/g/' + connectionGroup.identifier
);
} }
} } // end for each data source
// Resolve promise with default home page // Use default home page if no other is available
return SYSTEM_HOME_PAGE; return homePage || SYSTEM_HOME_PAGE;
}; };
@@ -126,10 +139,14 @@ angular.module('navigation').factory('userPageService', ['$injector',
var deferred = $q.defer(); var deferred = $q.defer();
// Resolve promise using home page derived from root connection group // Resolve promise using home page derived from root connection groups
connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) dataSourceService.apply(
.success(function rootConnectionGroupRetrieved(rootGroup) { connectionGroupService.getConnectionGroupTree,
deferred.resolve(generateHomePage(rootGroup)); authenticationService.getAvailableDataSources(),
ConnectionGroup.ROOT_IDENTIFIER
)
.then(function rootConnectionGroupsRetrieved(rootGroups) {
deferred.resolve(generateHomePage(rootGroups));
}); });
return deferred.promise; return deferred.promise;
@@ -140,94 +157,115 @@ angular.module('navigation').factory('userPageService', ['$injector',
* Returns all settings pages that the current user can visit. This can * Returns all settings pages that the current user can visit. This can
* include any of the various manage pages. * include any of the various manage pages.
* *
* @param {PermissionSet} permissions * @param {Object.<String, PermissionSet>} permissionSets
* The permissions for the current user. * A map of all permissions granted to the current user, where each
* key is the identifier of the corresponding data source.
* *
* @returns {Page[]} * @returns {Page[]}
* An array of all settings pages that the current user can visit. * 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 pages = [];
permissions = angular.copy(permissions); var canManageUsers = [];
var canManageConnections = [];
var canManageSessions = [];
// Ignore permission to update root group // Inspect the contents of each provided permission set
PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); angular.forEach(permissionSets, function inspectPermissions(permissions, dataSource) {
// Ignore permission to update self permissions = angular.copy(permissions);
PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, authenticationService.getCurrentUsername());
// Determine whether the current user needs access to the user management UI // Ignore permission to update root group
var canManageUsers = PermissionSet.removeConnectionGroupPermission(permissions,
PermissionSet.ObjectPermissionType.UPDATE,
ConnectionGroup.ROOT_IDENTIFIER);
// System permissions // Ignore permission to update self
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) PermissionSet.removeUserPermission(permissions,
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) PermissionSet.ObjectPermissionType.UPDATE,
authenticationService.getCurrentUsername());
// Permission to update users // Determine whether the current user needs access to the user management UI
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) if (
// System permissions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER)
// Permission to delete users // Permission to update users
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
// Permission to administer users // Permission to delete users
|| PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
// Determine whether the current user needs access to the connection management UI // Permission to administer users
var canManageConnections = || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER)
)
canManageUsers.push(dataSource);
// System permissions // Determine whether the current user needs access to the connection management UI
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) if (
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) // System permissions
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP) PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP)
// Permission to update connections or connection groups // Permission to update connections or connection groups
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
// Permission to delete connections or connection groups // Permission to delete connections or connection groups
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
// Permission to administer connections or connection groups // Permission to administer connections or connection groups
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER)
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER); || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER)
)
canManageConnections.push(dataSource);
var canManageSessions = // Determine whether the current user needs access to the session management UI
if (
// A user must be a system administrator to manage sessions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
)
canManageSessions.push(dataSource);
// A user must be a system administrator to manage sessions });
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER);
// If user can manage sessions, add link to sessions management page // If user can manage sessions, add link to sessions management page
if (canManageSessions) { if (canManageSessions.length) {
pages.push(new Page( pages.push(new PageDefinition({
'USER_MENU.ACTION_MANAGE_SESSIONS', name : 'USER_MENU.ACTION_MANAGE_SESSIONS',
'/settings/sessions' url : '/settings/sessions'
)); }));
} }
// If user can manage users, add link to user management page // If user can manage users, add link to user management page
if (canManageUsers) { if (canManageUsers.length) {
pages.push(new Page( pages.push(new PageDefinition({
'USER_MENU.ACTION_MANAGE_USERS', name : 'USER_MENU.ACTION_MANAGE_USERS',
'/settings/users' url : '/settings/users'
)); }));
} }
// If user can manage connections, add link to connections management page // If user can manage connections, add links for connection management pages
if (canManageConnections) { angular.forEach(canManageConnections, function addConnectionManagementLink(dataSource) {
pages.push(new Page( pages.push(new PageDefinition({
'USER_MENU.ACTION_MANAGE_CONNECTIONS', name : [
'/settings/connections' 'USER_MENU.ACTION_MANAGE_CONNECTIONS',
)); translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME'
} ],
url : '/settings/' + encodeURIComponent(dataSource) + '/connections'
}));
});
// Add link to user preferences (always accessible) // Add link to user preferences (always accessible)
pages.push(new Page( pages.push(new PageDefinition({
'USER_MENU.ACTION_MANAGE_PREFERENCES', name : 'USER_MENU.ACTION_MANAGE_PREFERENCES',
'/settings/preferences' url : '/settings/preferences'
)); }));
return pages; return pages;
}; };
@@ -245,10 +283,15 @@ angular.module('navigation').factory('userPageService', ['$injector',
var deferred = $q.defer(); 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 // Resolve promise using settings pages derived from permissions
permissionService.getPermissions(authenticationService.getCurrentUsername()) .then(function permissionsRetrieved(permissions) {
.success(function permissionsRetrieved(permissions) {
deferred.resolve(generateSettingsPages(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 * 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. * applicable pages of this sort, it may return a client page.
* *
* @param {ConnectionGroup} rootGroup * @param {Object.<String, ConnectionGroup>} rootGroups
* The root of the connection group tree for the current user. * 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 * @param {Object.<String, PermissionSet>} permissions
* The permissions for the current user. * A map of all permissions granted to the current user, where each
* key is the identifier of the corresponding data source.
* *
* @returns {Page[]} * @returns {Page[]}
* An array of all main pages that the current user can visit. * 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 = []; var pages = [];
// Get home page and settings pages // Get home page and settings pages
var homePage = generateHomePage(rootGroup); var homePage = generateHomePage(rootGroups);
var settingsPages = generateSettingsPages(permissions); var settingsPages = generateSettingsPages(permissions);
// Only include the home page in the list of main pages if the user // 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 // Add generic link to the first-available settings page
if (settingsPages.length) { if (settingsPages.length) {
pages.push(new Page( pages.push(new PageDefinition({
'USER_MENU.ACTION_MANAGE_SETTINGS', name : 'USER_MENU.ACTION_MANAGE_SETTINGS',
settingsPages[0].url url : settingsPages[0].url
)); }));
} }
return pages; return pages;
@@ -308,7 +353,7 @@ angular.module('navigation').factory('userPageService', ['$injector',
var deferred = $q.defer(); var deferred = $q.defer();
var rootGroup = null; var rootGroups = null;
var permissions = null; var permissions = null;
/** /**
@@ -316,20 +361,30 @@ angular.module('navigation').factory('userPageService', ['$injector',
* insufficient data is available, this function does nothing. * insufficient data is available, this function does nothing.
*/ */
var resolveMainPages = function resolveMainPages() { var resolveMainPages = function resolveMainPages() {
if (rootGroup && permissions) if (rootGroups && permissions)
deferred.resolve(generateMainPages(rootGroup, permissions)); deferred.resolve(generateMainPages(rootGroups, permissions));
}; };
// Retrieve root group, resolving main pages if possible // Retrieve root group, resolving main pages if possible
connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) dataSourceService.apply(
.success(function rootConnectionGroupRetrieved(retrievedRootGroup) { connectionGroupService.getConnectionGroupTree,
rootGroup = retrievedRootGroup; authenticationService.getAvailableDataSources(),
ConnectionGroup.ROOT_IDENTIFIER
)
.then(function rootConnectionGroupsRetrieved(retrievedRootGroups) {
rootGroups = retrievedRootGroups;
resolveMainPages(); resolveMainPages();
}); });
// Retrieve current permissions, resolving main pages if possible // Retrieve current permissions
permissionService.getPermissions(authenticationService.getCurrentUsername()) dataSourceService.apply(
.success(function permissionsRetrieved(retrievedPermissions) { permissionService.getPermissions,
authenticationService.getAvailableDataSources(),
authenticationService.getCurrentUsername()
)
// Resolving main pages if possible
.then(function permissionsRetrieved(retrievedPermissions) {
permissions = retrievedPermissions; permissions = retrievedPermissions;
resolveMainPages(); 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 Copyright (C) 2015 Glyptodon LLC
@@ -22,11 +22,13 @@
--> -->
<!-- Navigation links --> <!-- Navigation links -->
<li ng-repeat="page in pages"> <ul class="page-list-level" ng-repeat="level in levels track by $index">
<a class="home" ng-click="navigateToPage(page)" <li ng-repeat="page in getPages(level)" class="{{page.className}}">
ng-class="{current: isCurrentPage(page)}" href="#{{page.url}}"> <a class="home" ng-click="navigateToPage(page)"
{{page.name | translate}} ng-class="{current: isCurrentPage(page)}" href="#{{page.url}}">
</a> {{page.name | translate}}
</li> </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. * Service for operating on active connections via the REST API.
*/ */
angular.module('rest').factory('activeConnectionService', ['$http', 'authenticationService', angular.module('rest').factory('activeConnectionService', ['$injector',
function activeConnectionService($http, authenticationService) { function activeConnectionService($injector) {
// Required services
var $http = $injector.get('$http');
var $q = $injector.get('$q');
var authenticationService = $injector.get('authenticationService');
var service = {}; var service = {};
@@ -39,13 +44,12 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat
* result. If null, no filtering will be performed. Valid values are * result. If null, no filtering will be performed. Valid values are
* listed within PermissionSet.ObjectType. * listed within PermissionSet.ObjectType.
* *
* @returns {Promise.<Object.<String, ActiveConnection>>} * @returns {Promise.<Object.<String, ActiveConnection>>}
* A promise which will resolve with a map of @link{ActiveConnection} * A promise which will resolve with a map of @link{ActiveConnection}
* objects, where each key is the identifier of the corresponding * objects, where each key is the identifier of the corresponding
* active connection. * active connection.
*/ */
service.getActiveConnections = function getActiveConnections(permissionTypes) { service.getActiveConnections = function getActiveConnections(dataSource, permissionTypes) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -59,12 +63,74 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat
// Retrieve tunnels // Retrieve tunnels
return $http({ return $http({
method : 'GET', method : 'GET',
url : 'api/activeConnections', url : 'api/data/' + encodeURIComponent(dataSource) + '/activeConnections',
params : httpParameters 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 * Makes a request to the REST API to delete the active connections having
* the given identifiers, effectively disconnecting them, returning a * 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 * A promise for the HTTP call which will succeed if and only if the
* delete operation is successful. * delete operation is successful.
*/ */
service.deleteActiveConnections = function deleteActiveConnections(identifiers) { service.deleteActiveConnections = function deleteActiveConnections(dataSource, identifiers) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -96,7 +162,7 @@ angular.module('rest').factory('activeConnectionService', ['$http', 'authenticat
// Perform active connection deletion via PATCH // Perform active connection deletion via PATCH
return $http({ return $http({
method : 'PATCH', method : 'PATCH',
url : 'api/activeConnections', url : 'api/data/' + encodeURIComponent(dataSource) + '/activeConnections',
params : httpParameters, params : httpParameters,
data : activeConnectionPatch 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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -28,6 +28,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector',
// Required services // Required services
var $http = $injector.get('$http'); var $http = $injector.get('$http');
var $q = $injector.get('$q');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var cacheService = $injector.get('cacheService'); 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 * A promise which will resolve with a @link{ConnectionGroup} upon
* success. * 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 // Use the root connection group ID if no ID is passed in
connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER;
@@ -75,12 +76,12 @@ angular.module('rest').factory('connectionGroupService', ['$injector',
return $http({ return $http({
cache : cacheService.connections, cache : cacheService.connections,
method : 'GET', method : 'GET',
url : 'api/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree', url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree',
params : httpParameters params : httpParameters
}); });
}; };
/** /**
* Makes a request to the REST API to get an individual connection group, * Makes a request to the REST API to get an individual connection group,
* returning a promise that provides the corresponding * returning a promise that provides the corresponding
@@ -94,7 +95,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector',
* A promise which will resolve with a @link{ConnectionGroup} upon * A promise which will resolve with a @link{ConnectionGroup} upon
* success. * success.
*/ */
service.getConnectionGroup = function getConnectionGroup(connectionGroupID) { service.getConnectionGroup = function getConnectionGroup(dataSource, connectionGroupID) {
// Use the root connection group ID if no ID is passed in // Use the root connection group ID if no ID is passed in
connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER;
@@ -108,7 +109,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector',
return $http({ return $http({
cache : cacheService.connections, cache : cacheService.connections,
method : 'GET', method : 'GET',
url : 'api/connectionGroups/' + encodeURIComponent(connectionGroupID), url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID),
params : httpParameters 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 * A promise for the HTTP call which will succeed if and only if the
* save operation is successful. * save operation is successful.
*/ */
service.saveConnectionGroup = function saveConnectionGroup(connectionGroup) { service.saveConnectionGroup = function saveConnectionGroup(dataSource, connectionGroup) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -138,7 +139,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector',
if (!connectionGroup.identifier) { if (!connectionGroup.identifier) {
return $http({ return $http({
method : 'POST', method : 'POST',
url : 'api/connectionGroups', url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups',
params : httpParameters, params : httpParameters,
data : connectionGroup data : connectionGroup
}) })
@@ -154,7 +155,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector',
else { else {
return $http({ return $http({
method : 'PUT', method : 'PUT',
url : 'api/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier),
params : httpParameters, params : httpParameters,
data : connectionGroup 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 * A promise for the HTTP call which will succeed if and only if the
* delete operation is successful. * delete operation is successful.
*/ */
service.deleteConnectionGroup = function deleteConnectionGroup(connectionGroup) { service.deleteConnectionGroup = function deleteConnectionGroup(dataSource, connectionGroup) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -187,7 +188,7 @@ angular.module('rest').factory('connectionGroupService', ['$injector',
// Delete connection group // Delete connection group
return $http({ return $http({
method : 'DELETE', method : 'DELETE',
url : 'api/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier),
params : httpParameters 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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * 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 * // Do something with the connection
* }); * });
*/ */
service.getConnection = function getConnection(id) { service.getConnection = function getConnection(dataSource, id) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -59,7 +59,7 @@ angular.module('rest').factory('connectionService', ['$injector',
return $http({ return $http({
cache : cacheService.connections, cache : cacheService.connections,
method : 'GET', method : 'GET',
url : 'api/connections/' + encodeURIComponent(id), url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id),
params : httpParameters params : httpParameters
}); });
@@ -77,7 +77,7 @@ angular.module('rest').factory('connectionService', ['$injector',
* A promise which will resolve with an array of * A promise which will resolve with an array of
* @link{ConnectionHistoryEntry} objects upon success. * @link{ConnectionHistoryEntry} objects upon success.
*/ */
service.getConnectionHistory = function getConnectionHistory(id) { service.getConnectionHistory = function getConnectionHistory(dataSource, id) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -87,7 +87,7 @@ angular.module('rest').factory('connectionService', ['$injector',
// Retrieve connection history // Retrieve connection history
return $http({ return $http({
method : 'GET', method : 'GET',
url : 'api/connections/' + encodeURIComponent(id) + '/history', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/history',
params : httpParameters params : httpParameters
}); });
@@ -105,7 +105,7 @@ angular.module('rest').factory('connectionService', ['$injector',
* A promise which will resolve with an map of parameter name/value * A promise which will resolve with an map of parameter name/value
* pairs upon success. * pairs upon success.
*/ */
service.getConnectionParameters = function getConnectionParameters(id) { service.getConnectionParameters = function getConnectionParameters(dataSource, id) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -116,7 +116,7 @@ angular.module('rest').factory('connectionService', ['$injector',
return $http({ return $http({
cache : cacheService.connections, cache : cacheService.connections,
method : 'GET', method : 'GET',
url : 'api/connections/' + encodeURIComponent(id) + '/parameters', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/parameters',
params : httpParameters 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 * A promise for the HTTP call which will succeed if and only if the
* save operation is successful. * save operation is successful.
*/ */
service.saveConnection = function saveConnection(connection) { service.saveConnection = function saveConnection(dataSource, connection) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -146,7 +146,7 @@ angular.module('rest').factory('connectionService', ['$injector',
if (!connection.identifier) { if (!connection.identifier) {
return $http({ return $http({
method : 'POST', method : 'POST',
url : 'api/connections', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections',
params : httpParameters, params : httpParameters,
data : connection data : connection
}) })
@@ -162,7 +162,7 @@ angular.module('rest').factory('connectionService', ['$injector',
else { else {
return $http({ return $http({
method : 'PUT', method : 'PUT',
url : 'api/connections/' + encodeURIComponent(connection.identifier), url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier),
params : httpParameters, params : httpParameters,
data : connection 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 * A promise for the HTTP call which will succeed if and only if the
* delete operation is successful. * delete operation is successful.
*/ */
service.deleteConnection = function deleteConnection(connection) { service.deleteConnection = function deleteConnection(dataSource, connection) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -195,7 +195,7 @@ angular.module('rest').factory('connectionService', ['$injector',
// Delete connection // Delete connection
return $http({ return $http({
method : 'DELETE', method : 'DELETE',
url : 'api/connections/' + encodeURIComponent(connection.identifier), url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier),
params : httpParameters 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 // Required services
var $http = $injector.get('$http'); var $http = $injector.get('$http');
var $q = $injector.get('$q');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var cacheService = $injector.get('cacheService'); 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 * given user, returning a promise that provides an array of
* @link{Permission} objects if successful. * @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 * @param {String} userID
* The ID of the user to retrieve the permissions for. * 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 * A promise which will resolve with a @link{PermissionSet} upon
* success. * success.
*/ */
service.getPermissions = function getPermissions(userID) { service.getPermissions = function getPermissions(dataSource, userID) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -59,17 +65,22 @@ angular.module('rest').factory('permissionService', ['$injector',
return $http({ return $http({
cache : cacheService.users, cache : cacheService.users,
method : 'GET', method : 'GET',
url : 'api/users/' + encodeURIComponent(userID) + '/permissions', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(userID) + '/permissions',
params : httpParameters params : httpParameters
}); });
}; };
/** /**
* Makes a request to the REST API to add permissions for a given user, * Makes a request to the REST API to add permissions for a given user,
* returning a promise that can be used for processing the results of the * returning a promise that can be used for processing the results of the
* call. * 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 * @param {String} userID
* The ID of the user to modify the permissions of. * 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 * A promise for the HTTP call which will succeed if and only if the
* add operation is successful. * add operation is successful.
*/ */
service.addPermissions = function addPermissions(userID, permissions) { service.addPermissions = function addPermissions(dataSource, userID, permissions) {
return service.patchPermissions(userID, permissions, null); 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 * returning a promise that can be used for processing the results of the
* call. * 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 * @param {String} userID
* The ID of the user to modify the permissions of. * 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 * A promise for the HTTP call which will succeed if and only if the
* remove operation is successful. * remove operation is successful.
*/ */
service.removePermissions = function removePermissions(userID, permissions) { service.removePermissions = function removePermissions(dataSource, userID, permissions) {
return service.patchPermissions(userID, null, 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 * user, returning a promise that can be used for processing the results of
* the call. * 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 * @param {String} userID
* The ID of the user to modify the permissions of. * 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 * A promise for the HTTP call which will succeed if and only if the
* patch operation is successful. * patch operation is successful.
*/ */
service.patchPermissions = function patchPermissions(userID, permissionsToAdd, permissionsToRemove) { service.patchPermissions = function patchPermissions(dataSource, userID, permissionsToAdd, permissionsToRemove) {
var permissionPatch = []; var permissionPatch = [];
@@ -217,7 +238,7 @@ angular.module('rest').factory('permissionService', ['$injector',
// Patch user permissions // Patch user permissions
return $http({ return $http({
method : 'PATCH', method : 'PATCH',
url : 'api/users/' + encodeURIComponent(userID) + '/permissions', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(userID) + '/permissions',
params : httpParameters, params : httpParameters,
data : permissionPatch 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 * @link{Form} objects if successful. Each element of the array describes
* a logical grouping of possible attributes. * 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[]>} * @returns {Promise.<Form[]>}
* A promise which will resolve with an array of @link{Form} * A promise which will resolve with an array of @link{Form}
* objects, where each @link{Form} describes a logical grouping of * objects, where each @link{Form} describes a logical grouping of
* possible attributes. * possible attributes.
*/ */
service.getUserAttributes = function getUserAttributes() { service.getUserAttributes = function getUserAttributes(dataSource) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -55,7 +61,7 @@ angular.module('rest').factory('schemaService', ['$injector',
return $http({ return $http({
cache : cacheService.schema, cache : cacheService.schema,
method : 'GET', method : 'GET',
url : 'api/schema/users/attributes', url : 'api/schema/' + encodeURIComponent(dataSource) + '/users/attributes',
params : httpParameters params : httpParameters
}); });
@@ -67,12 +73,18 @@ angular.module('rest').factory('schemaService', ['$injector',
* @link{Form} objects if successful. Each element of the array describes * @link{Form} objects if successful. Each element of the array describes
* a logical grouping of possible attributes. * 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[]>} * @returns {Promise.<Form[]>}
* A promise which will resolve with an array of @link{Form} * A promise which will resolve with an array of @link{Form}
* objects, where each @link{Form} describes a logical grouping of * objects, where each @link{Form} describes a logical grouping of
* possible attributes. * possible attributes.
*/ */
service.getConnectionAttributes = function getConnectionAttributes() { service.getConnectionAttributes = function getConnectionAttributes(dataSource) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -83,7 +95,7 @@ angular.module('rest').factory('schemaService', ['$injector',
return $http({ return $http({
cache : cacheService.schema, cache : cacheService.schema,
method : 'GET', method : 'GET',
url : 'api/schema/connections/attributes', url : 'api/schema/' + encodeURIComponent(dataSource) + '/connections/attributes',
params : httpParameters params : httpParameters
}); });
@@ -95,12 +107,18 @@ angular.module('rest').factory('schemaService', ['$injector',
* of @link{Form} objects if successful. Each element of the array * of @link{Form} objects if successful. Each element of the array
* a logical grouping of possible attributes. * 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[]>} * @returns {Promise.<Form[]>}
* A promise which will resolve with an array of @link{Form} * A promise which will resolve with an array of @link{Form}
* objects, where each @link{Form} describes a logical grouping of * objects, where each @link{Form} describes a logical grouping of
* possible attributes. * possible attributes.
*/ */
service.getConnectionGroupAttributes = function getConnectionGroupAttributes() { service.getConnectionGroupAttributes = function getConnectionGroupAttributes(dataSource) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -111,7 +129,7 @@ angular.module('rest').factory('schemaService', ['$injector',
return $http({ return $http({
cache : cacheService.schema, cache : cacheService.schema,
method : 'GET', method : 'GET',
url : 'api/schema/connectionGroups/attributes', url : 'api/schema/' + encodeURIComponent(dataSource) + '/connectionGroups/attributes',
params : httpParameters params : httpParameters
}); });
@@ -122,11 +140,16 @@ angular.module('rest').factory('schemaService', ['$injector',
* a promise that provides a map of @link{Protocol} objects by protocol * a promise that provides a map of @link{Protocol} objects by protocol
* name if successful. * 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>>} * @returns {Promise.<Object.<String, Protocol>>}
* A promise which will resolve with a map of @link{Protocol} * A promise which will resolve with a map of @link{Protocol}
* objects by protocol name upon success. * objects by protocol name upon success.
*/ */
service.getProtocols = function getProtocols() { service.getProtocols = function getProtocols(dataSource) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -137,7 +160,7 @@ angular.module('rest').factory('schemaService', ['$injector',
return $http({ return $http({
cache : cacheService.schema, cache : cacheService.schema,
method : 'GET', method : 'GET',
url : 'api/schema/protocols', url : 'api/schema/' + encodeURIComponent(dataSource) + '/protocols',
params : httpParameters 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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -28,6 +28,7 @@ angular.module('rest').factory('userService', ['$injector',
// Required services // Required services
var $http = $injector.get('$http'); var $http = $injector.get('$http');
var $q = $injector.get('$q');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var cacheService = $injector.get('cacheService'); 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 * returning a promise that provides an array of @link{User} objects if
* successful. * 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] * @param {String[]} [permissionTypes]
* The set of permissions to filter with. A user must have one or more * The set of permissions to filter with. A user must have one or more
* of these permissions for a user to appear in the result. * of these permissions for a user to appear in the result.
@@ -51,7 +57,7 @@ angular.module('rest').factory('userService', ['$injector',
* A promise which will resolve with an array of @link{User} objects * A promise which will resolve with an array of @link{User} objects
* upon success. * upon success.
*/ */
service.getUsers = function getUsers(permissionTypes) { service.getUsers = function getUsers(dataSource, permissionTypes) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -66,7 +72,7 @@ angular.module('rest').factory('userService', ['$injector',
return $http({ return $http({
cache : cacheService.users, cache : cacheService.users,
method : 'GET', method : 'GET',
url : 'api/users', url : 'api/data/' + encodeURIComponent(dataSource) + '/users',
params : httpParameters params : httpParameters
}); });
@@ -76,14 +82,19 @@ angular.module('rest').factory('userService', ['$injector',
* Makes a request to the REST API to get the user having the given * Makes a request to the REST API to get the user having the given
* username, returning a promise that provides the corresponding * username, returning a promise that provides the corresponding
* @link{User} if successful. * @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 * @param {String} username
* The username of the user to retrieve. * The username of the user to retrieve.
* *
* @returns {Promise.<User>} * @returns {Promise.<User>}
* A promise which will resolve with a @link{User} upon success. * 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 // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -94,7 +105,7 @@ angular.module('rest').factory('userService', ['$injector',
return $http({ return $http({
cache : cacheService.users, cache : cacheService.users,
method : 'GET', method : 'GET',
url : 'api/users/' + encodeURIComponent(username), url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username),
params : httpParameters 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 * 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. * 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 * @param {User} user
* The user to delete. * 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 * A promise for the HTTP call which will succeed if and only if the
* delete operation is successful. * delete operation is successful.
*/ */
service.deleteUser = function deleteUser(user) { service.deleteUser = function deleteUser(dataSource, user) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -121,7 +137,7 @@ angular.module('rest').factory('userService', ['$injector',
// Delete user // Delete user
return $http({ return $http({
method : 'DELETE', method : 'DELETE',
url : 'api/users/' + encodeURIComponent(user.username), url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username),
params : httpParameters 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 * 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. * 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 * @param {User} user
* The user to create. * 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 * A promise for the HTTP call which will succeed if and only if the
* create operation is successful. * create operation is successful.
*/ */
service.createUser = function createUser(user) { service.createUser = function createUser(dataSource, user) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -154,7 +175,7 @@ angular.module('rest').factory('userService', ['$injector',
// Create user // Create user
return $http({ return $http({
method : 'POST', method : 'POST',
url : 'api/users', url : 'api/data/' + encodeURIComponent(dataSource) + '/users',
params : httpParameters, params : httpParameters,
data : user 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 * 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. * 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 * @param {User} user
* The user to update. * 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 * A promise for the HTTP call which will succeed if and only if the
* save operation is successful. * save operation is successful.
*/ */
service.saveUser = function saveUser(user) { service.saveUser = function saveUser(dataSource, user) {
// Build HTTP parameters set // Build HTTP parameters set
var httpParameters = { var httpParameters = {
@@ -187,7 +213,7 @@ angular.module('rest').factory('userService', ['$injector',
// Update user // Update user
return $http({ return $http({
method : 'PUT', method : 'PUT',
url : 'api/users/' + encodeURIComponent(user.username), url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username),
params : httpParameters, params : httpParameters,
data : user 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, * 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. * 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 * @param {String} username
* The username of the user to update. * 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 * A promise for the HTTP call which will succeed if and only if the
* password update operation is successful. * password update operation is successful.
*/ */
service.updateUserPassword = function updateUserPassword(username, service.updateUserPassword = function updateUserPassword(dataSource, username,
oldPassword, newPassword) { oldPassword, newPassword) {
// Build HTTP parameters set // Build HTTP parameters set
@@ -227,7 +258,7 @@ angular.module('rest').factory('userService', ['$injector',
// Update user password // Update user password
return $http({ return $http({
method : 'PUT', method : 'PUT',
url : 'api/users/' + encodeURIComponent(username) + '/password', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) + '/password',
params : httpParameters, params : httpParameters,
data : new UserPasswordUpdate({ data : new UserPasswordUpdate({
oldPassword : oldPassword, oldPassword : oldPassword,

View File

@@ -46,17 +46,6 @@ angular.module('manage').controller('settingsController', ['$scope', '$injector'
*/ */
$scope.activeTab = $routeParams.tab; $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 // Retrieve settings pages
userPageService.getSettingsPages() userPageService.getSettingsPages()
.then(function settingsPagesRetrieved(pages) { .then(function settingsPagesRetrieved(pages) {

View File

@@ -41,13 +41,18 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe
var PermissionSet = $injector.get('PermissionSet'); var PermissionSet = $injector.get('PermissionSet');
// Required services // Required services
var $location = $injector.get('$location'); var $routeParams = $injector.get('$routeParams');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService'); var connectionGroupService = $injector.get('connectionGroupService');
var dataSourceService = $injector.get('dataSourceService');
var guacNotification = $injector.get('guacNotification'); var guacNotification = $injector.get('guacNotification');
var permissionService = $injector.get('permissionService'); var permissionService = $injector.get('permissionService');
// Identifier of the current user /**
* The identifier of the current user.
*
* @type String
*/
var currentUsername = authenticationService.getCurrentUsername(); 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. * 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 * 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 * All permissions associated with the current user, or null if the
* user's permissions have not yet been loaded. * user's permissions have not yet been loaded.
* *
* @type PermissionSet * @type Object.<String, PermissionSet>
*/ */
$scope.permissions = null; $scope.permissions = null;
@@ -111,57 +123,142 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe
*/ */
$scope.isLoaded = function isLoaded() { $scope.isLoaded = function isLoaded() {
return $scope.rootGroup !== null return $scope.rootGroup !== null
&& $scope.permissions !== null && $scope.permissions !== null;
&& $scope.canManageConnections !== null
&& $scope.canCreateConnections !== null };
&& $scope.canCreateConnectionGroups !== null;
/**
* Returns whether the current user can create new connections
* within at least one data source.
*
* @return {Boolean}
* true if the current user can create new connections within
* at least one data source, false otherwise.
*/
$scope.canCreateConnections = function canCreateConnections() {
// Abort if permissions have not yet loaded
if (!$scope.permissions)
return false;
// For each data source
for (var dataSource in $scope.permissions) {
// Retrieve corresponding permission set
var permissionSet = $scope.permissions[dataSource];
// Can create connections if adminstrator or have explicit permission
if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_CONNECTION))
return true;
}
// No data sources allow connection creation
return false;
};
/**
* Returns whether the current user can create new connection
* groups within at least one data source.
*
* @return {Boolean}
* true if the current user can create new connection groups
* within at least one data source, false otherwise.
*/
$scope.canCreateConnectionGroups = function canCreateConnectionGroups() {
// Abort if permissions have not yet loaded
if (!$scope.permissions)
return false;
// For each data source
for (var dataSource in $scope.permissions) {
// Retrieve corresponding permission set
var permissionSet = $scope.permissions[dataSource];
// Can create connections groups if adminstrator or have explicit permission
if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP))
return true;
}
// No data sources allow connection group creation
return false;
};
/**
* Returns whether the current user can create new connections or
* connection groups or make changes to existing connections or
* connection groups within at least one data source. The
* connection management interface as a whole is useless if this
* function returns false.
*
* @return {Boolean}
* true if the current user can create new connections/groups
* or make changes to existing connections/groups within at
* least one data source, false otherwise.
*/
$scope.canManageConnections = function canManageConnections() {
// Abort if permissions have not yet loaded
if (!$scope.permissions)
return false;
// Creating connections/groups counts as management
if ($scope.canCreateConnections() || $scope.canCreateConnectionGroups())
return true;
// Ignore permission to update root group
PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER);
// For each data source
for (var dataSource in $scope.permissions) {
// Retrieve corresponding permission set
var permissionSet = $scope.permissions[dataSource];
// Can manage connections if granted explicit update or delete
if (PermissionSet.hasConnectionPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE)
|| PermissionSet.hasConnectionPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE))
return true;
// Can manage connections groups if granted explicit update or delete
if (PermissionSet.hasConnectionGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE)
|| PermissionSet.hasConnectionGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE))
return true;
}
// No data sources allow management of connections or groups
return false;
}; };
// Retrieve current permissions // Retrieve current permissions
permissionService.getPermissions(currentUsername) dataSourceService.apply(
.success(function permissionsRetrieved(permissions) { permissionService.getPermissions,
[$scope.dataSource],
currentUsername
)
.then(function permissionsRetrieved(permissions) {
$scope.permissions = permissions; $scope.permissions = permissions;
// Ignore permission to update root group
PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER);
// Determine whether the current user can create new users
$scope.canCreateConnections =
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION);
// Determine whether the current user can create new users
$scope.canCreateConnectionGroups =
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
|| PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP);
// Determine whether the current user can manage other connections or groups
$scope.canManageConnections =
// Permission to manage connections
$scope.canCreateConnections
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
|| PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
// Permission to manage groups
|| $scope.canCreateConnectionGroups
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
|| PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE);
// Return to home if there's nothing to do here
if (!$scope.canManageConnections)
$location.path('/');
}); });
// Retrieve all connections for which we have UPDATE or DELETE permission // Retrieve all connections for which we have UPDATE or DELETE permission
connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER, dataSourceService.apply(
[PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE]) connectionGroupService.getConnectionGroupTree,
.success(function connectionGroupReceived(rootGroup) { [$scope.dataSource],
$scope.rootGroup = rootGroup; 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(); 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. * 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 // 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() { .success(function passwordUpdated() {
// Clear the password fields // Clear the password fields
@@ -174,7 +182,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe
}); });
// Retrieve current permissions // Retrieve current permissions
permissionService.getPermissions(username) permissionService.getPermissions(dataSource, username)
.success(function permissionsRetrieved(permissions) { .success(function permissionsRetrieved(permissions) {
// Add action for changing password if permission is granted // Add action for changing password if permission is granted

View File

@@ -44,19 +44,20 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
// Required services // Required services
var $filter = $injector.get('$filter'); var $filter = $injector.get('$filter');
var $translate = $injector.get('$translate'); var $translate = $injector.get('$translate');
var $q = $injector.get('$q');
var activeConnectionService = $injector.get('activeConnectionService'); var activeConnectionService = $injector.get('activeConnectionService');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService'); var connectionGroupService = $injector.get('connectionGroupService');
var dataSourceService = $injector.get('dataSourceService');
var guacNotification = $injector.get('guacNotification'); var guacNotification = $injector.get('guacNotification');
var permissionService = $injector.get('permissionService');
/** /**
* All permissions associated with the current user, or null if the * The identifiers of all data sources accessible by the current
* user's permissions have not yet been loaded. * user.
* *
* @type PermissionSet * @type String[]
*/ */
$scope.permissions = null; var dataSources = authenticationService.getAvailableDataSources();
/** /**
* The ActiveConnectionWrappers of all active sessions accessible * 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 * All active connections, if known, grouped by corresponding data
* have not yet been loaded. * 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 * Map of all visible connections by data source identifier and
* visible connections have not yet been loaded. * 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. * 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 * 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 * Adds the given connection to the internal set of visible
* connections. * connections.
* *
* @param {String} dataSource
* The identifier of the data source associated with the given
* connection.
*
* @param {Connection} connection * @param {Connection} connection
* The connection to add to the internal set of visible * The connection to add to the internal set of visible
* connections. * connections.
*/ */
var addConnection = function addConnection(connection) { var addConnection = function addConnection(dataSource, connection) {
// Add given connection to set of visible connections // 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 * Adds all descendant connections of the given connection group to
* the internal set of connections. * the internal set of connections.
* *
* @param {String} dataSource
* The identifier of the data source associated with the given
* connection group.
*
* @param {ConnectionGroup} connectionGroup * @param {ConnectionGroup} connectionGroup
* The connection group whose descendant connections should be * The connection group whose descendant connections should be
* added to the internal set of connections. * added to the internal set of connections.
*/ */
var addDescendantConnections = function addDescendantConnections(connectionGroup) { var addDescendantConnections = function addDescendantConnections(dataSource, connectionGroup) {
// Add all child connections // Add all child connections
if (connectionGroup.childConnections) angular.forEach(connectionGroup.childConnections, function addConnectionForDataSource(connection) {
connectionGroup.childConnections.forEach(addConnection); addConnection(dataSource, connection);
});
// Add all child connection groups // Add all child connection groups
if (connectionGroup.childConnectionGroups) angular.forEach(connectionGroup.childConnectionGroups, function addConnectionGroupForDataSource(connectionGroup) {
connectionGroup.childConnectionGroups.forEach(addDescendantConnections); 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, * within the scope. If required data has not yet finished loading,
* this function has no effect. * this function has no effect.
*/ */
var wrapActiveConnections = function wrapActiveConnections() { var wrapAllActiveConnections = function wrapAllActiveConnections() {
// Abort if not all required data is available // Abort if not all required data is available
if (!activeConnections || !connections || !sessionDateFormat) if (!allActiveConnections || !allConnections || !sessionDateFormat)
return; return;
// Wrap all active connections for sake of display // Wrap all active connections for sake of display
$scope.wrappers = []; $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]; // Retrieve corresponding connection
var connection = connections[activeConnection.connectionIdentifier]; var connection = allConnections[dataSource][activeConnection.connectionIdentifier];
$scope.wrappers.push(new ActiveConnectionWrapper( // Add wrapper
connection.name, $scope.wrappers.push(new ActiveConnectionWrapper({
$filter('date')(activeConnection.startDate, sessionDateFormat), dataSource : dataSource,
activeConnection 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 // Retrieve all connections
connectionGroupService.getConnectionGroupTree(ConnectionGroup.ROOT_IDENTIFIER) dataSourceService.apply(
.success(function connectionGroupReceived(retrievedRootGroup) { connectionGroupService.getConnectionGroupTree,
dataSources,
ConnectionGroup.ROOT_IDENTIFIER
)
.then(function connectionGroupsReceived(rootGroups) {
// Load connections from retrieved group tree allConnections = {};
connections = {};
addDescendantConnections(retrievedRootGroup); // Load connections from each received root group
angular.forEach(rootGroups, function connectionGroupReceived(rootGroup, dataSource) {
allConnections[dataSource] = {};
addDescendantConnections(dataSource, rootGroup);
});
// Attempt to produce wrapped list of active connections // Attempt to produce wrapped list of active connections
wrapActiveConnections(); wrapAllActiveConnections();
}); });
// Query active sessions // Query active sessions
activeConnectionService.getActiveConnections().success(function sessionsRetrieved(retrievedActiveConnections) { dataSourceService.apply(
activeConnectionService.getActiveConnections,
dataSources
)
.then(function sessionsRetrieved(retrievedActiveConnections) {
// Store received list // Store received map of active connections
activeConnections = retrievedActiveConnections; allActiveConnections = retrievedActiveConnections;
// Attempt to produce wrapped list of active connections // Attempt to produce wrapped list of active connections
wrapActiveConnections(); wrapAllActiveConnections();
}); });
@@ -223,7 +246,7 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
sessionDateFormat = retrievedSessionDateFormat; sessionDateFormat = retrievedSessionDateFormat;
// Attempt to produce wrapped list of active connections // 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. * to be useful, false otherwise.
*/ */
$scope.isLoaded = function isLoaded() { $scope.isLoaded = function isLoaded() {
return $scope.wrappers !== null;
return $scope.wrappers !== null
&& $scope.sessionDateFormat !== null
&& $scope.permissions !== null;
}; };
/** /**
@@ -276,7 +295,7 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
className : "danger", className : "danger",
// Handle action // Handle action
callback : function deleteCallback() { callback : function deleteCallback() {
deleteSessionsImmediately(); deleteAllSessionsImmediately();
guacNotification.showStatus(false); guacNotification.showStatus(false);
} }
}; };
@@ -285,24 +304,36 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
* Immediately deletes the selected sessions, without prompting the * Immediately deletes the selected sessions, without prompting the
* user for confirmation. * user for confirmation.
*/ */
var deleteSessionsImmediately = function deleteSessionsImmediately() { var deleteAllSessionsImmediately = function deleteAllSessionsImmediately() {
// Perform deletion var deletionRequests = [];
activeConnectionService.deleteActiveConnections(Object.keys(selectedWrappers))
.success(function activeConnectionsDeleted() { // 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 // Remove deleted connections from wrapper array
$scope.wrappers = $scope.wrappers.filter(function activeConnectionStillExists(wrapper) { $scope.wrappers = $scope.wrappers.filter(function activeConnectionStillExists(wrapper) {
return !(wrapper.activeConnection.identifier in selectedWrappers); return !(wrapper.activeConnection.identifier in (allSelectedWrappers[wrapper.dataSource] || {}));
}); });
// Clear selection // Clear selection
selectedWrappers = {}; allSelectedWrappers = {};
}) },
// Notify of any errors // Notify of any errors
.error(function activeConnectionDeletionFailed(error) { function activeConnectionDeletionFailed(error) {
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_ERROR', 'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_ERROR',
@@ -335,8 +366,10 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
$scope.canDeleteSessions = function canDeleteSessions() { $scope.canDeleteSessions = function canDeleteSessions() {
// We can delete sessions if at least one is selected // We can delete sessions if at least one is selected
for (var identifier in selectedWrappers) for (var dataSource in allSelectedWrappers) {
return true; for (var identifier in allSelectedWrappers[dataSource])
return true;
}
return false; return false;
@@ -351,6 +384,11 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
*/ */
$scope.wrapperSelectionChange = function wrapperSelectionChange(wrapper) { $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 // Add wrapper to map if selected
if (wrapper.checked) if (wrapper.checked)
selectedWrappers[wrapper.activeConnection.identifier] = wrapper; 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) { controller: ['$scope', '$injector', function settingsUsersController($scope, $injector) {
// Required types // Required types
var ManageableUser = $injector.get('ManageableUser');
var PermissionSet = $injector.get('PermissionSet'); var PermissionSet = $injector.get('PermissionSet');
var User = $injector.get('User');
// Required services // Required services
var $location = $injector.get('$location'); var $location = $injector.get('$location');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var dataSourceService = $injector.get('dataSourceService');
var guacNotification = $injector.get('guacNotification'); var guacNotification = $injector.get('guacNotification');
var permissionService = $injector.get('permissionService'); var permissionService = $injector.get('permissionService');
var userService = $injector.get('userService'); 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 * All visible users, along with their corresponding data sources.
* permissions have not yet been loaded, this will be null.
* *
* @type Boolean * @type ManageableUser[]
*/ */
$scope.canManageUsers = null; $scope.manageableUsers = 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;
/** /**
* The name of the new user to create, if any, when user creation * 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 = ""; $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. * user's permissions have not yet been loaded.
* *
* @type PermissionSet * @type Object.<String, PermissionSet>
*/ */
$scope.permissions = null; $scope.permissions = null;
@@ -110,80 +104,152 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings
*/ */
$scope.isLoaded = function isLoaded() { $scope.isLoaded = function isLoaded() {
return $scope.users !== null return $scope.manageableUsers !== null
&& $scope.permissions !== null && $scope.permissions !== null;
&& $scope.canManageUsers !== null
&& $scope.canCreateUsers !== 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 // Retrieve current permissions
permissionService.getPermissions(currentUsername) dataSourceService.apply(permissionService.getPermissions, dataSources, currentUsername)
.success(function permissionsRetrieved(permissions) { .then(function permissionsRetrieved(permissions) {
// Store retrieved permissions
$scope.permissions = 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 // Return to home if there's nothing to do here
if (!$scope.canManageUsers) if (!canManageUsers())
$location.path('/'); $location.path('/');
});
// Retrieve all users for whom we have UPDATE or DELETE permission var userPromise;
userService.getUsers([PermissionSet.ObjectPermissionType.UPDATE,
PermissionSet.ObjectPermissionType.DELETE]) // If users can be created, list all readable users
.success(function usersReceived(users) { if ($scope.canCreateUsers())
userPromise = dataSourceService.apply(userService.getUsers, dataSources);
// Otherwise, list only updateable/deletable users
else
userPromise = dataSourceService.apply(userService.getUsers, dataSources, [
PermissionSet.ObjectPermissionType.UPDATE,
PermissionSet.ObjectPermissionType.DELETE
]);
userPromise.then(function usersReceived(userArrays) {
var addedUsers = {};
$scope.manageableUsers = [];
// For each user in each data source
angular.forEach(dataSources, function addUserList(dataSource) {
angular.forEach(userArrays[dataSource], function addUser(user) {
// Do not add the same user twice
if (addedUsers[user.username])
return;
// Add user to overall list
addedUsers[user.username] = user;
$scope.manageableUsers.push(new ManageableUser ({
'dataSource' : dataSource,
'user' : user
}));
});
});
// Display only other users, not self
$scope.users = users.filter(function isNotSelf(user) {
return user.username !== currentUsername;
}); });
}); });
/** /**
* Creates a new user having the username specified in the user * Navigates to an interface for creating a new user having the
* creation interface. * username specified.
*/ */
$scope.newUser = function newUser() { $scope.newUser = function newUser() {
$location.url('/manage/' + encodeURIComponent(getDefaultDataSource()) + '/users/' + encodeURIComponent($scope.newUsername));
// 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 = "";
}; };
}] }]

View File

@@ -34,36 +34,3 @@
text-align: center; text-align: center;
margin: 1em 0; 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 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 Copyright (C) 2014 Glyptodon LLC

View File

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

View File

@@ -29,11 +29,11 @@
<a class="add-connection button" <a class="add-connection button"
ng-show="canCreateConnections" 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" <a class="add-connection-group button"
ng-show="canCreateConnectionGroups" 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> </div>
@@ -41,7 +41,7 @@
<div class="connection-list"> <div class="connection-list">
<guac-group-list <guac-group-list
page-size="25" page-size="25"
connection-group="rootGroup" connection-groups="rootGroups"
connection-template="'app/settings/templates/connection.html'" connection-template="'app/settings/templates/connection.html'"
connection-group-template="'app/settings/templates/connectionGroup.html'"/> connection-group-template="'app/settings/templates/connectionGroup.html'"/>
</div> </div>

View File

@@ -25,24 +25,25 @@
<p>{{'SETTINGS_USERS.HELP_USERS' | translate}}</p> <p>{{'SETTINGS_USERS.HELP_USERS' | translate}}</p>
<!-- Form action buttons --> <!-- 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"/> <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> <button class="add-user" ng-click="newUser()">{{'SETTINGS_USERS.ACTION_NEW_USER' | translate}}</button>
</div> </div>
<!-- List of users this user has access to --> <!-- List of users this user has access to -->
<div class="user-list"> <div class="user-list">
<div ng-repeat="user in userPage" class="user list-item"> <div ng-repeat="manageableUser in manageableUserPage" class="user list-item">
<a ng-href="#/manage/users/{{user.username}}"> <a ng-href="#/manage/{{manageableUser.dataSource}}/users/{{manageableUser.user.username}}">
<div class="caption"> <div class="caption">
<div class="icon user"></div> <div class="icon user"></div>
<span class="name">{{user.username}}</span> <span class="name">{{manageableUser.user.username}}</span>
</div> </div>
</a> </a>
</div> </div>
</div> </div>
<!-- Pager controls for user list --> <!-- 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> </div>

View File

@@ -31,44 +31,47 @@ angular.module('settings').factory('ActiveConnectionWrapper', [
* properties, such as a checked option. * properties, such as a checked option.
* *
* @constructor * @constructor
* @param {String} name * @param {ActiveConnectionWrapper|Object} template
* The display name of the active connection. * The object whose properties should be copied within the new
* * ActiveConnectionWrapper.
* @param {String} startDate
* The date and time this session began, pre-formatted for display.
*
* @param {ActiveConnection} activeConnection
* The ActiveConnection to wrap.
*/ */
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. * The display name of this connection.
* *
* @type String * @type String
*/ */
this.name = name; this.name = template.name;
/** /**
* The date and time this session began, pre-formatted for display. * The date and time this session began, pre-formatted for display.
* *
* @type String * @type String
*/ */
this.startDate = startDate; this.startDate = template.startDate;
/** /**
* The wrapped ActiveConnection. * The wrapped ActiveConnection.
* *
* @type ActiveConnection * @type ActiveConnection
*/ */
this.activeConnection = activeConnection; this.activeConnection = template.activeConnection;
/** /**
* A flag indicating that the active connection has been selected. * A flag indicating that the active connection has been selected.
* *
* @type Boolean * @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" : { "FORM" : {
"FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD", "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD",
@@ -240,7 +244,9 @@
"FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD",
"FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN",
"FIELD_HEADER_USERNAME" : "Username:", "FIELD_HEADER_USERNAME" : "Username:",
"INFO_READ_ONLY" : "Sorry, but this user account cannot be edited.",
"SECTION_HEADER_CONNECTIONS" : "Connections", "SECTION_HEADER_CONNECTIONS" : "Connections",
"SECTION_HEADER_EDIT_USER" : "Edit User", "SECTION_HEADER_EDIT_USER" : "Edit User",
"SECTION_HEADER_PERMISSIONS" : "Permissions", "SECTION_HEADER_PERMISSIONS" : "Permissions",