2 * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.controller.usermanager;
11 import java.io.Serializable;
12 import java.security.MessageDigest;
13 import java.security.NoSuchAlgorithmException;
14 import java.security.SecureRandom;
15 import java.util.ArrayList;
16 import java.util.Collections;
17 import java.util.Iterator;
18 import java.util.List;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
22 import javax.xml.bind.annotation.XmlAccessType;
23 import javax.xml.bind.annotation.XmlAccessorType;
24 import javax.xml.bind.annotation.XmlElement;
25 import javax.xml.bind.annotation.XmlRootElement;
27 import org.opendaylight.controller.configuration.ConfigurationObject;
28 import org.opendaylight.controller.sal.authorization.AuthResultEnum;
29 import org.opendaylight.controller.sal.packet.BitBufferHelper;
30 import org.opendaylight.controller.sal.utils.HexEncode;
31 import org.opendaylight.controller.sal.utils.Status;
32 import org.opendaylight.controller.sal.utils.StatusCode;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * Configuration Java Object which represents a Local AAA user configuration
38 * information for User Manager.
41 @XmlAccessorType(XmlAccessType.NONE)
42 public class UserConfig extends ConfigurationObject implements Serializable {
43 private static final long serialVersionUID = 1L;
44 private static Logger log = LoggerFactory.getLogger(UserConfig.class);
45 private static final boolean strongPasswordCheck = Boolean.getBoolean("enableStrongPasswordCheck");
46 private static final String DIGEST_ALGORITHM = "SHA-384";
47 private static final String BAD_PASSWORD = "Bad Password";
48 private static final int USERNAME_MAXLENGTH = 32;
49 protected static final String PASSWORD_REGEX = "(?=.*[^a-zA-Z0-9])(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,256}$";
50 private static final Pattern INVALID_USERNAME_CHARACTERS = Pattern.compile("([/\\s\\.\\?#%;\\\\]+)");
51 private static MessageDigest oneWayFunction;
52 private static SecureRandom randomGenerator;
56 UserConfig.oneWayFunction = MessageDigest.getInstance(DIGEST_ALGORITHM);
57 } catch (NoSuchAlgorithmException e) {
58 log.error(String.format("Implementation of %s digest algorithm not found: %s", DIGEST_ALGORITHM,
61 UserConfig.randomGenerator = new SecureRandom(BitBufferHelper.toByteArray(System.currentTimeMillis()));
68 protected String user;
71 * List of roles a user can have
78 protected List<String> roles;
82 * Should be 8 to 256 characters long,
83 * contain both upper and lower case letters, at least one number,
84 * and at least one non alphanumeric character.
87 private String password;
97 * Construct a UserConfig object and takes care of hashing the user password
102 * the plain text password
106 public UserConfig(String user, String password, List<String> roles) {
110 * Password validation to be done on clear text password. If fails, mark
111 * the password with a well known label, so that object validation can
112 * report the proper error. Only if password is a valid one, generate
113 * salt, concatenate it with clear text password and hash the
114 * resulting string. Hash result is going to be our stored password.
116 if (validateClearTextPassword(password).isSuccess()) {
117 this.salt = BitBufferHelper.toByteArray(randomGenerator.nextLong());
118 this.password = hash(salt, password);
121 this.password = BAD_PASSWORD;
124 this.roles = (roles == null) ? Collections.<String>emptyList() : new ArrayList<String>(roles);
127 public String getUser() {
131 public String getPassword() {
135 public List<String> getRoles() {
136 return new ArrayList<String>(roles);
139 public byte[] getSalt() {
140 return (salt == null) ? null : salt.clone();
144 public int hashCode() {
145 final int prime = 31;
147 result = prime * result
148 + ((password == null) ? 0 : password.hashCode());
149 result = prime * result + ((roles == null) ? 0 : roles.hashCode());
150 result = prime * result + ((user == null) ? 0 : user.hashCode());
155 public boolean equals(Object obj) {
162 if (getClass() != obj.getClass()) {
165 UserConfig other = (UserConfig) obj;
166 if (password == null) {
167 if (other.password != null) {
170 } else if (!password.equals(other.password)) {
174 if (other.roles != null) {
177 } else if (!roles.equals(other.roles)) {
181 if (other.user != null) {
184 } else if (!user.equals(other.user)) {
191 public String toString() {
192 return "UserConfig[user=" + user + ", password=" + password + ", roles=" + roles +"]";
195 public Status validate() {
196 Status validCheck = validateUsername();
197 if (validCheck.isSuccess()) {
198 // Password validation was run at object construction time
199 validCheck = (!password.equals(BAD_PASSWORD)) ? new Status(StatusCode.SUCCESS) : new Status(
200 StatusCode.BADREQUEST,
201 "Password should be 8 to 256 characters long, contain both upper and lower case letters, "
202 + "at least one number and at least one non alphanumeric character");
204 if (validCheck.isSuccess()) {
205 validCheck = validateRoles();
210 protected Status validateUsername() {
211 if (user == null || user.isEmpty()) {
212 return new Status(StatusCode.BADREQUEST, "Username cannot be empty");
215 Matcher mUser = UserConfig.INVALID_USERNAME_CHARACTERS.matcher(user);
216 if (user.length() > UserConfig.USERNAME_MAXLENGTH || mUser.find() == true) {
217 return new Status(StatusCode.BADREQUEST,
218 "Username can have 1-32 non-whitespace "
219 + "alphanumeric characters and any special "
220 + "characters except ./#%;?\\");
223 return new Status(StatusCode.SUCCESS);
226 public static Status validateClearTextPassword(String password) {
227 if (password == null || password.isEmpty()) {
228 return new Status(StatusCode.BADREQUEST, "Password cannot be empty");
231 if (strongPasswordCheck && !password.matches(UserConfig.PASSWORD_REGEX)) {
232 return new Status(StatusCode.BADREQUEST, "Password should be 8 to 256 characters long, "
233 + "contain both upper and lower case letters, at least one number "
234 + "and at least one non alphanumeric character");
236 return new Status(StatusCode.SUCCESS);
239 protected Status validateRoles() {
240 if (roles == null || roles.isEmpty()) {
241 return new Status(StatusCode.BADREQUEST, "No role specified");
243 return new Status(StatusCode.SUCCESS);
246 public Status update(String currentPassword, String newPassword, List<String> newRoles) {
248 // To make any changes to a user configured profile, current password
249 // must always be provided
250 if (!isPasswordMatch(currentPassword)) {
251 return new Status(StatusCode.BADREQUEST, "Current password is incorrect");
254 // Create a new object with the proposed modifications
255 UserConfig proposed = new UserConfig();
256 proposed.user = this.user;
257 proposed.password = (newPassword == null)? this.password : hash(this.salt, newPassword);
258 proposed.roles = (newRoles == null)? this.roles : newRoles;
261 Status status = proposed.validate();
262 if (!status.isSuccess()) {
266 // Accept the modifications
267 this.user = proposed.user;
268 this.password = proposed.password;
269 this.roles = new ArrayList<String>(proposed.roles);
274 public boolean isPasswordMatch(String otherPass) {
275 return this.password.equals(hash(this.salt, otherPass));
278 public AuthResponse authenticate(String clearTextPassword) {
279 AuthResponse locResponse = new AuthResponse();
280 if (isPasswordMatch(clearTextPassword)) {
281 locResponse.setStatus(AuthResultEnum.AUTH_ACCEPT_LOC);
282 locResponse.addData(getRolesString());
284 locResponse.setStatus(AuthResultEnum.AUTH_REJECT_LOC);
289 protected String getRolesString() {
290 StringBuffer buffer = new StringBuffer();
291 if (!roles.isEmpty()) {
292 Iterator<String> iter = roles.iterator();
293 buffer.append(iter.next());
294 while (iter.hasNext()) {
296 buffer.append(iter.next());
299 return buffer.toString();
302 private static byte[] concatenate(byte[] salt, String password) {
303 byte[] messageArray = password.getBytes();
304 byte[] concatenation = new byte[salt.length + password.length()];
305 System.arraycopy(salt, 0, concatenation, 0, salt.length);
306 System.arraycopy(messageArray, 0, concatenation, salt.length, messageArray.length);
307 return concatenation;
310 private static String hash(byte[] salt, String message) {
311 if (message == null) {
312 log.warn("Password hash requested but empty or no password provided");
315 if (salt == null || salt.length == 0) {
316 log.warn("Password hash requested but empty or no salt provided");
320 // Concatenate salt and password
321 byte[] messageArray = message.getBytes();
322 byte[] concatenation = new byte[salt.length + message.length()];
323 System.arraycopy(salt, 0, concatenation, 0, salt.length);
324 System.arraycopy(messageArray, 0, concatenation, salt.length, messageArray.length);
326 UserConfig.oneWayFunction.reset();
327 return HexEncode.bytesToHexString(UserConfig.oneWayFunction.digest(concatenate(salt, message)));
331 * Returns UserConfig instance populated with the passed parameters. It does
332 * not run any checks on the passed parameters.
337 * the plain text password
340 * @return the UserConfig object populated with the passed parameters. No
341 * validity check is run on the input parameters.
343 public static UserConfig getUncheckedUserConfig(String userName, String password, List<String> roles) {
344 UserConfig config = new UserConfig();
345 config.user = userName;
346 config.salt = BitBufferHelper.toByteArray(randomGenerator.nextLong());
347 config.password = hash(config.salt, password);
348 config.roles = roles;