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();
} }
catch(GuacamoleSecurityException e) {
throw new HTTPException(Response.Status.FORBIDDEN, e.getMessage() != null ? e.getMessage() : "Permission denied."); // 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()
);
} }
catch(GuacamoleClientException e) {
throw new HTTPException(Response.Status.BAD_REQUEST, e.getMessage() != null ? e.getMessage() : "Invalid Request."); // 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()
);
} }
catch(GuacamoleException e) {
logger.error("Unexpected GuacamoleException caught while executing " + invocation.getMethod().getName() + ".", e); // Generic permission denied
throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() != null ? e.getMessage() : "Unexpected server error."); catch (GuacamoleSecurityException e) {
// 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) {
// 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) {
// 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
catch(GuacamoleException e) { if (userContext == null)
logger.error("Exception caught while authenticating user.", e); throw new GuacamoleInvalidCredentialsException("Permission Denied.",
throw new HTTPException(Status.INTERNAL_SERVER_ERROR, CredentialsInfo.USERNAME_PASSWORD);
"Unexpected server error.");
}
// Authentication failed. }
if (userContext == null) { catch (GuacamoleException e) {
// 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,22 +21,27 @@
THE SOFTWARE. THE SOFTWARE.
--> -->
<!-- Generic input types --> <!-- Field header -->
<input ng-show="field.type === 'TEXT'" type="text" ng-model="typedValue" autocorrect="off" autocapitalize="off"/> <span class="field-header">{{getFieldHeader() | translate}}</span>
<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 === 'BOOLEAN'" type="checkbox" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
<!-- Password field --> <!-- Generic input types -->
<div ng-show="field.type === 'PASSWORD'" class="password-field"> <div class="form-field">
<input type="{{passwordInputType}}" ng-model="typedValue" autocorrect="off" autocapitalize="off"/> <input ng-show="field.type === 'TEXT'" type="text" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
<div class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div> <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 === 'BOOLEAN'" type="checkbox" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
<!-- Password field -->
<div ng-show="field.type === 'PASSWORD'" class="password-field">
<input type="{{passwordInputType}}" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
<div class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div>
</div>
<!-- Multiline field -->
<textarea ng-show="field.type === 'MULTILINE'" ng-model="typedValue" autocorrect="off" autocapitalize="off"></textarea>
<!-- 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>
</div> </div>
<!-- Multiline field --> </label>
<textarea ng-show="field.type === 'MULTILINE'" ng-model="typedValue" autocorrect="off" autocapitalize="off"></textarea>
<!-- 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>
</div>

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,26 +1,25 @@
<!--
Copyright 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.
-->
<div class="login-ui" ng-class="{error: loginError}" > <div class="login-ui" ng-class="{error: loginError}" >
<!--
Copyright 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.
-->
<!-- 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,22 +34,30 @@ THE SOFTWARE.
</head> </head>
<body ng-class="page.bodyClassName"> <body ng-class="page.bodyClassName">
<!-- Global status/error dialog --> <!-- Content for logged-in users -->
<div ng-class="{shown: guacNotification.status}" class="status-outer"> <div ng-if="!expectedCredentials">
<div class="status-middle">
<guac-notification notification="guacNotification.status"></guac-notification> <!-- Global status/error dialog -->
<div ng-class="{shown: guacNotification.status}" class="status-outer">
<div class="status-middle">
<guac-notification notification="guacNotification.status"></guac-notification>
</div>
</div> </div>
<div id="content" ng-view>
</div>
<!-- Notification area -->
<div id="notificationArea">
<div ng-repeat="wrapper in guacNotification.notifications">
<guac-notification notification="wrapper.notification"></guac-notification>
</div>
</div>
</div> </div>
<div id="content" ng-view> <!-- Login screen for logged-out users -->
</div> <guac-login ng-show="expectedCredentials" form="expectedCredentials"></guac-login>
<!-- Notification area -->
<div id="notificationArea">
<div ng-repeat="wrapper in guacNotification.notifications">
<guac-notification notification="wrapper.notification"></guac-notification>
</div>
</div>
<script type="text/javascript" src="guacamole.min.js"></script> <script type="text/javascript" src="guacamole.min.js"></script>
</body> </body>

View File

@@ -141,12 +141,12 @@
"LOGIN": { "LOGIN": {
"ACTION_LOGIN" : "@:APP.ACTION_LOGIN", "ACTION_LOGIN" : "@:APP.ACTION_LOGIN",
"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"
}, },