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.ConnectionGroupPermissionService;
import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionService;
import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionService; 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.Field;
import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.form.Form;
import org.glyptodon.guacamole.net.auth.User; 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"; 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 * All attributes related to restricting user accounts, within a logical
* form. * form.
*/ */
public static final Form ACCOUNT_RESTRICTIONS = new Form("restrictions", "Account Restrictions", Arrays.asList( public static final Form ACCOUNT_RESTRICTIONS = new Form("restrictions", Arrays.<Field>asList(
new Field(DISABLED_ATTRIBUTE_NAME, "Disabled", "true") 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>(); Map<String, String> attributes = new HashMap<String, String>();
// Set disabled attribute // 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; return attributes;
} }
@@ -223,7 +234,10 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
public void setAttributes(Map<String, String> attributes) { public void setAttributes(Map<String, String> attributes) {
// Translate disabled attribute // 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 com.google.inject.Provider;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.Credentials; 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 * 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 * 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 * @param credentials
* The credentials to use to produce the UserContext. * The credentials to use to produce the UserContext.
* *
* @return * @return
* A new UserContext instance for the user identified by the given * A new UserContext instance for the user identified by the given
* credentials, or null if the credentials are not valid. * credentials.
* *
* @throws GuacamoleException * @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 public org.glyptodon.guacamole.net.auth.UserContext
getUserContext(Credentials credentials) getUserContext(Credentials credentials)
@@ -67,7 +72,7 @@ public class UserContextService {
// Authenticate user // Authenticate user
ModeledUser user = userService.retrieveUser(credentials); ModeledUser user = userService.retrieveUser(credentials);
if (user != null && !user.getModel().isDisabled()) { if (user != null) {
// Upon successful authentication, return new user context // Upon successful authentication, return new user context
UserContext context = userContextProvider.get(); UserContext context = userContextProvider.get();
@@ -77,7 +82,7 @@ public class UserContextService {
} }
// Otherwise, unauthorized // 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; 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. * Creates a new, empty user.
*/ */
@@ -127,4 +134,28 @@ public class UserModel extends ObjectModel {
this.disabled = disabled; 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.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper;
import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectService; 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.ObjectPermissionModel;
import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionMapper;
import org.glyptodon.guacamole.auth.jdbc.security.PasswordEncryptionService; 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.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.ObjectPermission;
import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet;
import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission;
import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Service which provides convenience methods for creating, retrieving, and * 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> { 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 * All user permissions which are implicitly granted to the new user upon
* creation. * creation.
@@ -59,7 +71,43 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
ObjectPermission.Type.READ, ObjectPermission.Type.READ,
ObjectPermission.Type.UPDATE 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. * 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 * 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 * @param credentials
* The credentials to use when locating the user. * The credentials to use when locating the user.
@@ -221,8 +271,12 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
* @return * @return
* The existing ModeledUser object if the credentials given are valid, * The existing ModeledUser object if the credentials given are valid,
* null otherwise. * 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 // Get username and password
String username = credentials.getUsername(); String username = credentials.getUsername();
@@ -233,19 +287,55 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
if (userModel == null) if (userModel == null)
return null; return null;
// If password hash matches, return the retrieved user // If user is disabled, pretend user does not exist
byte[] hash = encryptionService.createPasswordHash(password, userModel.getPasswordSalt()); if (userModel.isDisabled())
if (Arrays.equals(hash, userModel.getPasswordHash())) { return null;
// Return corresponding user, set up cyclic reference // Verify provided password is correct
ModeledUser user = getObjectInstance(null, userModel); byte[] hash = encryptionService.createPasswordHash(password, userModel.getPasswordSalt());
user.setCurrentUser(new AuthenticatedUser(user, credentials)); if (!Arrays.equals(hash, userModel.getPasswordHash()))
return user; 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 now-authenticated user
return null; 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" : { "USER_ATTRIBUTES" : {
"FIELD_HEADER_DISABLED" : "Login disabled:", "FIELD_HEADER_DISABLED" : "Login disabled:",
"FIELD_HEADER_EXPIRED" : "Password expired:",
"SECTION_HEADER_RESTRICTIONS" : "Account Restrictions" "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_hash` binary(32) NOT NULL,
`password_salt` binary(32), `password_salt` binary(32),
`disabled` boolean NOT NULL DEFAULT 0, `disabled` boolean NOT NULL DEFAULT 0,
`expired` boolean NOT NULL DEFAULT 0,
PRIMARY KEY (`user_id`), PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`) UNIQUE KEY `username` (`username`)

View File

@@ -26,3 +26,9 @@
ALTER TABLE guacamole_user ADD COLUMN disabled BOOLEAN NOT NULL DEFAULT 0; 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" : [
"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_hash" property="passwordHash" jdbcType="BINARY"/>
<result column="password_salt" property="passwordSalt" jdbcType="BINARY"/> <result column="password_salt" property="passwordSalt" jdbcType="BINARY"/>
<result column="disabled" property="disabled" jdbcType="BOOLEAN"/> <result column="disabled" property="disabled" jdbcType="BOOLEAN"/>
<result column="expired" property="expired" jdbcType="BOOLEAN"/>
</resultMap> </resultMap>
<!-- Select all usernames --> <!-- Select all usernames -->
@@ -59,7 +60,8 @@
username, username,
password_hash, password_hash,
password_salt, password_salt,
disabled disabled,
expired
FROM guacamole_user FROM guacamole_user
WHERE username IN WHERE username IN
<foreach collection="identifiers" item="identifier" <foreach collection="identifiers" item="identifier"
@@ -77,7 +79,8 @@
username, username,
password_hash, password_hash,
password_salt, password_salt,
disabled disabled,
expired
FROM guacamole_user FROM guacamole_user
JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
WHERE username IN WHERE username IN
@@ -98,7 +101,8 @@
username, username,
password_hash, password_hash,
password_salt, password_salt,
disabled disabled,
expired
FROM guacamole_user FROM guacamole_user
WHERE WHERE
username = #{username,jdbcType=VARCHAR} username = #{username,jdbcType=VARCHAR}
@@ -119,13 +123,15 @@
username, username,
password_hash, password_hash,
password_salt, password_salt,
disabled disabled,
expired
) )
VALUES ( VALUES (
#{object.identifier,jdbcType=VARCHAR}, #{object.identifier,jdbcType=VARCHAR},
#{object.passwordHash,jdbcType=BINARY}, #{object.passwordHash,jdbcType=BINARY},
#{object.passwordSalt,jdbcType=BINARY}, #{object.passwordSalt,jdbcType=BINARY},
#{object.disabled,jdbcType=BOOLEAN} #{object.disabled,jdbcType=BOOLEAN},
#{object.expired,jdbcType=BOOLEAN}
) )
</insert> </insert>
@@ -135,7 +141,8 @@
UPDATE guacamole_user UPDATE guacamole_user
SET password_hash = #{object.passwordHash,jdbcType=BINARY}, SET password_hash = #{object.passwordHash,jdbcType=BINARY},
password_salt = #{object.passwordSalt,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} WHERE user_id = #{object.objectID,jdbcType=VARCHAR}
</update> </update>

View File

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

View File

@@ -26,3 +26,9 @@
ALTER TABLE guacamole_user ADD COLUMN disabled boolean NOT NULL DEFAULT FALSE; 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" : [
"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_hash" property="passwordHash" jdbcType="BINARY"/>
<result column="password_salt" property="passwordSalt" jdbcType="BINARY"/> <result column="password_salt" property="passwordSalt" jdbcType="BINARY"/>
<result column="disabled" property="disabled" jdbcType="BOOLEAN"/> <result column="disabled" property="disabled" jdbcType="BOOLEAN"/>
<result column="expired" property="expired" jdbcType="BOOLEAN"/>
</resultMap> </resultMap>
<!-- Select all usernames --> <!-- Select all usernames -->
@@ -59,7 +60,8 @@
username, username,
password_hash, password_hash,
password_salt, password_salt,
disabled disabled,
expired
FROM guacamole_user FROM guacamole_user
WHERE username IN WHERE username IN
<foreach collection="identifiers" item="identifier" <foreach collection="identifiers" item="identifier"
@@ -77,7 +79,8 @@
username, username,
password_hash, password_hash,
password_salt, password_salt,
disabled disabled,
expired
FROM guacamole_user FROM guacamole_user
JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
WHERE username IN WHERE username IN
@@ -98,7 +101,8 @@
username, username,
password_hash, password_hash,
password_salt, password_salt,
disabled disabled,
expired
FROM guacamole_user FROM guacamole_user
WHERE WHERE
username = #{username,jdbcType=VARCHAR} username = #{username,jdbcType=VARCHAR}
@@ -119,13 +123,15 @@
username, username,
password_hash, password_hash,
password_salt, password_salt,
disabled disabled,
expired
) )
VALUES ( VALUES (
#{object.identifier,jdbcType=VARCHAR}, #{object.identifier,jdbcType=VARCHAR},
#{object.passwordHash,jdbcType=BINARY}, #{object.passwordHash,jdbcType=BINARY},
#{object.passwordSalt,jdbcType=BINARY}, #{object.passwordSalt,jdbcType=BINARY},
#{object.disabled,jdbcType=BOOLEAN} #{object.disabled,jdbcType=BOOLEAN},
#{object.expired,jdbcType=BOOLEAN}
) )
</insert> </insert>
@@ -135,7 +141,8 @@
UPDATE guacamole_user UPDATE guacamole_user
SET password_hash = #{object.passwordHash,jdbcType=BINARY}, SET password_hash = #{object.passwordHash,jdbcType=BINARY},
password_salt = #{object.passwordSalt,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} WHERE user_id = #{object.objectID,jdbcType=VARCHAR}
</update> </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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -20,42 +20,29 @@
* THE SOFTWARE. * 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 * Creates a new EnumField with the given name and possible values.
* legal value of a field. *
* * @param name
* @constructor * The unique name to associate with this field.
* @param {FieldOption|Object} [template={}] *
* The object whose properties should be copied within the new * @param options
* FieldOption. * 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 * 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
@@ -27,7 +27,11 @@ import org.codehaus.jackson.map.annotate.JsonSerialize;
/** /**
* Represents an arbitrary field, such as an HTTP parameter, the parameter of a * 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 * @author Michael Jumper
*/ */
@@ -35,7 +39,11 @@ import org.codehaus.jackson.map.annotate.JsonSerialize;
public class Field { 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 { public static class Type {
@@ -83,67 +91,51 @@ public class Field {
*/ */
private String name; private String name;
/**
* A human-readable name to be presented to the user.
*/
private String title;
/** /**
* The type of this field. * The type of this field.
*/ */
private String type; private String type;
/** /**
* The value of this field, when checked. This is only applicable to * A collection of all legal values of this field.
* BOOLEAN fields.
*/ */
private String value; private Collection<String> options;
/** /**
* A collection of all associated field options. * Creates a new Parameter with no associated name or type.
*/
private Collection<FieldOption> options;
/**
* Creates a new Parameter with no associated name, title, or type.
*/ */
public Field() { 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 * @param name
* The unique name to associate with this field. * The unique name to associate with this field.
* *
* @param title
* The human-readable title to associate with this field.
*
* @param type * @param type
* The type of this field. * The type of this field.
*/ */
public Field(String name, String title, String type) { public Field(String name, String type) {
this.name = name; this.name = name;
this.title = title; this.type = type;
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 * @param name
* The unique name to associate with this field. * The unique name to associate with this field.
* *
* @param title * @param type
* The human-readable title to associate with this field. * The type of this field.
* *
* @param options * @param options
* A collection of all possible valid options for this field. * 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.name = name;
this.title = title; this.type = type;
this.type = Type.ENUM;
this.options = options; this.options = options;
} }
@@ -167,49 +159,6 @@ public class Field {
this.name = name; 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. * Returns the type of this field.
* *
@@ -232,25 +181,23 @@ public class Field {
/** /**
* Returns a mutable collection of field options. Changes to this * Returns a mutable collection of field options. Changes to this
* collection directly affect the available options. This is only * collection directly affect the available options.
* applicable to ENUM fields.
* *
* @return * @return
* A mutable collection of field options, or null if the field has no * A mutable collection of field options, or null if the field has no
* options. * options.
*/ */
public Collection<FieldOption> getOptions() { public Collection<String> getOptions() {
return options; return options;
} }
/** /**
* Sets the options available as possible values of this field. This is * Sets the options available as possible values of this field.
* only applicable to ENUM fields.
* *
* @param options * @param options
* The options to associate with this field. * The options to associate with this field.
*/ */
public void setOptions(Collection<FieldOption> options) { public void setOptions(Collection<String> options) {
this.options = options; this.options = options;
} }

View File

@@ -40,41 +40,32 @@ public class Form {
*/ */
private String name; private String name;
/**
* The a human-readable title describing this form.
*/
private String title;
/** /**
* All fields associated with this form. * All fields associated with this form.
*/ */
private Collection<Field> fields; private Collection<Field> fields;
/** /**
* Creates a new Form object with no associated fields. The name and title * Creates a new Form object with no associated fields. The name is left
* of the form are left unset as null. If no form name is provided, this * unset as null. If no form name is provided, this form must not be used
* form must not be used in the same context as another unnamed form. * in the same context as another unnamed form.
*/ */
public Form() { public Form() {
fields = new ArrayList<Field>(); fields = new ArrayList<Field>();
} }
/** /**
* Creates a new Form object having the given name and title, and * Creates a new Form object having the given name and containing the given
* containing the given fields. * fields.
* *
* @param name * @param name
* A name which uniquely identifies this form. * A name which uniquely identifies this form.
* *
* @param title
* A human-readable title describing this form.
*
* @param fields * @param fields
* The fields to provided within the new Form. * 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.name = name;
this.title = title;
this.fields = fields; this.fields = fields;
} }
@@ -120,26 +111,4 @@ public class Form {
this.name = name; 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.Collection;
import java.util.Collections; import java.util.Collections;
import org.glyptodon.guacamole.form.Field; 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. * 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()); 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 * CredentialsInfo object which describes standard username/password
* credentials. * credentials.
*/ */
public static final CredentialsInfo USERNAME_PASSWORD = new CredentialsInfo(Arrays.asList( public static final CredentialsInfo USERNAME_PASSWORD = new CredentialsInfo(Arrays.asList(
new Field("username", "username", Field.Type.USERNAME), USERNAME,
new Field("password", "password", Field.Type.PASSWORD) PASSWORD
)); ));
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,8 @@
* Failed logins may also result in guacInsufficientCredentials or * Failed logins may also result in guacInsufficientCredentials or
* guacInvalidCredentials events, if the provided credentials were rejected for * guacInvalidCredentials events, if the provided credentials were rejected for
* being insufficient or invalid respectively. Both events will be provided * 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 * expected credentials returned by the REST endpoint. This set of credentials
* will be in the form of a Field array. * 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 // Request credentials if provided credentials were invalid
if (error.type === Error.Type.INVALID_CREDENTIALS) 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 // Request more credentials if provided credentials were not enough
else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) 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; return authenticationProcess.promise;

View File

@@ -29,12 +29,12 @@ angular.module('form').controller('checkboxFieldController', ['$scope',
// Update typed value when model is changed // Update typed value when model is changed
$scope.$watch('model', function modelChanged(model) { $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 // Update string value in model when typed value is changed
$scope.$watch('typedValue', function typedValueChanged(typedValue) { $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 // Required services
var translationStringService = $injector.get('translationStringService'); 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 * 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

@@ -1,4 +1,4 @@
<div class="password-field"> <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 class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div>
</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; $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, * The credentials that the authentication service is currently expecting,
* if any. If the user is logged in, this will be null. * if any. If the user is logged in, this will be null.
* *
* @type Form[]|Form|Field[]|Field * @type Field[]
*/ */
$scope.expectedCredentials = null; $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 // 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.title = 'APP.NAME';
$scope.page.bodyClassName = ''; $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 // 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.title = 'APP.NAME';
$scope.page.bodyClassName = ''; $scope.page.bodyClassName = '';
$scope.expectedCredentials = expected; $scope.loginHelpText = error.message;
$scope.acceptedCredentials = parameters;
$scope.expectedCredentials = error.expected;
}); });
// Clear login screen if login was successful // Clear login screen if login was successful
$scope.$on('guacLogin', function loginSuccessful() { $scope.$on('guacLogin', function loginSuccessful() {
$scope.loginHelpText = null;
$scope.acceptedCredentials = null;
$scope.expectedCredentials = null; $scope.expectedCredentials = null;
}); });

View File

@@ -35,13 +35,28 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
// Login directive scope // Login directive scope
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 * The login form or set of fields. This will be displayed to the user
* to capture their credentials. * 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', directive.controller = ['$scope', '$injector',
function loginController($scope, $injector) { function loginController($scope, $injector) {
// Required types
var Error = $injector.get('Error');
var Field = $injector.get('Field');
// Required services // Required services
var $route = $injector.get('$route'); var $route = $injector.get('$route');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
/** /**
* Whether an error occurred during login. * A description of the error that occurred during login, if any.
* *
* @type Boolean * @type String
*/ */
$scope.loginError = false; $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 * Submits the currently-specified username and password to the
@@ -71,20 +143,42 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
*/ */
$scope.login = function login() { $scope.login = function login() {
// Start with cleared status
$scope.loginError = null;
// Attempt login once existing session is destroyed // Attempt login once existing session is destroyed
authenticationService.authenticate($scope.values) authenticationService.authenticate($scope.enteredValues)
// Clear and reload upon success // Clear and reload upon success
.then(function loginSuccessful() { .then(function loginSuccessful() {
$scope.loginError = false; $scope.enteredValues = {};
$scope.values = {};
$route.reload(); $route.reload();
}) })
// Reset upon failure // Reset upon failure
['catch'](function loginFailed() { ['catch'](function loginFailed(error) {
$scope.loginError = true;
$scope.values.password = ''; // 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; max-width: 3em;
margin: 0.5em auto; 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"] { .login-ui .login-dialog .buttons input[type="submit"] {
width: 100%; width: 100%;
margin: 0; 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. Copyright 2014 Glyptodon LLC.
@@ -22,7 +22,7 @@
--> -->
<!-- Login error message --> <!-- 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"> <div class="login-dialog-middle">
@@ -34,14 +34,18 @@
<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 message/instructions -->
<p ng-show="helpText">{{helpText | translate}}</p>
<!-- Login fields --> <!-- Login fields -->
<div class="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> </div>
<!-- Submit button --> <!-- Submit button -->
<div class="buttons"> <div class="buttons">
<input type="submit" name="login" class="login" value="{{'LOGIN.ACTION_LOGIN' | translate}}"/> <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> </div>
</form> </form>
@@ -49,4 +53,5 @@
</div> </div>
</div> </div>
</div> </div>

View File

@@ -46,13 +46,6 @@ angular.module('rest').factory('Field', [function defineField() {
*/ */
this.name = template.name; 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, * The type string defining which values this parameter may contain,
* as well as what properties are applicable. Valid types are listed * 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; this.type = template.type || Field.Type.TEXT;
/** /**
* The value to set the parameter to, in the case of a BOOLEAN * All possible legal values for this parameter.
* parameter, to enable that parameter's effect.
* *
* @type String * @type String[]
*/
this.value = template.value;
/**
* All possible legal values for this parameter. This property is only
* applicable to ENUM type parameters.
*
* @type FieldOption[]
*/ */
this.options = template.options; 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 * The type string associated with parameters that may contain only a
* single possible value, where that value enables the parameter's * 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 * @type String
*/ */

View File

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

View File

@@ -46,13 +46,6 @@ angular.module('rest').factory('Protocol', [function defineProtocol() {
*/ */
this.name = template.name; 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, * An array of forms containing all known parameters for this protocol,
* their types, and other information. * their types, and other information.

View File

@@ -50,7 +50,10 @@
</div> </div>
<!-- Login screen for logged-out users --> <!-- 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> <script type="text/javascript" src="app.js"></script>
</body> </body>

View File

@@ -7,6 +7,7 @@
"ACTION_ACKNOWLEDGE" : "OK", "ACTION_ACKNOWLEDGE" : "OK",
"ACTION_CANCEL" : "Cancel", "ACTION_CANCEL" : "Cancel",
"ACTION_CLONE" : "Clone", "ACTION_CLONE" : "Clone",
"ACTION_CONTINUE" : "Continue",
"ACTION_DELETE" : "Delete", "ACTION_DELETE" : "Delete",
"ACTION_DELETE_SESSIONS" : "Kill Sessions", "ACTION_DELETE_SESSIONS" : "Kill Sessions",
"ACTION_LOGIN" : "Login", "ACTION_LOGIN" : "Login",
@@ -23,6 +24,7 @@
"DIALOG_HEADER_ERROR" : "Error", "DIALOG_HEADER_ERROR" : "Error",
"ERROR_PASSWORD_BLANK" : "Your password cannot be blank.",
"ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.", "ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.",
"FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD" : "Password:",
@@ -143,7 +145,11 @@
"LOGIN": { "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", "ERROR_INVALID_LOGIN" : "Invalid Login",
@@ -161,7 +167,7 @@
"ACTION_SAVE" : "@:APP.ACTION_SAVE", "ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection",
"DIALOG_HEADER_ERROR" : "Error", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_HEADER_LOCATION" : "Location:", "FIELD_HEADER_LOCATION" : "Location:",
"FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_NAME" : "Name:",
@@ -194,7 +200,7 @@
"ACTION_SAVE" : "@:APP.ACTION_SAVE", "ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection Group", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection Group",
"DIALOG_HEADER_ERROR" : "Error", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_HEADER_LOCATION" : "Location:", "FIELD_HEADER_LOCATION" : "Location:",
"FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_NAME" : "Name:",
@@ -216,8 +222,8 @@
"ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_DELETE" : "@:APP.ACTION_DELETE",
"ACTION_SAVE" : "@:APP.ACTION_SAVE", "ACTION_SAVE" : "@:APP.ACTION_SAVE",
"DIALOG_HEADER_CONFIRM_DELETE" : "Delete User", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete User",
"DIALOG_HEADER_ERROR" : "Error", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
@@ -414,7 +420,7 @@
"ACTION_NEW_CONNECTION" : "New Connection", "ACTION_NEW_CONNECTION" : "New Connection",
"ACTION_NEW_CONNECTION_GROUP" : "New Group", "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.", "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", "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", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
"FIELD_HEADER_LANGUAGE" : "Display language:", "FIELD_HEADER_LANGUAGE" : "Display language:",
@@ -469,7 +475,7 @@
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_NEW_USER" : "New User", "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.", "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", "ACTION_DELETE" : "Kill Sessions",
"DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions", "DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions",
"DIALOG_HEADER_ERROR" : "Error", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
"FIELD_PLACEHOLDER_FILTER" : "Filter", "FIELD_PLACEHOLDER_FILTER" : "Filter",

View File

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