Add strong password check for users
[controller.git] / opendaylight / usermanager / api / src / main / java / org / opendaylight / controller / usermanager / UserConfig.java
index 0e6a48ab52b32bdc66eb114ad26021ef07aa56ad..07c814adf14c7b2d35f9991a9bd3e5875296f922 100644 (file)
@@ -9,6 +9,9 @@
 package org.opendaylight.controller.usermanager;
 
 import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -16,6 +19,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.opendaylight.controller.sal.authorization.AuthResultEnum;
+import org.opendaylight.controller.sal.utils.HexEncode;
 import org.opendaylight.controller.sal.utils.Status;
 import org.opendaylight.controller.sal.utils.StatusCode;
 import org.opendaylight.controller.usermanager.AuthResponse;
@@ -27,27 +31,48 @@ import org.opendaylight.controller.usermanager.AuthResponse;
 public class UserConfig implements Serializable {
     private static final long serialVersionUID = 1L;
 
-    /*
-     * Clear text password as we are moving to some MD5 digest for when saving
-     * configurations
-     */
     protected String user;
     protected List<String> roles;
     private String password;
+
+    private static final boolean strongPasswordCheck = Boolean.getBoolean("enableStrongPasswordCheck");
+    private static final String BAD_PASSWORD = "Bad Password";
     private static final int USERNAME_MAXLENGTH = 32;
-    private static final int PASSWORD_MINLENGTH = 5;
-    private static final int PASSWORD_MAXLENGTH = 256;
-    private static final Pattern INVALID_USERNAME_CHARACTERS = Pattern
-            .compile("([/\\s\\.\\?#%;\\\\]+)");
+    protected static final String PASSWORD_REGEX = "(?=.*[^\\w])(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,256}$";
+    private static final Pattern INVALID_USERNAME_CHARACTERS = Pattern.compile("([/\\s\\.\\?#%;\\\\]+)");
+    private static MessageDigest oneWayFunction = null;
+    static {
+        try {
+            UserConfig.oneWayFunction = MessageDigest.getInstance("SHA-1");
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+    }
 
     public UserConfig() {
     }
 
+    /**
+     * Construct a UserConfig object and takes care of hashing the user password
+     *
+     * @param user
+     *            the user name
+     * @param password
+     *            the plain text password
+     * @param roles
+     *            the list of roles
+     */
     public UserConfig(String user, String password, List<String> roles) {
         this.user = user;
-        this.password = password;
-        this.roles = (roles == null) ? new ArrayList<String>()
-                : new ArrayList<String>(roles);
+
+        /*
+         * Password validation to be done on clear text password. If fails, mark
+         * the password with a well known label, so that object validation can
+         * report the proper error. Only if password is a valid one, hash it.
+         */
+        this.password = (validatePassword(password).isSuccess()) ? hash(password) : BAD_PASSWORD;
+
+        this.roles = (roles == null) ? new ArrayList<String>() : new ArrayList<String>(roles);
     }
 
     public String getUser() {
@@ -75,28 +100,37 @@ public class UserConfig implements Serializable {
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj)
+        if (this == obj) {
             return true;
-        if (obj == null)
+        }
+        if (obj == null) {
             return false;
-        if (getClass() != obj.getClass())
+        }
+        if (getClass() != obj.getClass()) {
             return false;
+        }
         UserConfig other = (UserConfig) obj;
         if (password == null) {
-            if (other.password != null)
+            if (other.password != null) {
                 return false;
-        } else if (!password.equals(other.password))
+            }
+        } else if (!password.equals(other.password)) {
             return false;
+        }
         if (roles == null) {
-            if (other.roles != null)
+            if (other.roles != null) {
                 return false;
-        } else if (!roles.equals(other.roles))
+            }
+        } else if (!roles.equals(other.roles)) {
             return false;
+        }
         if (user == null) {
-            if (other.user != null)
+            if (other.user != null) {
                 return false;
-        } else if (!user.equals(other.user))
+            }
+        } else if (!user.equals(other.user)) {
             return false;
+        }
         return true;
     }
 
@@ -106,12 +140,15 @@ public class UserConfig implements Serializable {
     }
 
     public Status validate() {
-        Status validCheck = validateRoles();
+        Status validCheck = validateUsername();
         if (validCheck.isSuccess()) {
-            validCheck = validateUsername();
+            validCheck = (!password.equals(BAD_PASSWORD)) ? new Status(StatusCode.SUCCESS) : new Status(
+                    StatusCode.BADREQUEST,
+                    "Password should be 8 to 256 characters long, contain both upper and lower case letters, "
+                            + "at least one number and at least one non alphanumeric character");
         }
         if (validCheck.isSuccess()) {
-            validCheck = validatePassword();
+            validCheck = validateRoles();
         }
         return validCheck;
     }
@@ -122,8 +159,7 @@ public class UserConfig implements Serializable {
         }
 
         Matcher mUser = UserConfig.INVALID_USERNAME_CHARACTERS.matcher(user);
-        if (user.length() > UserConfig.USERNAME_MAXLENGTH
-                || mUser.find() == true) {
+        if (user.length() > UserConfig.USERNAME_MAXLENGTH || mUser.find() == true) {
             return new Status(StatusCode.BADREQUEST,
                     "Username can have 1-32 non-whitespace "
                             + "alphanumeric characters and any special "
@@ -133,15 +169,15 @@ public class UserConfig implements Serializable {
         return new Status(StatusCode.SUCCESS);
     }
 
-    private Status validatePassword() {
+    private Status validatePassword(String password) {
         if (password == null || password.isEmpty()) {
             return new Status(StatusCode.BADREQUEST, "Password cannot be empty");
         }
 
-        if (password.length() < UserConfig.PASSWORD_MINLENGTH
-                || password.length() > UserConfig.PASSWORD_MAXLENGTH) {
-            return new Status(StatusCode.BADREQUEST,
-                    "Password should have 5-256 characters");
+        if (strongPasswordCheck && !password.matches(UserConfig.PASSWORD_REGEX)) {
+            return new Status(StatusCode.BADREQUEST, "Password should be 8 to 256 characters long, "
+                    + "contain both upper and lower case letters, at least one number "
+                    + "and at least one non alphanumeric character");
         }
         return new Status(StatusCode.SUCCESS);
     }
@@ -153,20 +189,19 @@ public class UserConfig implements Serializable {
         return new Status(StatusCode.SUCCESS);
     }
 
-    public Status update(String currentPassword, String newPassword,
-            List<String> newRoles) {
+    public Status update(String currentPassword, String newPassword, List<String> newRoles) {
+
         // To make any changes to a user configured profile, current password
         // must always be provided
-        if (!this.password.equals(currentPassword)) {
-            return new Status(StatusCode.BADREQUEST,
-                    "Current password is incorrect");
+        if (!this.password.equals(hash(currentPassword))) {
+            return new Status(StatusCode.BADREQUEST, "Current password is incorrect");
         }
 
         // Create a new object with the proposed modifications
         UserConfig proposed = new UserConfig();
         proposed.user = this.user;
-        proposed.password = (newPassword != null)? newPassword : this.password;
-        proposed.roles = (newRoles != null)? newRoles : this.roles;
+        proposed.password = (newPassword == null)? this.password : hash(newPassword);
+        proposed.roles = (newRoles == null)? this.roles : newRoles;
 
         // Validate it
         Status status = proposed.validate();
@@ -184,7 +219,7 @@ public class UserConfig implements Serializable {
 
     public AuthResponse authenticate(String clearTextPass) {
         AuthResponse locResponse = new AuthResponse();
-        if (password.equals(clearTextPass)) {
+        if (password.equals(hash(clearTextPass))) {
             locResponse.setStatus(AuthResultEnum.AUTH_ACCEPT_LOC);
             locResponse.addData(getRolesString());
         } else {
@@ -205,4 +240,33 @@ public class UserConfig implements Serializable {
         }
         return buffer.toString();
     }
+
+    public static String hash(String message) {
+        if (message == null) {
+            return message;
+        }
+        UserConfig.oneWayFunction.reset();
+        return HexEncode.bytesToHexString(UserConfig.oneWayFunction.digest(message.getBytes(Charset.defaultCharset())));
+    }
+
+    /**
+     * Returns UserConfig instance populated with the passed parameters. It does
+     * not run any checks on the passed parameters.
+     *
+     * @param userName
+     *            the user name
+     * @param password
+     *            the plain text password
+     * @param roles
+     *            the list of roles
+     * @return the UserConfig object populated with the passed parameters. No
+     *         validity check is run on the input parameters.
+     */
+    public static UserConfig getUncheckedUserConfig(String userName, String password, List<String> roles) {
+        UserConfig config = new UserConfig();
+        config.user = userName;
+        config.password = hash(password);
+        config.roles = roles;
+        return config;
+    }
 }