Merge pull request #195 from glyptodon/password-expiration

GUAC-1176: Implement password expiration attribute
This commit is contained in:
James Muehlner
2015-06-08 16:32:15 -07:00
46 changed files with 859 additions and 652 deletions

View File

@@ -37,6 +37,7 @@ import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionPermis
import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionGroupPermissionService;
import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionService;
import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionService;
import org.glyptodon.guacamole.form.BooleanField;
import org.glyptodon.guacamole.form.Field;
import org.glyptodon.guacamole.form.Form;
import org.glyptodon.guacamole.net.auth.User;
@@ -58,12 +59,19 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
*/
public static final String DISABLED_ATTRIBUTE_NAME = "disabled";
/**
* The name of the attribute which controls whether a user's password is
* expired and must be reset upon login.
*/
public static final String EXPIRED_ATTRIBUTE_NAME = "expired";
/**
* All attributes related to restricting user accounts, within a logical
* form.
*/
public static final Form ACCOUNT_RESTRICTIONS = new Form("restrictions", "Account Restrictions", Arrays.asList(
new Field(DISABLED_ATTRIBUTE_NAME, "Disabled", "true")
public static final Form ACCOUNT_RESTRICTIONS = new Form("restrictions", Arrays.<Field>asList(
new BooleanField(DISABLED_ATTRIBUTE_NAME, "true"),
new BooleanField(EXPIRED_ATTRIBUTE_NAME, "true")
));
/**
@@ -214,7 +222,10 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
Map<String, String> attributes = new HashMap<String, String>();
// Set disabled attribute
attributes.put("disabled", getModel().isDisabled() ? "true" : null);
attributes.put(DISABLED_ATTRIBUTE_NAME, getModel().isDisabled() ? "true" : null);
// Set password expired attribute
attributes.put(EXPIRED_ATTRIBUTE_NAME, getModel().isExpired() ? "true" : null);
return attributes;
}
@@ -223,7 +234,10 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
public void setAttributes(Map<String, String> attributes) {
// Translate disabled attribute
getModel().setDisabled("true".equals(attributes.get("disabled")));
getModel().setDisabled("true".equals(attributes.get(DISABLED_ATTRIBUTE_NAME)));
// Translate password expired attribute
getModel().setExpired("true".equals(attributes.get(EXPIRED_ATTRIBUTE_NAME)));
}

View File

@@ -26,6 +26,8 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
/**
* Service which creates new UserContext instances for valid users based on
@@ -49,17 +51,20 @@ public class UserContextService {
/**
* Authenticates the user having the given credentials, returning a new
* UserContext instance if the credentials are valid.
* UserContext instance only if the credentials are valid. If the
* credentials are invalid or expired, an appropriate GuacamoleException
* will be thrown.
*
* @param credentials
* The credentials to use to produce the UserContext.
*
* @return
* A new UserContext instance for the user identified by the given
* credentials, or null if the credentials are not valid.
* credentials.
*
* @throws GuacamoleException
* If an error occurs during authentication.
* If an error occurs during authentication, or if the given
* credentials are invalid or expired.
*/
public org.glyptodon.guacamole.net.auth.UserContext
getUserContext(Credentials credentials)
@@ -67,7 +72,7 @@ public class UserContextService {
// Authenticate user
ModeledUser user = userService.retrieveUser(credentials);
if (user != null && !user.getModel().isDisabled()) {
if (user != null) {
// Upon successful authentication, return new user context
UserContext context = userContextProvider.get();
@@ -77,7 +82,7 @@ public class UserContextService {
}
// Otherwise, unauthorized
return null;
throw new GuacamoleInvalidCredentialsException("Invalid login", CredentialsInfo.USERNAME_PASSWORD);
}

View File

@@ -48,6 +48,13 @@ public class UserModel extends ObjectModel {
*/
private boolean disabled;
/**
* Whether the user's password is expired. If a user's password is expired,
* it must be changed immediately upon login, and the account cannot be
* used until this occurs.
*/
private boolean expired;
/**
* Creates a new, empty user.
*/
@@ -127,4 +134,28 @@ public class UserModel extends ObjectModel {
this.disabled = disabled;
}
/**
* Returns whether the user's password has expired. If a user's password is
* expired, it must be immediately changed upon login. A user account with
* an expired password cannot be used until the password has been changed.
*
* @return
* true if the user's password has expired, false otherwise.
*/
public boolean isExpired() {
return expired;
}
/**
* Sets whether the user's password is expired. If a user's password is
* expired, it must be immediately changed upon login. A user account with
* an expired password cannot be used until the password has been changed.
*
* @param expired
* true to expire the user's password, false otherwise.
*/
public void setExpired(boolean expired) {
this.expired = expired;
}
}

View File

@@ -27,6 +27,7 @@ import com.google.inject.Provider;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper;
import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectService;
@@ -37,11 +38,17 @@ import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionMapper;
import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionModel;
import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionMapper;
import org.glyptodon.guacamole.auth.jdbc.security.PasswordEncryptionService;
import org.glyptodon.guacamole.form.Field;
import org.glyptodon.guacamole.form.PasswordField;
import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.glyptodon.guacamole.net.auth.permission.ObjectPermission;
import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet;
import org.glyptodon.guacamole.net.auth.permission.SystemPermission;
import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service which provides convenience methods for creating, retrieving, and
@@ -51,6 +58,11 @@ import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet;
*/
public class UserService extends ModeledDirectoryObjectService<ModeledUser, User, UserModel> {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* All user permissions which are implicitly granted to the new user upon
* creation.
@@ -59,7 +71,43 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
ObjectPermission.Type.READ,
ObjectPermission.Type.UPDATE
};
/**
* The name of the HTTP password parameter to expect if the user is
* changing their expired password upon login.
*/
private static final String NEW_PASSWORD_PARAMETER = "new-password";
/**
* The password field to provide the user when their password is expired
* and must be changed.
*/
private static final Field NEW_PASSWORD = new PasswordField(NEW_PASSWORD_PARAMETER);
/**
* The name of the HTTP password confirmation parameter to expect if the
* user is changing their expired password upon login.
*/
private static final String CONFIRM_NEW_PASSWORD_PARAMETER = "confirm-new-password";
/**
* The password confirmation field to provide the user when their password
* is expired and must be changed.
*/
private static final Field CONFIRM_NEW_PASSWORD = new PasswordField(CONFIRM_NEW_PASSWORD_PARAMETER);
/**
* Information describing the expected credentials if a user's password is
* expired. If a user's password is expired, it must be changed during the
* login process.
*/
private static final CredentialsInfo EXPIRED_PASSWORD = new CredentialsInfo(Arrays.asList(
CredentialsInfo.USERNAME,
CredentialsInfo.PASSWORD,
NEW_PASSWORD,
CONFIRM_NEW_PASSWORD
));
/**
* Mapper for accessing users.
*/
@@ -213,7 +261,9 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
/**
* Retrieves the user corresponding to the given credentials from the
* database.
* database. If the user account is expired, and the credentials contain
* the necessary additional parameters to reset the user's password, the
* password is reset.
*
* @param credentials
* The credentials to use when locating the user.
@@ -221,8 +271,12 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
* @return
* The existing ModeledUser object if the credentials given are valid,
* null otherwise.
*
* @throws GuacamoleException
* If the provided credentials to not conform to expectations.
*/
public ModeledUser retrieveUser(Credentials credentials) {
public ModeledUser retrieveUser(Credentials credentials)
throws GuacamoleException {
// Get username and password
String username = credentials.getUsername();
@@ -233,19 +287,55 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
if (userModel == null)
return null;
// If password hash matches, return the retrieved user
byte[] hash = encryptionService.createPasswordHash(password, userModel.getPasswordSalt());
if (Arrays.equals(hash, userModel.getPasswordHash())) {
// If user is disabled, pretend user does not exist
if (userModel.isDisabled())
return null;
// Return corresponding user, set up cyclic reference
ModeledUser user = getObjectInstance(null, userModel);
user.setCurrentUser(new AuthenticatedUser(user, credentials));
return user;
// Verify provided password is correct
byte[] hash = encryptionService.createPasswordHash(password, userModel.getPasswordSalt());
if (!Arrays.equals(hash, userModel.getPasswordHash()))
return null;
// Create corresponding user object, set up cyclic reference
ModeledUser user = getObjectInstance(null, userModel);
user.setCurrentUser(new AuthenticatedUser(user, credentials));
// Update password if password is expired
if (userModel.isExpired()) {
// Pull new password from HTTP request
HttpServletRequest request = credentials.getRequest();
String newPassword = request.getParameter(NEW_PASSWORD_PARAMETER);
String confirmNewPassword = request.getParameter(CONFIRM_NEW_PASSWORD_PARAMETER);
// Require new password if account is expired
if (newPassword == null || confirmNewPassword == null) {
logger.info("The password of user \"{}\" has expired and must be reset.", username);
throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_PASSWORD_EXPIRED", EXPIRED_PASSWORD);
}
// New password must be different from old password
if (newPassword.equals(credentials.getPassword()))
throw new GuacamoleClientException("LOGIN.ERROR_PASSWORD_SAME");
// New password must not be blank
if (newPassword.isEmpty())
throw new GuacamoleClientException("LOGIN.ERROR_PASSWORD_BLANK");
// Confirm that the password was entered correctly twice
if (!newPassword.equals(confirmNewPassword))
throw new GuacamoleClientException("LOGIN.ERROR_PASSWORD_MISMATCH");
// Change password and reset expiration flag
userModel.setExpired(false);
user.setPassword(newPassword);
userMapper.update(userModel);
logger.info("Expired password of user \"{}\" has been reset.", username);
}
// Otherwise, the credentials do not match
return null;
// Return now-authenticated user
return user;
}

View File

@@ -1,9 +1,25 @@
{
"LOGIN" : {
"ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK",
"ERROR_PASSWORD_SAME" : "The new password must be different from the expired password.",
"ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
"INFO_PASSWORD_EXPIRED" : "Your password has expired and must be reset. Please enter a new password to continue.",
"FIELD_HEADER_NEW_PASSWORD" : "New password",
"FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Confirm new password"
},
"USER_ATTRIBUTES" : {
"FIELD_HEADER_DISABLED" : "Login disabled:",
"FIELD_HEADER_EXPIRED" : "Password expired:",
"SECTION_HEADER_RESTRICTIONS" : "Account Restrictions"
}
}

View File

@@ -0,0 +1,13 @@
{
"LOGIN" : {
"ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK",
"ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
"FIELD_HEADER_NEW_PASSWORD" : "Mot de passe",
"FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Répéter mot de passe"
}
}

View File

@@ -0,0 +1,13 @@
{
"LOGIN" : {
"ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK",
"ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
"FIELD_HEADER_NEW_PASSWORD" : "Новый пароль",
"FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Подтверждение пароля"
}
}

View File

@@ -77,6 +77,7 @@ CREATE TABLE `guacamole_user` (
`password_hash` binary(32) NOT NULL,
`password_salt` binary(32),
`disabled` boolean NOT NULL DEFAULT 0,
`expired` boolean NOT NULL DEFAULT 0,
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`)

View File

@@ -26,3 +26,9 @@
ALTER TABLE guacamole_user ADD COLUMN disabled BOOLEAN NOT NULL DEFAULT 0;
--
-- Add per-user password expiration flag
--
ALTER TABLE guacamole_user ADD COLUMN expired BOOLEAN NOT NULL DEFAULT 0;

View File

@@ -10,7 +10,9 @@
],
"translations" : [
"translations/en_US.json"
"translations/en.json",
"translations/fr.json",
"translations/ru.json"
]
}

View File

@@ -33,6 +33,7 @@
<result column="password_hash" property="passwordHash" jdbcType="BINARY"/>
<result column="password_salt" property="passwordSalt" jdbcType="BINARY"/>
<result column="disabled" property="disabled" jdbcType="BOOLEAN"/>
<result column="expired" property="expired" jdbcType="BOOLEAN"/>
</resultMap>
<!-- Select all usernames -->
@@ -59,7 +60,8 @@
username,
password_hash,
password_salt,
disabled
disabled,
expired
FROM guacamole_user
WHERE username IN
<foreach collection="identifiers" item="identifier"
@@ -77,7 +79,8 @@
username,
password_hash,
password_salt,
disabled
disabled,
expired
FROM guacamole_user
JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
WHERE username IN
@@ -98,7 +101,8 @@
username,
password_hash,
password_salt,
disabled
disabled,
expired
FROM guacamole_user
WHERE
username = #{username,jdbcType=VARCHAR}
@@ -119,13 +123,15 @@
username,
password_hash,
password_salt,
disabled
disabled,
expired
)
VALUES (
#{object.identifier,jdbcType=VARCHAR},
#{object.passwordHash,jdbcType=BINARY},
#{object.passwordSalt,jdbcType=BINARY},
#{object.disabled,jdbcType=BOOLEAN}
#{object.disabled,jdbcType=BOOLEAN},
#{object.expired,jdbcType=BOOLEAN}
)
</insert>
@@ -135,7 +141,8 @@
UPDATE guacamole_user
SET password_hash = #{object.passwordHash,jdbcType=BINARY},
password_salt = #{object.passwordSalt,jdbcType=BINARY},
disabled = #{object.disabled,jdbcType=BOOLEAN}
disabled = #{object.disabled,jdbcType=BOOLEAN},
expired = #{object.expired,jdbcType=BOOLEAN}
WHERE user_id = #{object.objectID,jdbcType=VARCHAR}
</update>

View File

@@ -118,6 +118,7 @@ CREATE TABLE guacamole_user (
password_hash bytea NOT NULL,
password_salt bytea,
disabled boolean NOT NULL DEFAULT FALSE,
expired boolean NOT NULL DEFAULT FALSE,
PRIMARY KEY (user_id),

View File

@@ -26,3 +26,9 @@
ALTER TABLE guacamole_user ADD COLUMN disabled boolean NOT NULL DEFAULT FALSE;
--
-- Add per-user password expiration flag
--
ALTER TABLE guacamole_user ADD COLUMN expired boolean NOT NULL DEFAULT FALSE;

View File

@@ -10,7 +10,9 @@
],
"translations" : [
"translations/en_US.json"
"translations/en.json",
"translations/fr.json",
"translations/ru.json"
]
}

View File

@@ -33,6 +33,7 @@
<result column="password_hash" property="passwordHash" jdbcType="BINARY"/>
<result column="password_salt" property="passwordSalt" jdbcType="BINARY"/>
<result column="disabled" property="disabled" jdbcType="BOOLEAN"/>
<result column="expired" property="expired" jdbcType="BOOLEAN"/>
</resultMap>
<!-- Select all usernames -->
@@ -59,7 +60,8 @@
username,
password_hash,
password_salt,
disabled
disabled,
expired
FROM guacamole_user
WHERE username IN
<foreach collection="identifiers" item="identifier"
@@ -77,7 +79,8 @@
username,
password_hash,
password_salt,
disabled
disabled,
expired
FROM guacamole_user
JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
WHERE username IN
@@ -98,7 +101,8 @@
username,
password_hash,
password_salt,
disabled
disabled,
expired
FROM guacamole_user
WHERE
username = #{username,jdbcType=VARCHAR}
@@ -119,13 +123,15 @@
username,
password_hash,
password_salt,
disabled
disabled,
expired
)
VALUES (
#{object.identifier,jdbcType=VARCHAR},
#{object.passwordHash,jdbcType=BINARY},
#{object.passwordSalt,jdbcType=BINARY},
#{object.disabled,jdbcType=BOOLEAN}
#{object.disabled,jdbcType=BOOLEAN},
#{object.expired,jdbcType=BOOLEAN}
)
</insert>
@@ -135,7 +141,8 @@
UPDATE guacamole_user
SET password_hash = #{object.passwordHash,jdbcType=BINARY},
password_salt = #{object.passwordSalt,jdbcType=BINARY},
disabled = #{object.disabled,jdbcType=BOOLEAN}
disabled = #{object.disabled,jdbcType=BOOLEAN},
expired = #{object.expired,jdbcType=BOOLEAN}
WHERE user_id = #{object.objectID,jdbcType=VARCHAR}
</update>

View File

@@ -0,0 +1,53 @@
/*
* 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.form;
import java.util.Collections;
/**
* Represents a field with strictly one possible value. It is assumed that the
* field may be blank, but that its sole non-blank value is the value provided.
* The provided value represents "true" while all other values, including
* having no associated value, represent "false".
*
* @author Michael Jumper
*/
public class BooleanField extends Field {
/**
* Creates a new BooleanField with the given name and truth value. The
* truth value is the value that, when assigned to this field, means that
* this field is "true".
*
* @param name
* The unique name to associate with this field.
*
* @param truthValue
* The value to consider "true" for this field. All other values will
* be considered "false".
*/
public BooleanField(String name, String truthValue) {
super(name, Field.Type.BOOLEAN, Collections.singletonList(truthValue));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2014 Glyptodon LLC
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -20,42 +20,29 @@
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.form;
import java.util.Collection;
/**
* Service which defines the FieldOption class.
* Represents an arbitrary field with a finite, enumerated set of possible
* values.
*
* @author Michael Jumper
*/
angular.module('rest').factory('FieldOption', [function defineFieldOption() {
public class EnumField extends Field {
/**
* The object returned by REST API calls when representing a single possible
* legal value of a field.
*
* @constructor
* @param {FieldOption|Object} [template={}]
* The object whose properties should be copied within the new
* FieldOption.
* Creates a new EnumField with the given name and possible values.
*
* @param name
* The unique name to associate with this field.
*
* @param options
* All possible legal options for this field.
*/
var FieldOption = function FieldOption(template) {
public EnumField(String name, Collection<String> options) {
super(name, Field.Type.ENUM, options);
}
// Use empty object by default
template = template || {};
/**
* A human-readable name for this parameter value.
*
* @type String
*/
this.title = template.title;
/**
* The actual value to set the parameter to, if this option is
* selected.
*
* @type String
*/
this.value = template.value;
};
return FieldOption;
}]);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Glyptodon LLC
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -27,7 +27,11 @@ import org.codehaus.jackson.map.annotate.JsonSerialize;
/**
* Represents an arbitrary field, such as an HTTP parameter, the parameter of a
* remote desktop protocol, or an input field within a form.
* remote desktop protocol, or an input field within a form. Fields are generic
* and typed dynamically through a type string, with the semantics of the field
* defined by the type string. The behavior of each field type is defined
* either through the web application itself (see FormService.js) or through
* extensions.
*
* @author Michael Jumper
*/
@@ -35,7 +39,11 @@ import org.codehaus.jackson.map.annotate.JsonSerialize;
public class Field {
/**
* All possible types of field.
* All types of fields which are available by default. Additional field
* types may be defined by extensions by using a unique field type name and
* registering that name with the form service within JavaScript.
*
* See FormService.js.
*/
public static class Type {
@@ -83,67 +91,51 @@ public class Field {
*/
private String name;
/**
* A human-readable name to be presented to the user.
*/
private String title;
/**
* The type of this field.
*/
private String type;
/**
* The value of this field, when checked. This is only applicable to
* BOOLEAN fields.
* A collection of all legal values of this field.
*/
private String value;
private Collection<String> options;
/**
* A collection of all associated field options.
*/
private Collection<FieldOption> options;
/**
* Creates a new Parameter with no associated name, title, or type.
* Creates a new Parameter with no associated name or type.
*/
public Field() {
}
/**
* Creates a new Parameter with the given name, title, and type.
* Creates a new Field with the given name and type.
*
* @param name
* The unique name to associate with this field.
*
* @param title
* The human-readable title to associate with this field.
*
* @param type
* The type of this field.
*/
public Field(String name, String title, String type) {
this.name = name;
this.title = title;
this.type = type;
public Field(String name, String type) {
this.name = name;
this.type = type;
}
/**
* Creates a new ENUM Parameter with the given name, title, and options.
* Creates a new Field with the given name, type, and possible values.
*
* @param name
* The unique name to associate with this field.
*
* @param title
* The human-readable title to associate with this field.
* @param type
* The type of this field.
*
* @param options
* A collection of all possible valid options for this field.
*/
public Field(String name, String title, Collection<FieldOption> options) {
public Field(String name, String type, Collection<String> options) {
this.name = name;
this.title = title;
this.type = Type.ENUM;
this.type = type;
this.options = options;
}
@@ -167,49 +159,6 @@ public class Field {
this.name = name;
}
/**
* Returns the human-readable title associated with this field.
*
* @return
* The human-readable title associated with this field.
*/
public String getTitle() {
return title;
}
/**
* Sets the title associated with this field. The title must be a human-
* readable string which describes accurately this field.
*
* @param title
* A human-readable string describing this field.
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Returns the value that should be assigned to this field if enabled. This
* is only applicable to BOOLEAN fields.
*
* @return
* The value that should be assigned to this field if enabled.
*/
public String getValue() {
return value;
}
/**
* Sets the value that should be assigned to this field if enabled. This is
* only applicable to BOOLEAN fields.
*
* @param value
* The value that should be assigned to this field if enabled.
*/
public void setValue(String value) {
this.value = value;
}
/**
* Returns the type of this field.
*
@@ -232,25 +181,23 @@ public class Field {
/**
* Returns a mutable collection of field options. Changes to this
* collection directly affect the available options. This is only
* applicable to ENUM fields.
* collection directly affect the available options.
*
* @return
* A mutable collection of field options, or null if the field has no
* options.
*/
public Collection<FieldOption> getOptions() {
public Collection<String> getOptions() {
return options;
}
/**
* Sets the options available as possible values of this field. This is
* only applicable to ENUM fields.
* Sets the options available as possible values of this field.
*
* @param options
* The options to associate with this field.
*/
public void setOptions(Collection<FieldOption> options) {
public void setOptions(Collection<String> options) {
this.options = options;
}

View File

@@ -40,41 +40,32 @@ public class Form {
*/
private String name;
/**
* The a human-readable title describing this form.
*/
private String title;
/**
* All fields associated with this form.
*/
private Collection<Field> fields;
/**
* Creates a new Form object with no associated fields. The name and title
* of the form are left unset as null. If no form name is provided, this
* form must not be used in the same context as another unnamed form.
* Creates a new Form object with no associated fields. The name is left
* unset as null. If no form name is provided, this form must not be used
* in the same context as another unnamed form.
*/
public Form() {
fields = new ArrayList<Field>();
}
/**
* Creates a new Form object having the given name and title, and
* containing the given fields.
* Creates a new Form object having the given name and containing the given
* fields.
*
* @param name
* A name which uniquely identifies this form.
*
* @param title
* A human-readable title describing this form.
*
* @param fields
* The fields to provided within the new Form.
*/
public Form(String name, String title, Collection<Field> fields) {
public Form(String name, Collection<Field> fields) {
this.name = name;
this.title = title;
this.fields = fields;
}
@@ -120,26 +111,4 @@ public class Form {
this.name = name;
}
/**
* Returns the human-readable title associated with this form. A form's
* title describes the form, but need not be unique.
*
* @return
* A human-readable title describing this form.
*/
public String getTitle() {
return title;
}
/**
* Sets the human-readable title associated with this form. A form's title
* describes the form, but need not be unique.
*
* @param title
* A human-readable title describing this form.
*/
public void setTitle(String title) {
this.title = title;
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.form;
/**
* Represents a field which can contain multiple lines of text.
*
* @author Michael Jumper
*/
public class MultilineField extends Field {
/**
* Creates a new MultilineField with the given name.
*
* @param name
* The unique name to associate with this field.
*/
public MultilineField(String name) {
super(name, Field.Type.MULTILINE);
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.form;
/**
* Represents a field which may contain only integer values.
*
* @author Michael Jumper
*/
public class NumericField extends Field {
/**
* Creates a new NumericField with the given name.
*
* @param name
* The unique name to associate with this field.
*/
public NumericField(String name) {
super(name, Field.Type.NUMERIC);
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.form;
/**
* Represents a field which contains sensitive text information related to
* authenticating a user.
*
* @author Michael Jumper
*/
public class PasswordField extends Field {
/**
* Creates a new PasswordField with the given name.
*
* @param name
* The unique name to associate with this field.
*/
public PasswordField(String name) {
super(name, Field.Type.PASSWORD);
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.form;
/**
* Represents a basic text field. The field may generally contain any data, but
* may not contain multiple lines.
*
* @author Michael Jumper
*/
public class TextField extends Field {
/**
* Creates a new TextField with the given name.
*
* @param name
* The unique name to associate with this field.
*/
public TextField(String name) {
super(name, Field.Type.TEXT);
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.form;
/**
* Represents a text field which will contain the uniquely-identifying name of
* a user.
*
* @author Michael Jumper
*/
public class UsernameField extends Field {
/**
* Creates a new UsernameField with the given name.
*
* @param name
* The unique name to associate with this field.
*/
public UsernameField(String name) {
super(name, Field.Type.USERNAME);
}
}

View File

@@ -26,6 +26,8 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.glyptodon.guacamole.form.Field;
import org.glyptodon.guacamole.form.PasswordField;
import org.glyptodon.guacamole.form.UsernameField;
/**
* Information which describes a set of valid credentials.
@@ -67,13 +69,25 @@ public class CredentialsInfo {
*/
public static final CredentialsInfo EMPTY = new CredentialsInfo(Collections.<Field>emptyList());
/**
* A field describing the username HTTP parameter expected by Guacamole
* during login, if usernames are being used.
*/
public static final Field USERNAME = new UsernameField("username");
/**
* A field describing the password HTTP parameter expected by Guacamole
* during login, if passwords are being used.
*/
public static final Field PASSWORD = new PasswordField("password");
/**
* CredentialsInfo object which describes standard username/password
* credentials.
*/
public static final CredentialsInfo USERNAME_PASSWORD = new CredentialsInfo(Arrays.asList(
new Field("username", "username", Field.Type.USERNAME),
new Field("password", "password", Field.Type.PASSWORD)
USERNAME,
PASSWORD
));
}

View File

@@ -35,11 +35,6 @@ import org.glyptodon.guacamole.form.Form;
*/
public class ProtocolInfo {
/**
* The human-readable title associated with this protocol.
*/
private String title;
/**
* The unique name associated with this protocol.
*/
@@ -51,65 +46,37 @@ public class ProtocolInfo {
private Collection<Form> forms;
/**
* Creates a new ProtocolInfo with no associated name, title, or
* forms.
* Creates a new ProtocolInfo with no associated name or forms.
*/
public ProtocolInfo() {
this.forms = new ArrayList<Form>();
}
/**
* Creates a new ProtocolInfo having the given name and title, but without
* any forms.
* Creates a new ProtocolInfo having the given name, but without any forms.
*
* @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) {
public ProtocolInfo(String name) {
this.name = name;
this.title = title;
this.forms = new ArrayList<Form>();
}
/**
* Creates a new ProtocolInfo having the given name, title, and forms.
* Creates a new ProtocolInfo having the given name and forms.
*
* @param name
* The unique name associated with the protocol.
*
* @param title
* The human-readable title to associate with the protocol.
*
* @param forms
* The forms to associate with the protocol.
*/
public ProtocolInfo(String name, String title, Collection<Form> forms) {
public ProtocolInfo(String name, Collection<Form> forms) {
this.name = name;
this.title = title;
this.forms = forms;
}
/**
* Returns the human-readable title associated with this protocol.
*
* @return The human-readable title associated with this protocol.
*/
public String getTitle() {
return title;
}
/**
* Sets the human-readable title associated with this protocol.
*
* @param title The human-readable title to associate with this protocol.
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Returns the unique name of this protocol. The protocol name is the
* value required by the corresponding protocol plugin for guacd.

View File

@@ -1,251 +1,156 @@
{
"title" : "RDP",
"name" : "rdp",
"forms" : [
{
"title" : "Network",
"name" : "network",
"fields" : [
{
"name" : "hostname",
"title" : "Hostname",
"type" : "TEXT"
},
{
"name" : "port",
"title" : "Port",
"type" : "NUMERIC"
}
]
},
{
"title" : "Authentication",
"name" : "authentication",
"fields" : [
{
"name" : "username",
"title" : "Username",
"type" : "USERNAME"
},
{
"name" : "password",
"title" : "Password",
"type" : "PASSWORD"
},
{
"name" : "domain",
"title" : "Domain",
"type" : "TEXT"
},
{
"name" : "security",
"title" : "Security mode",
"type" : "ENUM",
"options" : [
{
"value" : "",
"title" : ""
},
{
"value" : "rdp",
"title" : "RDP encryption"
},
{
"value" : "tls",
"title" : "TLS encryption"
},
{
"value" : "nla",
"title" : "NLA (Network Level Authentication)"
},
{
"value" : "any",
"title" : "Any"
}
]
"options" : [ "", "rdp", "tls", "nla", "any" ]
},
{
"name" : "disable-auth",
"title" : "Disable authentication",
"type" : "BOOLEAN",
"value" : "true"
"name" : "disable-auth",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "ignore-cert",
"title" : "Ignore server certificate",
"type" : "BOOLEAN",
"value" : "true"
"name" : "ignore-cert",
"type" : "BOOLEAN",
"options" : [ "true" ]
}
]
},
{
"title" : "Basic Parameters",
"name" : "basic-parameters",
"fields" : [
{
"name" : "initial-program",
"title" : "Initial program",
"type" : "TEXT"
},
{
"name" : "client-name",
"title" : "Client name",
"type" : "TEXT"
},
{
"name" : "server-layout",
"title" : "Keyboard layout",
"type" : "ENUM",
"options" : [
{
"value" : "",
"title" : ""
},
{
"value" : "en-us-qwerty",
"title" : "US English (Qwerty)"
},
{
"value" : "fr-fr-azerty",
"title" : "French (Azerty)"
},
{
"value" : "de-de-qwertz",
"title" : "German (Qwertz)"
},
{
"value" : "it-it-qwerty",
"title" : "Italian (Qwerty)"
},
{
"value" : "sv-se-qwerty",
"title" : "Swedish (Qwerty)"
},
{
"value" : "failsafe",
"title" : "Unicode"
}
"",
"en-us-qwerty",
"fr-fr-azerty",
"de-de-qwertz",
"it-it-qwerty",
"sv-se-qwerty",
"failsafe"
]
},
{
"name" : "console",
"title" : "Administrator console",
"type" : "BOOLEAN",
"value" : "true"
"name" : "console",
"type" : "BOOLEAN",
"options" : [ "true" ]
}
]
},
{
"title" : "Display",
"name" : "display",
"fields" : [
{
"name" : "width",
"title" : "Display width",
"type" : "NUMERIC"
},
{
"name" : "height",
"title" : "Display height",
"type" : "NUMERIC"
},
{
"name" : "dpi",
"title" : "Display resolution (DPI)",
"type" : "NUMERIC"
},
{
"name" : "color-depth",
"title" : "Color depth",
"type" : "ENUM",
"options" : [
{
"value" : "",
"title" : ""
},
{
"value" : "8",
"title" : "256 color"
},
{
"value" : "16",
"title" : "Low color (16-bit)"
},
{
"value" : "24",
"title" : "True color (24-bit)"
},
{
"value" : "32",
"title" : "True color (32-bit)"
}
]
"options" : [ "", "8", "16", "24", "32" ]
}
]
},
{
"title" : "Device Redirection",
"name" : "device-redirection",
"fields" : [
{
"name" : "console-audio",
"title" : "Support audio in console",
"type" : "BOOLEAN",
"value" : "true"
"name" : "console-audio",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "disable-audio",
"title" : "Disable audio",
"type" : "BOOLEAN",
"value" : "true"
"name" : "disable-audio",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "enable-printing",
"title" : "Enable printing",
"type" : "BOOLEAN",
"value" : "true"
"name" : "enable-printing",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "enable-drive",
"title" : "Enable drive",
"type" : "BOOLEAN",
"value" : "true"
"name" : "enable-drive",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "drive-path",
"title" : "Drive path",
"type" : "TEXT"
},
{
"name" : "static-channels",
"title" : "Static channel names",
"type" : "TEXT"
}
]
},
{
"title" : "RemoteApp",
"name" : "remoteapp",
"fields" : [
{
"name" : "remote-app",
"title" : "RemoteApp program",
"type" : "TEXT"
},
{
"name" : "remote-app-dir",
"title" : "RemoteApp working directory",
"type" : "TEXT"
},
{
"name" : "remote-app-args",
"title" : "RemoteApp parameters",
"type" : "TEXT"
}
]

View File

@@ -1,140 +1,65 @@
{
"title" : "SSH",
"name" : "ssh",
"forms" : [
{
"title" : "Network",
"name" : "network",
"fields" : [
{
"name" : "hostname",
"title" : "Hostname",
"type" : "TEXT"
},
{
"name" : "port",
"title" : "Port",
"type" : "NUMERIC"
}
]
},
{
"title" : "Authentication",
"name" : "authentication",
"fields" : [
{
"name" : "username",
"title" : "Username",
"type" : "USERNAME"
},
{
"name" : "password",
"title" : "Password",
"type" : "PASSWORD"
},
{
"name" : "private-key",
"title" : "Private key",
"type" : "MULTILINE"
},
{
"name" : "passphrase",
"title" : "Passphrase",
"type" : "PASSWORD"
}
]
},
{
"title" : "Display",
"name" : "display",
"fields" : [
{
"name" : "font-name",
"title" : "Font name",
"type" : "TEXT"
},
{
"name" : "font-size",
"title" : "Font size",
"type" : "ENUM",
"options" : [
{
"value" : "",
"title" : ""
},
{
"value" : "8",
"title" : "8"
},
{
"value" : "9",
"title" : "9"
},
{
"value" : "10",
"title" : "10"
},
{
"value" : "11",
"title" : "11"
},
{
"value" : "12",
"title" : "12"
},
{
"value" : "14",
"title" : "14"
},
{
"value" : "18",
"title" : "18"
},
{
"value" : "24",
"title" : "24"
},
{
"value" : "30",
"title" : "30"
},
{
"value" : "36",
"title" : "36"
},
{
"value" : "48",
"title" : "48"
},
{
"value" : "60",
"title" : "60"
},
{
"value" : "72",
"title" : "72"
},
{
"value" : "96",
"title" : "96"
}
]
"options" : [ "", "8", "9", "10", "11", "12", "14", "18", "24", "30", "36", "48", "60", "72", "96" ]
}
]
},
{
"title" : "SFTP",
"name" : "sftp",
"fields" : [
{
"name" : "enable-sftp",
"title" : "Enable SFTP",
"type" : "BOOLEAN",
"value" : "true"
"name" : "enable-sftp",
"type" : "BOOLEAN",
"options" : [ "true" ]
}
]
}

View File

@@ -1,122 +1,50 @@
{
"title" : "Telnet",
"name" : "telnet",
"forms" : [
{
"title" : "Network",
"name" : "network",
"fields" : [
{
"name" : "hostname",
"title" : "Hostname",
"type" : "TEXT"
},
{
"name" : "port",
"title" : "Port",
"type" : "NUMERIC"
}
]
},
{
"title" : "Authentication",
"name" : "authentication",
"fields" : [
{
"name" : "username",
"title" : "Username",
"type" : "USERNAME"
},
{
"name" : "password",
"title" : "Password",
"type" : "PASSWORD"
},
{
"name" : "password-regex",
"title" : "Password regular expression",
"type" : "TEXT"
}
]
},
{
"title" : "Display",
"name" : "display",
"fields" : [
{
"name" : "font-name",
"title" : "Font name",
"type" : "TEXT"
},
{
"name" : "font-size",
"title" : "Font size",
"type" : "ENUM",
"options" : [
{
"value" : "",
"title" : ""
},
{
"value" : "8",
"title" : "8"
},
{
"value" : "9",
"title" : "9"
},
{
"value" : "10",
"title" : "10"
},
{
"value" : "11",
"title" : "11"
},
{
"value" : "12",
"title" : "12"
},
{
"value" : "14",
"title" : "14"
},
{
"value" : "18",
"title" : "18"
},
{
"value" : "24",
"title" : "24"
},
{
"value" : "30",
"title" : "30"
},
{
"value" : "36",
"title" : "36"
},
{
"value" : "48",
"title" : "48"
},
{
"value" : "60",
"title" : "60"
},
{
"value" : "72",
"title" : "72"
},
{
"value" : "96",
"title" : "96"
}
]
"options" : [ "", "8", "9", "10", "11", "12", "14", "18", "24", "30", "36", "48", "60", "72", "96" ]
}
]
}

View File

@@ -1,132 +1,81 @@
{
"title" : "VNC",
"name" : "vnc",
"forms" : [
{
"title" : "Network",
"name" : "network",
"fields" : [
{
"name" : "hostname",
"title" : "Hostname",
"type" : "TEXT"
},
{
"name" : "port",
"title" : "Port",
"type" : "NUMERIC"
}
]
},
{
"title" : "Authentication",
"name" : "authentication",
"fields" : [
{
"name" : "password",
"title" : "Password",
"type" : "PASSWORD"
}
]
},
{
"title" : "Display",
"name" : "display",
"fields" : [
{
"name" : "read-only",
"title" : "Read-only",
"type" : "BOOLEAN",
"value" : "true"
"name" : "read-only",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "swap-red-blue",
"title" : "Swap red/blue components",
"type" : "BOOLEAN",
"value" : "true"
"name" : "swap-red-blue",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "cursor",
"title" : "Cursor",
"type" : "ENUM",
"options" : [
{
"value" : "",
"title" : ""
},
{
"value" : "local",
"title" : "Local"
},
{
"value" : "remote",
"title" : "Remote"
}
]
"options" : [ "", "local", "remote" ]
},
{
"name" : "color-depth",
"title" : "Color depth",
"type" : "ENUM",
"options" : [
{
"value" : "",
"title" : ""
},
{
"value" : "8",
"title" : "256 color"
},
{
"value" : "16",
"title" : "Low color (16-bit)"
},
{
"value" : "24",
"title" : "True color (24-bit)"
},
{
"value" : "32",
"title" : "True color (32-bit)"
}
]
"options" : [ "", "8", "16", "24", "32" ]
}
]
},
{
"title" : "Repeater",
"name" : "repeater",
"fields" : [
{
"name" : "dest-host",
"title" : "Repeater destination host",
"type" : "TEXT"
},
{
"name" : "dest-port",
"title" : "Repeater destination port",
"type" : "NUMERIC"
}
]
},
{
"title" : "Audio",
"name" : "audio",
"fields" : [
{
"name" : "enable-audio",
"title" : "Enable audio",
"type" : "BOOLEAN",
"value" : "true"
"name" : "enable-audio",
"type" : "BOOLEAN",
"options" : [ "true" ]
},
{
"name" : "audio-servername",
"title" : "Audio server name",
"type" : "TEXT"
}
]

View File

@@ -36,7 +36,8 @@
* 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
* the set of parameters originally given to authenticate() and the error that
* rejected the credentials. The Error object provided will contain set of
* expected credentials returned by the REST endpoint. This set of credentials
* will be in the form of a Field array.
*/
@@ -154,13 +155,13 @@ angular.module('auth').factory('authenticationService', ['$injector',
// Request credentials if provided credentials were invalid
if (error.type === Error.Type.INVALID_CREDENTIALS)
$rootScope.$broadcast('guacInvalidCredentials', parameters, error.expected);
$rootScope.$broadcast('guacInvalidCredentials', parameters, error);
// Request more credentials if provided credentials were not enough
else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS)
$rootScope.$broadcast('guacInsufficientCredentials', parameters, error.expected);
$rootScope.$broadcast('guacInsufficientCredentials', parameters, error);
authenticationProcess.reject();
authenticationProcess.reject(error);
});
return authenticationProcess.promise;

View File

@@ -29,12 +29,12 @@ angular.module('form').controller('checkboxFieldController', ['$scope',
// Update typed value when model is changed
$scope.$watch('model', function modelChanged(model) {
$scope.typedValue = (model === $scope.field.value);
$scope.typedValue = (model === $scope.field.options[0]);
});
// Update string value in model when typed value is changed
$scope.$watch('typedValue', function typedValueChanged(typedValue) {
$scope.model = (typedValue ? $scope.field.value : '');
$scope.model = (typedValue ? $scope.field.options[0] : '');
});
}]);

View File

@@ -30,6 +30,12 @@ angular.module('form').controller('selectFieldController', ['$scope', '$injector
// Required services
var translationStringService = $injector.get('translationStringService');
// Interpret undefined/null as empty string
$scope.$watch('model', function setModel(model) {
if (!model && model !== '')
$scope.model = '';
});
/**
* Produces the translation string for the given field option
* value. The translation string will be of the form:

View File

@@ -1,4 +1,4 @@
<div class="password-field">
<input type="{{passwordInputType}}" ng-model="model" autocorrect="off" autocapitalize="off"/>
<input type="{{passwordInputType}}" ng-model="model" ng-trim="false" autocorrect="off" autocapitalize="off"/>
<div class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div>
</div>

View File

@@ -1 +1 @@
<select ng-model="model" ng-options="option.value as getFieldOption(option.value) | translate for option in field.options | orderBy: value"></select>
<select ng-model="model" ng-options="option as getFieldOption(option) | translate for option in field.options | orderBy: value"></select>

View File

@@ -36,11 +36,30 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
*/
$scope.guacNotification = guacNotification;
/**
* The message to display to the user as instructions for the login
* process.
*
* @type String
*/
$scope.loginHelpText = null;
/**
* The credentials that the authentication service is has already accepted,
* pending additional credentials, if any. If the user is logged in, or no
* credentials have been accepted, this will be null. If credentials have
* been accepted, this will be a map of name/value pairs corresponding to
* the parameters submitted in a previous authentication attempt.
*
* @type Object.<String, String>
*/
$scope.acceptedCredentials = null;
/**
* 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
* @type Field[]
*/
$scope.expectedCredentials = null;
@@ -109,21 +128,27 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
};
// Display login screen if a whole new set of credentials is needed
$scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, expected) {
$scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) {
$scope.page.title = 'APP.NAME';
$scope.page.bodyClassName = '';
$scope.expectedCredentials = expected;
$scope.loginHelpText = null;
$scope.acceptedCredentials = {};
$scope.expectedCredentials = error.expected;
});
// Prompt for remaining credentials if provided credentials were not enough
$scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, expected) {
$scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) {
$scope.page.title = 'APP.NAME';
$scope.page.bodyClassName = '';
$scope.expectedCredentials = expected;
$scope.loginHelpText = error.message;
$scope.acceptedCredentials = parameters;
$scope.expectedCredentials = error.expected;
});
// Clear login screen if login was successful
$scope.$on('guacLogin', function loginSuccessful() {
$scope.loginHelpText = null;
$scope.acceptedCredentials = null;
$scope.expectedCredentials = null;
});

View File

@@ -35,13 +35,28 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
// Login directive scope
directive.scope = {
/**
* An optional instructional message to display within the login
* dialog.
*
* @type String
*/
helpText : '=',
/**
* The login form or set of fields. This will be displayed to the user
* to capture their credentials.
*
* @type Form[]|Form|Field[]|Field
* @type Field[]
*/
form : '='
form : '=',
/**
* A map of all field name/value pairs that have already been provided.
* If not null, the user will be prompted to continue their login
* attempt using only the fields which remain.
*/
values : '='
};
@@ -49,21 +64,78 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
directive.controller = ['$scope', '$injector',
function loginController($scope, $injector) {
// Required types
var Error = $injector.get('Error');
var Field = $injector.get('Field');
// Required services
var $route = $injector.get('$route');
var authenticationService = $injector.get('authenticationService');
/**
* Whether an error occurred during login.
*
* @type Boolean
* A description of the error that occurred during login, if any.
*
* @type String
*/
$scope.loginError = false;
/**
* All form values entered by the user.
* All form values entered by the user, as parameter name/value pairs.
*
* @type Object.<String, String>
*/
$scope.values = {};
$scope.enteredValues = {};
/**
* All form fields which have not yet been filled by the user.
*
* @type Field[]
*/
$scope.remainingFields = [];
/**
* Returns whether a previous login attempt is continuing.
*
* @return {Boolean}
* true if a previous login attempt is continuing, false otherwise.
*/
$scope.isContinuation = function isContinuation() {
// The login is continuing if any parameter values are provided
for (var name in $scope.values)
return true;
return false;
};
// Ensure provided values are included within entered values, even if
// they have no corresponding input fields
$scope.$watch('values', function resetEnteredValues(values) {
angular.extend($scope.enteredValues, values || {});
});
// Update field information when form is changed
$scope.$watch('form', function resetRemainingFields(fields) {
// If no fields are provided, then no fields remain
if (!fields) {
$scope.remainingFields = [];
return;
}
// Filter provided fields against provided values
$scope.remainingFields = fields.filter(function isRemaining(field) {
return !(field.name in $scope.values);
});
// Set default values for all unset fields
angular.forEach($scope.remainingFields, function setDefault(field) {
if (!$scope.enteredValues[field.name])
$scope.enteredValues[field.name] = '';
});
});
/**
* Submits the currently-specified username and password to the
@@ -71,20 +143,42 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
*/
$scope.login = function login() {
// Start with cleared status
$scope.loginError = null;
// Attempt login once existing session is destroyed
authenticationService.authenticate($scope.values)
authenticationService.authenticate($scope.enteredValues)
// Clear and reload upon success
.then(function loginSuccessful() {
$scope.loginError = false;
$scope.values = {};
$scope.enteredValues = {};
$route.reload();
})
// Reset upon failure
['catch'](function loginFailed() {
$scope.loginError = true;
$scope.values.password = '';
['catch'](function loginFailed(error) {
// Clear out passwords if the credentials were rejected for any reason
if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) {
// Flag generic error for invalid login
if (error.type === Error.Type.INVALID_CREDENTIALS)
$scope.loginError = 'LOGIN.ERROR_INVALID_LOGIN';
// Display error if anything else goes wrong
else
$scope.loginError = error.message;
// Clear all visible password fields
angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) {
// Remove entered value only if field is a password field
if (field.type === Field.Type.PASSWORD && field.name in $scope.enteredValues)
$scope.enteredValues[field.name] = '';
});
}
});
};

View File

@@ -86,3 +86,15 @@
max-width: 3em;
margin: 0.5em auto;
}
.login-ui.continuation div.login-dialog {
border-right: none;
border-left: none;
box-shadow: none;
max-width: 6in;
}
.login-ui.continuation .login-dialog .logo,
.login-ui.continuation .login-dialog .version {
display: none;
}

View File

@@ -38,4 +38,13 @@
.login-ui .login-dialog .buttons input[type="submit"] {
width: 100%;
margin: 0;
}
}
.login-ui.continuation .login-dialog .buttons input[type="submit"] {
width: auto;
}
.login-ui.initial .login-dialog input.continue-login,
.login-ui.continuation .login-dialog input.login {
display: none;
}

View File

@@ -1,4 +1,4 @@
<div class="login-ui" ng-class="{error: loginError}" >
<div class="login-ui" ng-class="{error: loginError, continuation: isContinuation(), initial: !isContinuation()}" >
<!--
Copyright 2014 Glyptodon LLC.
@@ -22,7 +22,7 @@
-->
<!-- Login error message -->
<p class="login-error">{{'LOGIN.ERROR_INVALID_LOGIN' | translate}}</p>
<p class="login-error">{{loginError | translate}}</p>
<div class="login-dialog-middle">
@@ -34,14 +34,18 @@
<img class="logo" src="images/guac-tricolor.png" alt=""/>
<div class="version">{{'APP.NAME' | translate}}</div>
<!-- Login message/instructions -->
<p ng-show="helpText">{{helpText | translate}}</p>
<!-- Login fields -->
<div class="login-fields">
<guac-form namespace="'LOGIN'" content="form" model="values"></guac-form>
<guac-form namespace="'LOGIN'" content="remainingFields" model="enteredValues"></guac-form>
</div>
<!-- Submit button -->
<div class="buttons">
<input type="submit" name="login" class="login" value="{{'LOGIN.ACTION_LOGIN' | translate}}"/>
<input type="submit" name="login" class="continue-login" value="{{'LOGIN.ACTION_CONTINUE' | translate}}"/>
</div>
</form>
@@ -49,4 +53,5 @@
</div>
</div>
</div>

View File

@@ -46,13 +46,6 @@ angular.module('rest').factory('Field', [function defineField() {
*/
this.name = template.name;
/**
* A human-readable name for this parameter.
*
* @type String
*/
this.title = template.title;
/**
* The type string defining which values this parameter may contain,
* as well as what properties are applicable. Valid types are listed
@@ -64,18 +57,9 @@ angular.module('rest').factory('Field', [function defineField() {
this.type = template.type || Field.Type.TEXT;
/**
* The value to set the parameter to, in the case of a BOOLEAN
* parameter, to enable that parameter's effect.
* All possible legal values for this parameter.
*
* @type String
*/
this.value = template.value;
/**
* All possible legal values for this parameter. This property is only
* applicable to ENUM type parameters.
*
* @type FieldOption[]
* @type String[]
*/
this.options = template.options;
@@ -123,7 +107,9 @@ angular.module('rest').factory('Field', [function defineField() {
/**
* The type string associated with parameters that may contain only a
* single possible value, where that value enables the parameter's
* effect.
* effect. It is assumed that each BOOLEAN field will provide exactly
* one possible value (option), which will be the value if that field
* is true.
*
* @type String
*/

View File

@@ -40,21 +40,13 @@ angular.module('rest').factory('Form', [function defineForm() {
template = template || {};
/**
* The name which uniquely identifies this parameter, or null if this
* field has no name.
* The name which uniquely identifies this form, or null if this form
* has no name.
*
* @type String
*/
this.name = template.name;
/**
* A human-readable name for this form, or null if this form has no
* name.
*
* @type String
*/
this.title = template.title;
/**
* All fields contained within this form.
*

View File

@@ -46,13 +46,6 @@ angular.module('rest').factory('Protocol', [function defineProtocol() {
*/
this.name = template.name;
/**
* A human-readable name for this protocol.
*
* @type String
*/
this.title = template.title;
/**
* An array of forms containing all known parameters for this protocol,
* their types, and other information.

View File

@@ -50,7 +50,10 @@
</div>
<!-- Login screen for logged-out users -->
<guac-login ng-show="expectedCredentials" form="expectedCredentials"></guac-login>
<guac-login ng-show="expectedCredentials"
help-text="loginHelpText"
form="expectedCredentials"
values="acceptedCredentials"></guac-login>
<script type="text/javascript" src="app.js"></script>
</body>

View File

@@ -7,6 +7,7 @@
"ACTION_ACKNOWLEDGE" : "OK",
"ACTION_CANCEL" : "Cancel",
"ACTION_CLONE" : "Clone",
"ACTION_CONTINUE" : "Continue",
"ACTION_DELETE" : "Delete",
"ACTION_DELETE_SESSIONS" : "Kill Sessions",
"ACTION_LOGIN" : "Login",
@@ -23,6 +24,7 @@
"DIALOG_HEADER_ERROR" : "Error",
"ERROR_PASSWORD_BLANK" : "Your password cannot be blank.",
"ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.",
"FIELD_HEADER_PASSWORD" : "Password:",
@@ -143,7 +145,11 @@
"LOGIN": {
"ACTION_LOGIN" : "@:APP.ACTION_LOGIN",
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE",
"ACTION_LOGIN" : "@:APP.ACTION_LOGIN",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"ERROR_INVALID_LOGIN" : "Invalid Login",
@@ -161,7 +167,7 @@
"ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection",
"DIALOG_HEADER_ERROR" : "Error",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_HEADER_LOCATION" : "Location:",
"FIELD_HEADER_NAME" : "Name:",
@@ -194,7 +200,7 @@
"ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection Group",
"DIALOG_HEADER_ERROR" : "Error",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_HEADER_LOCATION" : "Location:",
"FIELD_HEADER_NAME" : "Name:",
@@ -216,8 +222,8 @@
"ACTION_DELETE" : "@:APP.ACTION_DELETE",
"ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Delete User",
"DIALOG_HEADER_ERROR" : "Error",
"DIALOG_HEADER_CONFIRM_DELETE" : "Delete User",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
@@ -414,7 +420,7 @@
"ACTION_NEW_CONNECTION" : "New Connection",
"ACTION_NEW_CONNECTION_GROUP" : "New Group",
"DIALOG_HEADER_ERROR" : "Error",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"HELP_CONNECTIONS" : "Click or tap on a connection below to manage that connection. Depending on your access level, connections can be added and deleted, and their properties (protocol, hostname, port, etc.) can be changed.",
@@ -432,7 +438,7 @@
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"ERROR_PASSWORD_BLANK" : "Your password cannot be blank.",
"ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK",
"ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
"FIELD_HEADER_LANGUAGE" : "Display language:",
@@ -469,7 +475,7 @@
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_NEW_USER" : "New User",
"DIALOG_HEADER_ERROR" : "Error",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.",
@@ -484,7 +490,7 @@
"ACTION_DELETE" : "Kill Sessions",
"DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions",
"DIALOG_HEADER_ERROR" : "Error",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_PLACEHOLDER_FILTER" : "Filter",

View File

@@ -23,6 +23,7 @@
"DIALOG_HEADER_ERROR" : "Ошибка",
"ERROR_PASSWORD_BLANK" : "Пароль не может быть пустым.",
"ERROR_PASSWORD_MISMATCH" : "Указанные пароли не совпадают.",
"FIELD_HEADER_PASSWORD" : "Пароль:",
@@ -143,7 +144,10 @@
"LOGIN": {
"ACTION_LOGIN" : "@:APP.ACTION_LOGIN",
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_LOGIN" : "@:APP.ACTION_LOGIN",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"ERROR_INVALID_LOGIN" : "Неверные данные для входа",
@@ -161,7 +165,7 @@
"ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Удалить подключение",
"DIALOG_HEADER_ERROR" : "Ошибка",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_HEADER_LOCATION" : "Размещение:",
"FIELD_HEADER_NAME" : "Название:",
@@ -194,7 +198,7 @@
"ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Удалить группу подключений",
"DIALOG_HEADER_ERROR" : "Ошибка",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_HEADER_LOCATION" : "Размещение:",
"FIELD_HEADER_NAME" : "Название:",
@@ -216,8 +220,8 @@
"ACTION_DELETE" : "@:APP.ACTION_DELETE",
"ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Удалить пользователя",
"DIALOG_HEADER_ERROR" : "Ошибка",
"DIALOG_HEADER_CONFIRM_DELETE" : "Удалить пользователя",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
@@ -393,7 +397,7 @@
"ACTION_NEW_CONNECTION" : "Новое подключение",
"ACTION_NEW_CONNECTION_GROUP" : "Новая группа",
"DIALOG_HEADER_ERROR" : "Ошибка",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"HELP_CONNECTIONS" : "Нажмите на подключение, чтобы управлять им. В зависимости от прав доступа возможно добавление и удаление подключений, а также изменение их свойств (протокол, название сервера, порт и пр.).",
@@ -409,9 +413,9 @@
"ACTION_CANCEL" : "@:APP.ACTION_CANCEL",
"ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"ERROR_PASSWORD_BLANK" : "Пароль не может быть пустым.",
"ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK",
"ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
"FIELD_HEADER_LANGUAGE" : "Язык:",
@@ -448,7 +452,7 @@
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_NEW_USER" : "Новый пользователь",
"DIALOG_HEADER_ERROR" : "Ошибка",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"HELP_USERS" : "Нажмите на пользователя, чтобы управлять им. В зависимости от прав доступа возможно добавление и удаление пользователей, а также изменение паролей.",
@@ -463,7 +467,7 @@
"ACTION_DELETE" : "Завершить сессии",
"DIALOG_HEADER_CONFIRM_DELETE" : "Завершение сессий",
"DIALOG_HEADER_ERROR" : "Ошибка",
"DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_PLACEHOLDER_FILTER" : "Фильтр",