Merge pull request #153 from glyptodon/challenge-responses

GUAC-1161: Implement challenge-style responses
This commit is contained in:
James Muehlner
2015-04-21 22:31:53 -07:00
36 changed files with 1513 additions and 599 deletions

View File

@@ -0,0 +1,283 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.form;
import java.util.ArrayList;
import java.util.Collection;
/**
* Represents an arbitrary parameter, such as an HTTP parameter, or the
* parameter of a remote desktop protocol.
*
* @author Michael Jumper
*/
public class Parameter {
/**
* All possible types of parameter.
*/
public enum Type {
/**
* A text parameter, accepting arbitrary values.
*/
TEXT,
/**
* A username parameter. This parameter type generally behaves
* identically to arbitrary text parameters, but has semantic
* differences. If credential pass-through is in use, the value for this
* parameter may be automatically provided using the credentials
* originally used by the user to authenticate.
*/
USERNAME,
/**
* A password parameter, whose value is sensitive and must be hidden. If
* credential pass-through is in use, the value for this parameter may
* be automatically provided using the credentials originally used by
* the user to authenticate.
*/
PASSWORD,
/**
* A numeric parameter, whose value must contain only digits.
*/
NUMERIC,
/**
* A boolean parameter, whose value is either blank or "true".
*/
BOOLEAN,
/**
* An enumerated parameter, whose legal values are fully enumerated
* by a provided, finite list.
*/
ENUM,
/**
* A text parameter that can span more than one line.
*/
MULTILINE
}
/**
* The unique name that identifies this parameter.
*/
private String name;
/**
* A human-readable name to be presented to the user.
*/
private String title;
/**
* The type of this parameter.
*/
private Type type;
/**
* The value of this parameter, when checked. This is only applicable to
* BOOLEAN parameters.
*/
private String value;
/**
* A collection of all associated parameter options.
*/
private Collection<ParameterOption> options;
/**
* Creates a new Parameter with no associated name, title, or type.
*/
public Parameter() {
this.options = new ArrayList<ParameterOption>();
}
/**
* Creates a new Parameter with the given name, title, and type.
*
* @param name
* The unique name to associate with this parameter.
*
* @param title
* The human-readable title to associate with this parameter.
*
* @param type
* The type of this parameter.
*/
public Parameter(String name, String title, Type type) {
this.name = name;
this.title = title;
this.type = type;
this.options = new ArrayList<ParameterOption>();
}
/**
* Creates a new BOOLEAN Parameter with the given name, title, and value.
*
* @param name
* The unique name to associate with this parameter.
*
* @param title
* The human-readable title to associate with this parameter.
*
* @param value
* The value that should be assigned to this parameter if enabled.
*/
public Parameter(String name, String title, String value) {
this.name = name;
this.title = title;
this.type = Type.BOOLEAN;
this.value = value;
this.options = new ArrayList<ParameterOption>();
}
/**
* Creates a new ENUM Parameter with the given name, title, and options.
*
* @param name
* The unique name to associate with this parameter.
*
* @param title
* The human-readable title to associate with this parameter.
*
* @param options
* A collection of all possible valid options for this parameter.
*/
public Parameter(String name, String title, Collection<ParameterOption> options) {
this.name = name;
this.title = title;
this.type = Type.ENUM;
this.options = options;
}
/**
* Returns the unique name associated with this parameter.
*
* @return
* The unique name associated with this parameter.
*/
public String getName() {
return name;
}
/**
* Sets the unique name associated with this parameter.
*
* @param name
* The unique name to assign to this parameter.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns the human-readable title associated with this parameter.
*
* @return
* The human-readable title associated with this parameter.
*/
public String getTitle() {
return title;
}
/**
* Sets the title associated with this parameter. The title must be a
* human-readable string which describes accurately this parameter.
*
* @param title
* A human-readable string describing this parameter.
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Returns the value that should be assigned to this parameter if enabled.
* This is only applicable to BOOLEAN parameters.
*
* @return
* The value that should be assigned to this parameter if enabled.
*/
public String getValue() {
return value;
}
/**
* Sets the value that should be assigned to this parameter if enabled.
* This is only applicable to BOOLEAN parameters.
*
* @param value
* The value that should be assigned to this parameter if enabled.
*/
public void setValue(String value) {
this.value = value;
}
/**
* Returns the type of this parameter.
*
* @return
* The type of this parameter.
*/
public Type getType() {
return type;
}
/**
* Sets the type of this parameter.
*
* @param type
* The type of this parameter.
*/
public void setType(Type type) {
this.type = type;
}
/**
* Returns a mutable collection of parameter options. Changes to this
* collection directly affect the available options. This is only
* applicable to ENUM parameters.
*
* @return
* A mutable collection of parameter options.
*/
public Collection<ParameterOption> getOptions() {
return options;
}
/**
* Sets the options available as possible values of this parameter. This
* is only applicable to ENUM parameters.
*
* @param options
* The options to associate with this parameter.
*/
public void setOptions(Collection<ParameterOption> options) {
this.options = options;
}
}

View File

@@ -20,18 +20,17 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package org.glyptodon.guacamole.protocols; package org.glyptodon.guacamole.form;
/** /**
* Describes an available legal value for an enumerated protocol parameter. * Describes an available legal value for an enumerated parameter.
* *
* @author Michael Jumper * @author Michael Jumper
*/ */
public class ProtocolParameterOption { public class ParameterOption {
/** /**
* The value that will be sent to the client plugin if this option is * The value that will be assigned if this option is chosen.
* chosen.
*/ */
private String value; private String value;
@@ -41,20 +40,40 @@ public class ProtocolParameterOption {
private String title; private String title;
/** /**
* Returns the value that will be sent to the client plugin if this option * Creates a new ParameterOption with no associated value or title.
* is chosen. */
public ParameterOption() {
}
/**
* Creates a new ParameterOption having the given value and title.
* *
* @return The value that will be sent if this option is chosen. * @param value
* The value to assign if this option is chosen.
*
* @param title
* The human-readable title to associate with this option.
*/
public ParameterOption(String value, String title) {
this.value = value;
this.title = title;
}
/**
* Returns the value that will be assigned if this option is chosen.
*
* @return
* The value that will be assigned if this option is chosen.
*/ */
public String getValue() { public String getValue() {
return value; return value;
} }
/** /**
* Sets the value that will be sent to the client plugin if this option is * Sets the value that will be assigned if this option is chosen.
* chosen.
* *
* @param value The value to send if this option is chosen. * @param value
* The value to assign if this option is chosen.
*/ */
public void setValue(String value) { public void setValue(String value) {
this.value = value; this.value = value;
@@ -62,7 +81,9 @@ public class ProtocolParameterOption {
/** /**
* Returns the human-readable title describing the effect of this option. * Returns the human-readable title describing the effect of this option.
* @return The human-readable title describing the effect of this option. *
* @return
* The human-readable title describing the effect of this option.
*/ */
public String getTitle() { public String getTitle() {
return title; return title;
@@ -70,7 +91,9 @@ public class ProtocolParameterOption {
/** /**
* Sets the human-readable title describing the effect of this option. * Sets the human-readable title describing the effect of this option.
* @param title A human-readable title describing the effect of this option. *
* @param title
* A human-readable title describing the effect of this option.
*/ */
public void setTitle(String title) { public void setTitle(String title) {
this.title = title; this.title = title;

View File

@@ -0,0 +1,79 @@
/*
* 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.auth.credentials;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.glyptodon.guacamole.form.Parameter;
/**
* Information which describes a set of valid credentials.
*
* @author Michael Jumper
*/
public class CredentialsInfo {
/**
* All parameters required for valid credentials.
*/
private final Collection<Parameter> parameters;
/**
* Creates a new CredentialsInfo object which requires the given parameters
* for any conforming credentials.
*
* @param parameters
* The parameters to require.
*/
public CredentialsInfo(Collection<Parameter> parameters) {
this.parameters = parameters;
}
/**
* Returns all parameters required for valid credentials as described by
* this object.
*
* @return
* All parameters required for valid credentials.
*/
public Collection<Parameter> getParameters() {
return Collections.unmodifiableCollection(parameters);
}
/**
* CredentialsInfo object which describes empty credentials. No parameters
* are required.
*/
public static final CredentialsInfo EMPTY = new CredentialsInfo(Collections.EMPTY_LIST);
/**
* CredentialsInfo object which describes standard username/password
* credentials.
*/
public static final CredentialsInfo USERNAME_PASSWORD = new CredentialsInfo(Arrays.asList(
new Parameter("username", "username", Parameter.Type.USERNAME),
new Parameter("password", "password", Parameter.Type.PASSWORD)
));
}

View File

@@ -0,0 +1,100 @@
/*
* 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.auth.credentials;
import org.glyptodon.guacamole.GuacamoleSecurityException;
/**
* A security-related exception thrown when access is denied to a user because
* of a problem related to the provided credentials. Additional information
* describing the form of valid credentials is provided.
*
* @author Michael Jumper
*/
public class GuacamoleCredentialsException extends GuacamoleSecurityException {
/**
* Information describing the form of valid credentials.
*/
private final CredentialsInfo credentialsInfo;
/**
* Creates a new GuacamoleInvalidCredentialsException with the given
* message, cause, and associated credential information.
*
* @param message
* A human readable description of the exception that occurred.
*
* @param cause
* The cause of this exception.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleCredentialsException(String message, Throwable cause,
CredentialsInfo credentialsInfo) {
super(message, cause);
this.credentialsInfo = credentialsInfo;
}
/**
* Creates a new GuacamoleInvalidCredentialsException with the given
* message and associated credential information.
*
* @param message
* A human readable description of the exception that occurred.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleCredentialsException(String message, CredentialsInfo credentialsInfo) {
super(message);
this.credentialsInfo = credentialsInfo;
}
/**
* Creates a new GuacamoleInvalidCredentialsException with the given cause
* and associated credential information.
*
* @param cause
* The cause of this exception.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) {
super(cause);
this.credentialsInfo = credentialsInfo;
}
/**
* Returns information describing the form of valid credentials.
*
* @return
* Information describing the form of valid credentials.
*/
public CredentialsInfo getCredentialsInfo() {
return credentialsInfo;
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.auth.credentials;
/**
* A security-related exception thrown when access is denied to a user because
* the provided credentials are not sufficient for authentication to succeed.
* The validity or invalidity of the given credentials is not specified, and
* more information is needed before a decision can be made. Additional
* information describing the form of valid credentials is provided.
*
* @author Michael Jumper
*/
public class GuacamoleInsufficientCredentialsException extends GuacamoleCredentialsException {
/**
* Creates a new GuacamoleInsufficientCredentialsException with the given
* message, cause, and associated credential information.
*
* @param message
* A human readable description of the exception that occurred.
*
* @param cause
* The cause of this exception.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleInsufficientCredentialsException(String message, Throwable cause,
CredentialsInfo credentialsInfo) {
super(message, cause, credentialsInfo);
}
/**
* Creates a new GuacamoleInsufficientCredentialsException with the given
* message and associated credential information.
*
* @param message
* A human readable description of the exception that occurred.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleInsufficientCredentialsException(String message, CredentialsInfo credentialsInfo) {
super(message, credentialsInfo);
}
/**
* Creates a new GuacamoleInsufficientCredentialsException with the given
* cause and associated credential information.
*
* @param cause
* The cause of this exception.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleInsufficientCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) {
super(cause, credentialsInfo);
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.auth.credentials;
/**
* A security-related exception thrown when access is denied to a user because
* the provided credentials are invalid. Additional information describing
* the form of valid credentials is provided.
*
* @author Michael Jumper
*/
public class GuacamoleInvalidCredentialsException extends GuacamoleCredentialsException {
/**
* Creates a new GuacamoleInvalidCredentialsException with the given
* message, cause, and associated credential information.
*
* @param message
* A human readable description of the exception that occurred.
*
* @param cause
* The cause of this exception.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleInvalidCredentialsException(String message, Throwable cause,
CredentialsInfo credentialsInfo) {
super(message, cause, credentialsInfo);
}
/**
* Creates a new GuacamoleInvalidCredentialsException with the given
* message and associated credential information.
*
* @param message
* A human readable description of the exception that occurred.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleInvalidCredentialsException(String message, CredentialsInfo credentialsInfo) {
super(message, credentialsInfo);
}
/**
* Creates a new GuacamoleInvalidCredentialsException with the given cause
* and associated credential information.
*
* @param cause
* The cause of this exception.
*
* @param credentialsInfo
* Information describing the form of valid credentials.
*/
public GuacamoleInvalidCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) {
super(cause, credentialsInfo);
}
}

View File

@@ -24,6 +24,7 @@ package org.glyptodon.guacamole.protocols;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import org.glyptodon.guacamole.form.Parameter;
/** /**
* Describes a protocol and all parameters associated with it, as required by * Describes a protocol and all parameters associated with it, as required by
@@ -47,8 +48,49 @@ public class ProtocolInfo {
/** /**
* A collection of all associated protocol parameters. * A collection of all associated protocol parameters.
*/ */
private Collection<ProtocolParameter> parameters = private Collection<Parameter> parameters;
new ArrayList<ProtocolParameter>();
/**
* Creates a new ProtocolInfo with no associated name, title, or
* parameters.
*/
public ProtocolInfo() {
this.parameters = new ArrayList<Parameter>();
}
/**
* Creates a new ProtocolInfo having the given name and title, but without
* any parameters.
*
* @param name
* The unique name associated with the protocol.
*
* @param title
* The human-readable title to associate with the protocol.
*/
public ProtocolInfo(String name, String title) {
this.name = name;
this.title = title;
this.parameters = new ArrayList<Parameter>();
}
/**
* Creates a new ProtocolInfo having the given name, title, and parameters.
*
* @param name
* The unique name associated with the protocol.
*
* @param title
* The human-readable title to associate with the protocol.
*
* @param parameters
* The parameters to associate with the protocol.
*/
public ProtocolInfo(String name, String title, Collection<Parameter> parameters) {
this.name = name;
this.title = title;
this.parameters = parameters;
}
/** /**
* Returns the human-readable title associated with this protocol. * Returns the human-readable title associated with this protocol.
@@ -95,8 +137,19 @@ public class ProtocolInfo {
* *
* @return A mutable collection of protocol parameters. * @return A mutable collection of protocol parameters.
*/ */
public Collection<ProtocolParameter> getParameters() { public Collection<Parameter> getParameters() {
return parameters; return parameters;
} }
/**
* Sets the collection of protocol parameters associated with this
* protocol.
*
* @param parameters
* The collection of parameters to associate with this protocol.
*/
public void setParameters(Collection<Parameter> parameters) {
this.parameters = parameters;
}
} }

View File

@@ -1,192 +0,0 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.protocols;
import java.util.ArrayList;
import java.util.Collection;
/**
* Represents a parameter of a protocol.
*
* @author Michael Jumper
*/
public class ProtocolParameter {
/**
* All possible types of protocol parameter.
*/
public enum Type {
/**
* A text parameter, accepting arbitrary values.
*/
TEXT,
/**
* A username parameter. This parameter type generally behaves
* identically to arbitrary text parameters, but has semantic
* differences. If credential pass-through is in use, the value for this
* parameter may be automatically provided using the credentials
* originally used by the user to authenticate.
*/
USERNAME,
/**
* A password parameter, whose value is sensitive and must be hidden. If
* credential pass-through is in use, the value for this parameter may
* be automatically provided using the credentials originally used by
* the user to authenticate.
*/
PASSWORD,
/**
* A numeric parameter, whose value must contain only digits.
*/
NUMERIC,
/**
* A boolean parameter, whose value is either blank or "true".
*/
BOOLEAN,
/**
* An enumerated parameter, whose legal values are fully enumerated
* by a provided, finite list.
*/
ENUM,
/**
* A text parameter that can span more than one line.
*/
MULTILINE
}
/**
* The unique name that identifies this parameter to the protocol plugin.
*/
private String name;
/**
* A human-readable name to be presented to the user.
*/
private String title;
/**
* The type of this field.
*/
private Type type;
/**
* The value of this parameter, for boolean parameters.
*/
private String value;
/**
* A collection of all associated parameter options.
*/
private Collection<ProtocolParameterOption> options =
new ArrayList<ProtocolParameterOption>();
/**
* Returns the name associated with this protocol parameter.
* @return The name associated with this protocol parameter.
*/
public String getName() {
return name;
}
/**
* Sets the name associated with this protocol parameter. This name must
* uniquely identify this parameter among the others accepted by the
* corresponding protocol.
*
* @param name The name to assign to this protocol parameter.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns the title associated with this protocol parameter.
* @return The title associated with this protocol parameter.
*/
public String getTitle() {
return title;
}
/**
* Sets the title associated with this protocol parameter. The title must
* be a human-readable string which describes accurately this parameter.
*
* @param title A human-readable string describing this parameter.
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Returns the value associated with this protocol parameter.
* @return The value associated with this protocol parameter.
*/
public String getValue() {
return value;
}
/**
* Sets the value associated with this protocol parameter. The value must
* be a human-readable string which describes accurately this parameter.
*
* @param value A human-readable string describing this parameter.
*/
public void setValue(String value) {
this.value = value;
}
/**
* Returns the type of this parameter.
* @return The type of this parameter.
*/
public Type getType() {
return type;
}
/**
* Sets the type of this parameter.
* @param type The type of this parameter.
*/
public void setType(Type type) {
this.type = type;
}
/**
* Returns a mutable collection of protocol parameter options. Changes to
* this collection directly affect the available options.
*
* @return A mutable collection of parameter options.
*/
public Collection<ProtocolParameterOption> getOptions() {
return options;
}
}

View File

@@ -22,7 +22,7 @@
package org.glyptodon.guacamole.xml.protocol; package org.glyptodon.guacamole.xml.protocol;
import org.glyptodon.guacamole.protocols.ProtocolParameterOption; import org.glyptodon.guacamole.form.ParameterOption;
import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.xml.TagHandler;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
@@ -37,7 +37,7 @@ public class OptionTagHandler implements TagHandler {
/** /**
* The option backing this option tag. * The option backing this option tag.
*/ */
private ProtocolParameterOption option = new ProtocolParameterOption(); private ParameterOption option = new ParameterOption();
@Override @Override
public void init(Attributes attributes) throws SAXException { public void init(Attributes attributes) throws SAXException {
@@ -55,10 +55,10 @@ public class OptionTagHandler implements TagHandler {
} }
/** /**
* Returns the ProtocolParameterOption backing this tag. * Returns the ParameterOption backing this tag.
* @return The ProtocolParameterOption backing this tag. * @return The ParameterOption backing this tag.
*/ */
public ProtocolParameterOption asProtocolParameterOption() { public ParameterOption asParameterOption() {
return option; return option;
} }

View File

@@ -22,7 +22,7 @@
package org.glyptodon.guacamole.xml.protocol; package org.glyptodon.guacamole.xml.protocol;
import org.glyptodon.guacamole.protocols.ProtocolParameter; import org.glyptodon.guacamole.form.Parameter;
import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.xml.TagHandler;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
@@ -35,9 +35,9 @@ import org.xml.sax.SAXException;
public class ParamTagHandler implements TagHandler { public class ParamTagHandler implements TagHandler {
/** /**
* The ProtocolParameter backing this tag handler. * The Parameter backing this tag handler.
*/ */
private ProtocolParameter protocolParameter = new ProtocolParameter(); private Parameter protocolParameter = new Parameter();
@Override @Override
public void init(Attributes attributes) throws SAXException { public void init(Attributes attributes) throws SAXException {
@@ -51,31 +51,31 @@ public class ParamTagHandler implements TagHandler {
// Text field // Text field
if ("text".equals(type)) if ("text".equals(type))
protocolParameter.setType(ProtocolParameter.Type.TEXT); protocolParameter.setType(Parameter.Type.TEXT);
// Numeric field // Numeric field
else if ("numeric".equals(type)) else if ("numeric".equals(type))
protocolParameter.setType(ProtocolParameter.Type.NUMERIC); protocolParameter.setType(Parameter.Type.NUMERIC);
// Username field // Username field
else if ("username".equals(type)) else if ("username".equals(type))
protocolParameter.setType(ProtocolParameter.Type.USERNAME); protocolParameter.setType(Parameter.Type.USERNAME);
// Password field // Password field
else if ("password".equals(type)) else if ("password".equals(type))
protocolParameter.setType(ProtocolParameter.Type.PASSWORD); protocolParameter.setType(Parameter.Type.PASSWORD);
// Enumerated field // Enumerated field
else if ("enum".equals(type)) else if ("enum".equals(type))
protocolParameter.setType(ProtocolParameter.Type.ENUM); protocolParameter.setType(Parameter.Type.ENUM);
// Multiline field // Multiline field
else if ("multiline".equals(type)) else if ("multiline".equals(type))
protocolParameter.setType(ProtocolParameter.Type.MULTILINE); protocolParameter.setType(Parameter.Type.MULTILINE);
// Boolean field // Boolean field
else if ("boolean".equals(type)) { else if ("boolean".equals(type)) {
protocolParameter.setType(ProtocolParameter.Type.BOOLEAN); protocolParameter.setType(Parameter.Type.BOOLEAN);
if(protocolParameter.getValue() == null) if(protocolParameter.getValue() == null)
throw new SAXException throw new SAXException
@@ -99,7 +99,7 @@ public class ParamTagHandler implements TagHandler {
// Store stub in options collection // Store stub in options collection
protocolParameter.getOptions().add( protocolParameter.getOptions().add(
tagHandler.asProtocolParameterOption()); tagHandler.asParameterOption());
return tagHandler; return tagHandler;
} }
@@ -114,10 +114,10 @@ public class ParamTagHandler implements TagHandler {
} }
/** /**
* Returns the ProtocolParameter backing this tag. * Returns the Parameter backing this tag.
* @return The ProtocolParameter backing this tag. * @return The Parameter backing this tag.
*/ */
public ProtocolParameter asProtocolParameter() { public Parameter asParameter() {
return protocolParameter; return protocolParameter;
} }

View File

@@ -56,7 +56,7 @@ public class ProtocolTagHandler implements TagHandler {
ParamTagHandler tagHandler = new ParamTagHandler(); ParamTagHandler tagHandler = new ParamTagHandler();
// Store stub in parameters collection // Store stub in parameters collection
info.getParameters().add(tagHandler.asProtocolParameter()); info.getParameters().add(tagHandler.asParameter());
return tagHandler; return tagHandler;
} }

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
@@ -22,9 +22,15 @@
package org.glyptodon.guacamole.net.basic.rest; package org.glyptodon.guacamole.net.basic.rest;
import java.util.Collection;
import javax.ws.rs.core.Response;
import org.glyptodon.guacamole.form.Parameter;
/** /**
* A simple object to represent an error to be sent from the REST API. * Describes an error that occurred within a REST endpoint.
*
* @author James Muehlner * @author James Muehlner
* @author Michael Jumper
*/ */
public class APIError { public class APIError {
@@ -34,19 +40,143 @@ public class APIError {
private final String message; private final String message;
/** /**
* Get the error message. * All expected request parameters, if any.
* @return The error message. */
private final Collection<Parameter> expected;
/**
* The type of error that occurred.
*/
private final Type type;
/**
* All possible types of REST API errors.
*/
public enum Type {
/**
* The requested operation could not be performed because the request
* itself was malformed.
*/
BAD_REQUEST(Response.Status.BAD_REQUEST),
/**
* The credentials provided were invalid.
*/
INVALID_CREDENTIALS(Response.Status.FORBIDDEN),
/**
* The credentials provided were not necessarily invalid, but were not
* sufficient to determine validity.
*/
INSUFFICIENT_CREDENTIALS(Response.Status.FORBIDDEN),
/**
* An internal server error has occurred.
*/
INTERNAL_ERROR(Response.Status.INTERNAL_SERVER_ERROR),
/**
* An object related to the request does not exist.
*/
NOT_FOUND(Response.Status.NOT_FOUND),
/**
* Permission was denied to perform the requested operation.
*/
PERMISSION_DENIED(Response.Status.FORBIDDEN);
/**
* The HTTP status associated with this error type.
*/
private final Response.Status status;
/**
* Defines a new error type associated with the given HTTP status.
*
* @param status
* The HTTP status to associate with the error type.
*/
Type(Response.Status status) {
this.status = status;
}
/**
* Returns the HTTP status associated with this error type.
*
* @return
* The HTTP status associated with this error type.
*/
public Response.Status getStatus() {
return status;
}
}
/**
* Create a new APIError with the specified error message.
*
* @param type
* The type of error that occurred.
*
* @param message
* The error message.
*/
public APIError(Type type, String message) {
this.type = type;
this.message = message;
this.expected = null;
}
/**
* Create a new APIError with the specified error message and parameter
* information.
*
* @param type
* The type of error that occurred.
*
* @param message
* The error message.
*
* @param expected
* All parameters expected in the original request, or now required as
* a result of the original request.
*/
public APIError(Type type, String message, Collection<Parameter> expected) {
this.type = type;
this.message = message;
this.expected = expected;
}
/**
* Returns the type of error that occurred.
*
* @return
* The type of error that occurred.
*/
public Type getType() {
return type;
}
/**
* Returns an object which describes the required credentials.
*
* @return
* An object which describes the required credentials.
*/
public Collection<Parameter> getExpected() {
return expected;
}
/**
* Returns a human-readable error message describing the error that
* occurred.
*
* @return
* A human-readable error message.
*/ */
public String getMessage() { public String getMessage() {
return message; return message;
} }
/**
* Create a new APIError with the specified error message.
* @param message The error message.
*/
public APIError(String message) {
this.message = message;
}
} }

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.rest;
import java.util.Collection;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import org.glyptodon.guacamole.form.Parameter;
/**
* An exception that will result in the given error error information being
* returned from the API layer. All error messages have the same format which
* is defined by APIError.
*
* @author James Muehlner
* @author Michael Jumper
*/
public class APIException extends WebApplicationException {
/**
* Construct a new APIException with the given error. All information
* associated with this new exception will be extracted from the given
* APIError.
*
* @param error
* The error that occurred.
*/
public APIException(APIError error) {
super(Response.status(error.getType().getStatus()).entity(error).build());
}
/**
* Creates a new APIException with the given type and message. The
* corresponding APIError will be created from the provided information.
*
* @param type
* The type of error that occurred.
*
* @param message
* A human-readable message describing the error.
*/
public APIException(APIError.Type type, String message) {
this(new APIError(type, message));
}
/**
* Creates a new APIException with the given type, message, and parameter
* information. The corresponding APIError will be created from the
* provided information.
*
* @param type
* The type of error that occurred.
*
* @param message
* A human-readable message describing the error.
*
* @param expected
* All parameters expected in the original request, or now required as
* a result of the original request.
*/
public APIException(APIError.Type type, String message, Collection<Parameter> expected) {
this(new APIError(type, message, expected));
}
}

View File

@@ -27,7 +27,11 @@ import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleClientException;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleResourceNotFoundException;
import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.GuacamoleSecurityException;
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -49,15 +53,96 @@ public class AuthProviderRESTExceptionWrapper implements MethodInterceptor {
try { try {
return invocation.proceed(); return invocation.proceed();
} }
// Additional credentials are needed
catch (GuacamoleInsufficientCredentialsException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Permission denied.";
throw new APIException(
APIError.Type.INSUFFICIENT_CREDENTIALS,
message,
e.getCredentialsInfo().getParameters()
);
}
// The provided credentials are wrong
catch (GuacamoleInvalidCredentialsException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Permission denied.";
throw new APIException(
APIError.Type.INVALID_CREDENTIALS,
message,
e.getCredentialsInfo().getParameters()
);
}
// Generic permission denied
catch (GuacamoleSecurityException e) { catch (GuacamoleSecurityException e) {
throw new HTTPException(Response.Status.FORBIDDEN, e.getMessage() != null ? e.getMessage() : "Permission denied.");
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Permission denied.";
throw new APIException(
APIError.Type.PERMISSION_DENIED,
message
);
} }
// Arbitrary resource not found
catch (GuacamoleResourceNotFoundException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Not found.";
throw new APIException(
APIError.Type.NOT_FOUND,
message
);
}
// Arbitrary bad requests
catch (GuacamoleClientException e) { catch (GuacamoleClientException e) {
throw new HTTPException(Response.Status.BAD_REQUEST, e.getMessage() != null ? e.getMessage() : "Invalid Request.");
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Invalid request.";
throw new APIException(
APIError.Type.BAD_REQUEST,
message
);
} }
// All other errors
catch (GuacamoleException e) { catch (GuacamoleException e) {
logger.error("Unexpected GuacamoleException caught while executing " + invocation.getMethod().getName() + ".", e);
throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() != null ? e.getMessage() : "Unexpected server error."); // Generate default message
String message = e.getMessage();
if (message == null)
message = "Unexpected server error.";
logger.debug("Unexpected exception in REST endpoint.", e);
throw new APIException(
APIError.Type.INTERNAL_ERROR,
message
);
} }
} }

View File

@@ -1,58 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.rest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
/**
* An exception that will result in the given HTTP Status and message or entity
* being returned from the API layer.
*
* @author James Muehlner
*/
public class HTTPException extends WebApplicationException {
/**
* Construct a new HTTPException with the given HTTP status and entity.
*
* @param status The HTTP Status to use for the response.
* @param entity The entity to use as the body of the response.
*/
public HTTPException(Status status, Object entity) {
super(Response.status(status).entity(entity).build());
}
/**
* Construct a new HTTPException with the given HTTP status and message. The
* message will be wrapped in an APIError container.
*
* @param status The HTTP Status to use for the response.
* @param message The message to build the response entity with.
*/
public HTTPException(Status status, String message) {
super(Response.status(status).entity(new APIError(message)).build());
}
}

View File

@@ -35,16 +35,18 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response.Status;
import javax.xml.bind.DatatypeConverter; import javax.xml.bind.DatatypeConverter;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider; 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.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.GuacamoleSession;
import org.glyptodon.guacamole.net.basic.rest.APIError;
import org.glyptodon.guacamole.net.basic.rest.APIRequest; import org.glyptodon.guacamole.net.basic.rest.APIRequest;
import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure;
import org.glyptodon.guacamole.net.basic.rest.HTTPException; import org.glyptodon.guacamole.net.basic.rest.APIException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -233,15 +235,13 @@ public class TokenRESTService {
} }
// Request standard username/password if no user context was produced
if (userContext == null)
throw new GuacamoleInvalidCredentialsException("Permission Denied.",
CredentialsInfo.USERNAME_PASSWORD);
} }
catch (GuacamoleException e) { catch (GuacamoleException e) {
logger.error("Exception caught while authenticating user.", e);
throw new HTTPException(Status.INTERNAL_SERVER_ERROR,
"Unexpected server error.");
}
// Authentication failed.
if (userContext == null) {
// Log authentication failures with associated usernames // Log authentication failures with associated usernames
if (username != null) { if (username != null) {
@@ -255,8 +255,7 @@ public class TokenRESTService {
logger.debug("Anonymous authentication attempt from {} failed.", logger.debug("Anonymous authentication attempt from {} failed.",
getLoggableAddress(request), username); getLoggableAddress(request), username);
throw new HTTPException(Status.UNAUTHORIZED, "Permission Denied."); throw e;
} }
// Update existing session, if it exists // Update existing session, if it exists
@@ -291,7 +290,7 @@ public class TokenRESTService {
GuacamoleSession session = tokenSessionMap.remove(authToken); GuacamoleSession session = tokenSessionMap.remove(authToken);
if (session == null) if (session == null)
throw new HTTPException(Status.NOT_FOUND, "No such token."); throw new APIException(APIError.Type.NOT_FOUND, "No such token.");
session.invalidate(); session.invalidate();

View File

@@ -39,8 +39,6 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
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.AuthenticationProvider;
@@ -53,11 +51,12 @@ 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.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;
import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.remove; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.remove;
import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure;
import org.glyptodon.guacamole.net.basic.rest.HTTPException; import org.glyptodon.guacamole.net.basic.rest.APIException;
import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; 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;
@@ -276,12 +275,12 @@ public class UserRESTService {
// Validate data and path are sane // Validate data and path are sane
if (!user.getUsername().equals(username)) if (!user.getUsername().equals(username))
throw new HTTPException(Response.Status.BAD_REQUEST, throw new APIException(APIError.Type.BAD_REQUEST,
"Username in path does not match username provided JSON data."); "Username in path does not match username provided JSON data.");
// A user may not use this endpoint to modify himself // A user may not use this endpoint to modify himself
if (userContext.self().getIdentifier().equals(user.getUsername())) { if (userContext.self().getIdentifier().equals(user.getUsername())) {
throw new HTTPException(Response.Status.FORBIDDEN, throw new APIException(APIError.Type.PERMISSION_DENIED,
"Permission denied."); "Permission denied.");
} }
@@ -336,7 +335,7 @@ public class UserRESTService {
// Verify that the old password was correct // Verify that the old password was correct
if (authProvider.getUserContext(credentials) == null) { if (authProvider.getUserContext(credentials) == null) {
throw new HTTPException(Response.Status.FORBIDDEN, throw new APIException(APIError.Type.PERMISSION_DENIED,
"Permission denied."); "Permission denied.");
} }
@@ -467,7 +466,7 @@ public class UserRESTService {
// Unsupported patch operation // Unsupported patch operation
default: default:
throw new HTTPException(Status.BAD_REQUEST, throw new APIException(APIError.Type.BAD_REQUEST,
"Unsupported patch operation: \"" + operation + "\""); "Unsupported patch operation: \"" + operation + "\"");
} }
@@ -586,7 +585,7 @@ public class UserRESTService {
// Otherwise, the path is not supported // Otherwise, the path is not supported
else else
throw new HTTPException(Status.BAD_REQUEST, "Unsupported patch path: \"" + path + "\""); throw new APIException(APIError.Type.BAD_REQUEST, "Unsupported patch path: \"" + path + "\"");
} // end for each patch operation } // end for each patch operation

View File

@@ -32,10 +32,20 @@
* If a login attempt results in an existing token being replaced, 'guacLogout' * If a login attempt results in an existing token being replaced, 'guacLogout'
* will be broadcast first for the token being replaced, followed by * will be broadcast first for the token being replaced, followed by
* 'guacLogin' for the new token. * 'guacLogin' for the new token.
*
* Failed logins may also result in guacInsufficientCredentials or
* guacInvalidCredentials events, if the provided credentials were rejected for
* being insufficient or invalid respectively. Both events will be provided
* the set of parameters originally given to authenticate() and the set of
* expected credentials returned by the REST endpoint. This set of credentials
* will be in the form of a Field array.
*/ */
angular.module('auth').factory('authenticationService', ['$injector', angular.module('auth').factory('authenticationService', ['$injector',
function authenticationService($injector) { function authenticationService($injector) {
// Required types
var Error = $injector.get('Error');
// Required services // Required services
var $cookieStore = $injector.get('$cookieStore'); var $cookieStore = $injector.get('$cookieStore');
var $http = $injector.get('$http'); var $http = $injector.get('$http');
@@ -140,7 +150,16 @@ angular.module('auth').factory('authenticationService', ['$injector',
}) })
// If authentication fails, propogate failure to returned promise // If authentication fails, propogate failure to returned promise
.error(function authenticationFailed() { .error(function authenticationFailed(error) {
// Request credentials if provided credentials were invalid
if (error.type === Error.Type.INVALID_CREDENTIALS)
$rootScope.$broadcast('guacInvalidCredentials', parameters, error.expected);
// Request more credentials if provided credentials were not enough
else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS)
$rootScope.$broadcast('guacInsufficientCredentials', parameters, error.expected);
authenticationProcess.reject(); authenticationProcess.reject();
}); });

View File

@@ -113,34 +113,6 @@ angular.module('form').directive('guacForm', [function form() {
}; };
/**
* Produces the translation string for the header of the given
* field. The translation string will be of the form:
*
* <code>NAMESPACE.FIELD_HEADER_NAME<code>
*
* where <code>NAMESPACE</code> is the namespace provided to the
* directive and <code>NAME</code> is the field name transformed
* via translationStringService.canonicalize().
*
* @param {Field} field
* The field for which to produce the translation string.
*
* @returns {String}
* The translation string which produces the translated header
* of the field.
*/
$scope.getFieldHeader = function getFieldHeader(field) {
// If no field, or no name, then no header
if (!field || !field.name)
return '';
return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE')
+ '.FIELD_HEADER_' + translationStringService.canonicalize(field.name);
};
/** /**
* Determines whether the given object is a form, under the * Determines whether the given object is a form, under the
* assumption that the object is either a form or a field. * assumption that the object is either a form or a field.

View File

@@ -110,6 +110,31 @@ angular.module('form').directive('guacFormField', [function formField() {
}; };
/**
* Produces the translation string for the header of the current
* field. The translation string will be of the form:
*
* <code>NAMESPACE.FIELD_HEADER_NAME<code>
*
* where <code>NAMESPACE</code> is the namespace provided to the
* directive and <code>NAME</code> is the field name transformed
* via translationStringService.canonicalize().
*
* @returns {String}
* The translation string which produces the translated header
* of the field.
*/
$scope.getFieldHeader = function getFieldHeader() {
// If no field, or no name, then no header
if (!$scope.field || !$scope.field.name)
return '';
return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE')
+ '.FIELD_HEADER_' + translationStringService.canonicalize($scope.field.name);
};
/** /**
* Produces the translation string for the given field option * Produces the translation string for the given field option
* value. The translation string will be of the form: * value. The translation string will be of the form:

View File

@@ -25,13 +25,9 @@
<h3 ng-show="form.name">{{getSectionHeader(form) | translate}}</h3> <h3 ng-show="form.name">{{getSectionHeader(form) | translate}}</h3>
<!-- All fields in form --> <!-- All fields in form -->
<table class="fields"> <div class="fields">
<tr ng-repeat="field in form.fields"> <guac-form-field ng-repeat="field in form.fields" namespace="namespace"
<th>{{getFieldHeader(field) | translate}}</th> field="field" model="values[field.name]"></guac-form-field>
<td> </div>
<guac-form-field namespace="namespace" field="field" model="values[field.name]"></guac-form-field>
</td>
</tr>
</table>
</div> </div>

View File

@@ -1,4 +1,4 @@
<div class="form-field"> <label class="labeled-field" ng-class="{empty: !model}">
<!-- <!--
Copyright 2014 Glyptodon LLC. Copyright 2014 Glyptodon LLC.
@@ -21,7 +21,11 @@
THE SOFTWARE. THE SOFTWARE.
--> -->
<!-- Field header -->
<span class="field-header">{{getFieldHeader() | translate}}</span>
<!-- Generic input types --> <!-- Generic input types -->
<div class="form-field">
<input ng-show="field.type === 'TEXT'" type="text" ng-model="typedValue" autocorrect="off" autocapitalize="off"/> <input ng-show="field.type === 'TEXT'" type="text" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
<input ng-show="field.type === 'NUMERIC'" type="number" ng-model="typedValue" autocorrect="off" autocapitalize="off"/> <input ng-show="field.type === 'NUMERIC'" type="number" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
<input ng-show="field.type === 'USERNAME'" type="text" ng-model="typedValue" autocorrect="off" autocapitalize="off"/> <input ng-show="field.type === 'USERNAME'" type="text" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
@@ -38,5 +42,6 @@
<!-- Enumerated field --> <!-- Enumerated field -->
<select ng-show="field.type === 'ENUM'" ng-model="typedValue" ng-options="option.value as getFieldOption(option.value) | translate for option in field.options | orderBy: value"></select> <select ng-show="field.type === 'ENUM'" ng-model="typedValue" ng-options="option.value as getFieldOption(option.value) | translate for option in field.options | orderBy: value"></select>
</div> </div>
</label>

View File

@@ -1,31 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* The config block for setting up the authentication interceptor.
*/
angular.module('index').config(['$httpProvider',
function indexInterceptorConfig($httpProvider) {
$httpProvider.interceptors.push('authenticationInterceptor');
}]);

View File

@@ -39,26 +39,17 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
* *
* @returns {Promise} * @returns {Promise}
* A promise which resolves successfully only after an attempt to * A promise which resolves successfully only after an attempt to
* re-authenticate has been made. * re-authenticate has been made. If the authentication attempt fails,
* the promise will be rejected.
*/ */
var updateCurrentToken = ['$injector', function updateCurrentToken($injector) { var updateCurrentToken = ['$injector', function updateCurrentToken($injector) {
// Required services // Required services
var $location = $injector.get('$location'); var $location = $injector.get('$location');
var $q = $injector.get('$q');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
// Promise for authentication attempt
var authAttempt = $q.defer();
// Re-authenticate including any parameters in URL // Re-authenticate including any parameters in URL
authenticationService.updateCurrentToken($location.search()) return authenticationService.updateCurrentToken($location.search());
['finally'](function authenticationAttemptComplete() {
authAttempt.resolve();
});
// Return promise that will resolve regardless of success/failure
return authAttempt.promise;
}]; }];
@@ -161,15 +152,6 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
resolve : { updateCurrentToken: updateCurrentToken } resolve : { updateCurrentToken: updateCurrentToken }
}) })
// Login screen
.when('/login/', {
title : 'APP.NAME',
bodyClassName : 'login',
templateUrl : 'app/login/templates/login.html',
controller : 'loginController'
// No need to update token here - the login screen ignores all auth
})
// Client view // Client view
.when('/client/:type/:id/:params?', { .when('/client/:type/:id/:params?', {
bodyClassName : 'client', bodyClassName : 'client',

View File

@@ -36,6 +36,14 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
*/ */
$scope.guacNotification = guacNotification; $scope.guacNotification = guacNotification;
/**
* The credentials that the authentication service is currently expecting,
* if any. If the user is logged in, this will be null.
*
* @type Form[]|Form|Field[]|Field
*/
$scope.expectedCredentials = null;
/** /**
* Basic page-level information. * Basic page-level information.
*/ */
@@ -63,6 +71,10 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Broadcast keydown events // Broadcast keydown events
keyboard.onkeydown = function onkeydown(keysym) { keyboard.onkeydown = function onkeydown(keysym) {
// Do not handle key events if not logged in
if ($scope.expectedCredentials)
return true;
// Warn of pending keydown // Warn of pending keydown
var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeydown', keysym, keyboard); var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeydown', keysym, keyboard);
if (guacBeforeKeydownEvent.defaultPrevented) if (guacBeforeKeydownEvent.defaultPrevented)
@@ -77,6 +89,10 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
// Broadcast keyup events // Broadcast keyup events
keyboard.onkeyup = function onkeyup(keysym) { keyboard.onkeyup = function onkeyup(keysym) {
// Do not handle key events if not logged in
if ($scope.expectedCredentials)
return;
// Warn of pending keyup // Warn of pending keyup
var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeyup', keysym, keyboard); var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeyup', keysym, keyboard);
if (guacBeforeKeydownEvent.defaultPrevented) if (guacBeforeKeydownEvent.defaultPrevented)
@@ -92,6 +108,25 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
keyboard.reset(); keyboard.reset();
}; };
// Display login screen if a whole new set of credentials is needed
$scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, expected) {
$scope.page.title = 'APP.NAME';
$scope.page.bodyClassName = '';
$scope.expectedCredentials = expected;
});
// Prompt for remaining credentials if provided credentials were not enough
$scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, expected) {
$scope.page.title = 'APP.NAME';
$scope.page.bodyClassName = '';
$scope.expectedCredentials = expected;
});
// Clear login screen if login was successful
$scope.$on('guacLogin', function loginSuccessful() {
$scope.expectedCredentials = null;
});
// Update title and CSS class upon navigation // Update title and CSS class upon navigation
$scope.$on('$routeChangeSuccess', function(event, current, previous) { $scope.$on('$routeChangeSuccess', function(event, current, previous) {

View File

@@ -1,47 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
angular.module('index').factory('authenticationInterceptor', ['$location', '$q',
function authenticationInterceptor($location, $q) {
return {
// Redirect users to login if authorization fails
responseError: function handleErrorResponse(rejection) {
// Only redirect failed authentication requests
if ((rejection.status === 401 || rejection.status === 403)
&& rejection.config.url === 'api/tokens') {
// Only redirect if not already on login page
if ($location.path() !== '/login/')
$location.path('/login/');
}
return $q.reject(rejection);
}
};
}]);

View File

@@ -1,71 +0,0 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
angular.module('login').controller('loginController', ['$scope', '$injector',
function loginController($scope, $injector) {
// Required services
var $location = $injector.get('$location');
var authenticationService = $injector.get('authenticationService');
var userPageService = $injector.get('userPageService');
/**
* Whether an error occurred during login.
*
* @type Boolean
*/
$scope.loginError = false;
/**
* Whether the password field has focus.
*
* @type Boolean
*/
$scope.passwordFocused = false;
/**
* Submits the currently-specified username and password to the
* authentication service, redirecting to the main view if successful.
*/
$scope.login = function login() {
// Attempt login once existing session is destroyed
authenticationService.login($scope.username, $scope.password)
// Redirect to main view upon success
.then(function loginSuccessful() {
userPageService.getHomePage()
.then(function homePageRetrieved(homePage) {
$location.url(homePage.url);
});
})
// Reset and focus password upon failure
['catch'](function loginFailed() {
$scope.loginError = true;
$scope.passwordFocused = true;
$scope.password = '';
});
};
}]);

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive for displaying an arbitrary login form.
*/
angular.module('login').directive('guacLogin', [function guacLogin() {
// Login directive
var directive = {
restrict : 'E',
replace : true,
templateUrl : 'app/login/templates/login.html'
};
// Login directive scope
directive.scope = {
/**
* The login form or set of fields. This will be displayed to the user
* to capture their credentials.
*
* @type Form[]|Form|Field[]|Field
*/
form : '='
};
// Controller for login directive
directive.controller = ['$scope', '$injector',
function loginController($scope, $injector) {
// Required services
var $route = $injector.get('$route');
var authenticationService = $injector.get('authenticationService');
/**
* Whether an error occurred during login.
*
* @type Boolean
*/
$scope.loginError = false;
/**
* All form values entered by the user.
*/
$scope.values = {};
/**
* Submits the currently-specified username and password to the
* authentication service, redirecting to the main view if successful.
*/
$scope.login = function login() {
// Attempt login once existing session is destroyed
authenticationService.authenticate($scope.values)
// Clear and reload upon success
.then(function loginSuccessful() {
$scope.loginError = false;
$scope.values = {};
$route.reload();
})
// Reset upon failure
['catch'](function loginFailed() {
$scope.loginError = true;
$scope.values.password = '';
});
};
}];
return directive;
}]);

View File

@@ -23,4 +23,8 @@
/** /**
* The module for the login functionality. * The module for the login functionality.
*/ */
angular.module('login', ['element', 'navigation']); angular.module('login', [
'element',
'form',
'navigation'
]);

View File

@@ -27,6 +27,8 @@ div.login-ui {
left: 0; left: 0;
top: 0; top: 0;
display: table; display: table;
background: white;
z-index: 20;
} }
p.login-error { p.login-error {
@@ -52,3 +54,35 @@ p.login-error {
text-align: center; text-align: center;
color: #964040; color: #964040;
} }
.login-fields .form-field .password-field .toggle-password {
display: none;
}
.login-fields .labeled-field {
display: block;
position: relative;
}
.login-fields .labeled-field .field-header {
display: block;
position: absolute;
left: 0;
right: 0;
overflow: hidden;
z-index: -1;
margin: 0.5em;
font-size: 0.9em;
opacity: 0.5;
}
.login-fields .labeled-field.empty input {
background: transparent;
}
.login-fields .labeled-field input:focus {
background: white;
}

View File

@@ -1,3 +1,4 @@
<div class="login-ui" ng-class="{error: loginError}" >
<!-- <!--
Copyright 2014 Glyptodon LLC. Copyright 2014 Glyptodon LLC.
@@ -20,8 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
--> -->
<div class="login-ui" ng-class="{error: loginError}" >
<!-- Login error message --> <!-- Login error message -->
<p class="login-error">{{'LOGIN.ERROR_INVALID_LOGIN' | translate}}</p> <p class="login-error">{{'LOGIN.ERROR_INVALID_LOGIN' | translate}}</p>
@@ -35,10 +34,9 @@ THE SOFTWARE.
<img class="logo" src="images/guac-tricolor.png" alt=""/> <img class="logo" src="images/guac-tricolor.png" alt=""/>
<div class="version">{{'APP.NAME' | translate}}</div> <div class="version">{{'APP.NAME' | translate}}</div>
<!-- Login fields (username + password) --> <!-- Login fields -->
<div class="login-fields"> <div class="login-fields">
<input ng-model="username" placeholder="{{'LOGIN.FIELD_PLACEHOLDER_USERNAME' | translate}}" type="text" name="username" autofocus="autofocus" autocorrect="off" autocapitalize="off" class="username"/> <guac-form namespace="'LOGIN'" content="form" model="values"></guac-form>
<input ng-model="password" placeholder="{{'LOGIN.FIELD_PLACEHOLDER_PASSWORD' | translate}}" type="password" name="password" class="password" guac-focus="passwordFocused"/>
</div> </div>
<!-- Submit button --> <!-- Submit button -->

View File

@@ -26,3 +26,18 @@
.connection-parameters input[type=number] { .connection-parameters input[type=number] {
width: auto; width: auto;
} }
.connection-parameters .form .fields {
display: table;
}
.connection-parameters .form .fields .labeled-field {
display: table-row;
}
.connection-parameters .form .fields .field-header,
.connection-parameters .form .fields .form-field {
display: table-cell;
padding: 0.125em;
vertical-align: top;
}

View File

@@ -48,6 +48,7 @@ angular.module('navigation').directive('guacUserMenu', [function guacUserMenu()
// Get required services // Get required services
var $document = $injector.get('$document'); var $document = $injector.get('$document');
var $location = $injector.get('$location'); var $location = $injector.get('$location');
var $route = $injector.get('$route');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var userPageService = $injector.get('userPageService'); var userPageService = $injector.get('userPageService');
@@ -100,12 +101,15 @@ angular.module('navigation').directive('guacUserMenu', [function guacUserMenu()
}; };
/** /**
* Logs out the current user, redirecting them to back to the login * Logs out the current user, redirecting them to back to the root
* screen after logout completes. * after logout completes.
*/ */
$scope.logout = function logout() { $scope.logout = function logout() {
authenticationService.logout()['finally'](function logoutComplete() { authenticationService.logout()['finally'](function logoutComplete() {
$location.path('/login/'); if ($location.path() !== '/')
$location.url('/');
else
$route.reload();
}); });
}; };

View File

@@ -0,0 +1,122 @@
/*
* 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 defines the Error class.
*/
angular.module('rest').factory('Error', [function defineError() {
/**
* The object returned by REST API calls when an error occurs.
*
* @constructor
* @param {Error|Object} [template={}]
* The object whose properties should be copied within the new
* Error.
*/
var Error = function Error(template) {
// Use empty object by default
template = template || {};
/**
* A human-readable message describing the error that occurred.
*
* @type String
*/
this.message = template.message;
/**
* The type string defining which values this parameter may contain,
* as well as what properties are applicable. Valid types are listed
* within Error.Type.
*
* @type String
* @default Error.Type.INTERNAL_ERROR
*/
this.type = template.type || Error.Type.INTERNAL_ERROR;
/**
* Any parameters which were expected in the original request, or are
* now expected as a result of the original request, if any. If no
* such information is available, this will be null.
*
* @type Field[]
*/
this.expected = template.expected;
};
/**
* All valid field types.
*/
Error.Type = {
/**
* The requested operation could not be performed because the request
* itself was malformed.
*
* @type String
*/
BAD_REQUEST : 'BAD_REQUEST',
/**
* The credentials provided were invalid.
*
* @type String
*/
INVALID_CREDENTIALS : 'INVALID_CREDENTIALS',
/**
* The credentials provided were not necessarily invalid, but were not
* sufficient to determine validity.
*
* @type String
*/
INSUFFICIENT_CREDENTIALS : 'INSUFFICIENT_CREDENTIALS',
/**
* An internal server error has occurred.
*
* @type String
*/
INTERNAL_ERROR : 'INTERNAL_ERROR',
/**
* An object related to the request does not exist.
*
* @type String
*/
NOT_FOUND : 'NOT_FOUND',
/**
* Permission was denied to perform the requested operation.
*
* @type String
*/
PERMISSION_DENIED : 'PERMISSION_DENIED'
};
return Error;
}]);

View File

@@ -34,6 +34,9 @@ THE SOFTWARE.
</head> </head>
<body ng-class="page.bodyClassName"> <body ng-class="page.bodyClassName">
<!-- Content for logged-in users -->
<div ng-if="!expectedCredentials">
<!-- Global status/error dialog --> <!-- Global status/error dialog -->
<div ng-class="{shown: guacNotification.status}" class="status-outer"> <div ng-class="{shown: guacNotification.status}" class="status-outer">
<div class="status-middle"> <div class="status-middle">
@@ -51,6 +54,11 @@ THE SOFTWARE.
</div> </div>
</div> </div>
</div>
<!-- Login screen for logged-out users -->
<guac-login ng-show="expectedCredentials" form="expectedCredentials"></guac-login>
<script type="text/javascript" src="guacamole.min.js"></script> <script type="text/javascript" src="guacamole.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -145,8 +145,8 @@
"ERROR_INVALID_LOGIN" : "Invalid Login", "ERROR_INVALID_LOGIN" : "Invalid Login",
"FIELD_PLACEHOLDER_USERNAME" : "Username", "FIELD_HEADER_USERNAME" : "Username",
"FIELD_PLACEHOLDER_PASSWORD" : "Password" "FIELD_HEADER_PASSWORD" : "Password"
}, },