Allow abilitiy to boot controller with non-default admin credentials
[controller.git] / opendaylight / usermanager / api / src / main / java / org / opendaylight / controller / usermanager / UserConfig.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8
9 package org.opendaylight.controller.usermanager;
10
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;
21
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;
26
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;
35
36 /**
37  * Configuration Java Object which represents a Local AAA user configuration
38  * information for User Manager.
39  */
40 @XmlRootElement
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;
53
54     static {
55         try {
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,
59                     e.getMessage()));
60         }
61         UserConfig.randomGenerator = new SecureRandom(BitBufferHelper.toByteArray(System.currentTimeMillis()));
62     }
63
64     /**
65      * User Id
66      */
67     @XmlElement
68     protected String user;
69
70     /**
71      * List of roles a user can have
72      * example
73      * System-Admin
74      * Network-Admin
75      * Network-Operator
76      */
77     @XmlElement
78     protected List<String> roles;
79
80     /**
81      * Password
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.
85      */
86     @XmlElement
87     private String password;
88
89     private byte[] salt;
90
91
92
93     public UserConfig() {
94     }
95
96     /**
97      * Construct a UserConfig object and takes care of hashing the user password
98      *
99      * @param user
100      *            the user name
101      * @param password
102      *            the plain text password
103      * @param roles
104      *            the list of roles
105      */
106     public UserConfig(String user, String password, List<String> roles) {
107         this.user = user;
108
109         /*
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.
115          */
116         if (validateClearTextPassword(password).isSuccess()) {
117             this.salt = BitBufferHelper.toByteArray(randomGenerator.nextLong());
118             this.password = hash(salt, password);
119         } else {
120             this.salt = null;
121             this.password = BAD_PASSWORD;
122         }
123
124         this.roles = (roles == null) ? Collections.<String>emptyList() : new ArrayList<String>(roles);
125     }
126
127     public String getUser() {
128         return user;
129     }
130
131     public String getPassword() {
132         return password;
133     }
134
135     public List<String> getRoles() {
136         return new ArrayList<String>(roles);
137     }
138
139     public byte[] getSalt() {
140         return (salt == null) ? null : salt.clone();
141     }
142
143     @Override
144     public int hashCode() {
145         final int prime = 31;
146         int result = 1;
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());
151         return result;
152     }
153
154     @Override
155     public boolean equals(Object obj) {
156         if (this == obj) {
157             return true;
158         }
159         if (obj == null) {
160             return false;
161         }
162         if (getClass() != obj.getClass()) {
163             return false;
164         }
165         UserConfig other = (UserConfig) obj;
166         if (password == null) {
167             if (other.password != null) {
168                 return false;
169             }
170         } else if (!password.equals(other.password)) {
171             return false;
172         }
173         if (roles == null) {
174             if (other.roles != null) {
175                 return false;
176             }
177         } else if (!roles.equals(other.roles)) {
178             return false;
179         }
180         if (user == null) {
181             if (other.user != null) {
182                 return false;
183             }
184         } else if (!user.equals(other.user)) {
185             return false;
186         }
187         return true;
188     }
189
190     @Override
191     public String toString() {
192         return "UserConfig[user=" + user + ", password=" + password + ", roles=" + roles +"]";
193     }
194
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");
203         }
204         if (validCheck.isSuccess()) {
205             validCheck = validateRoles();
206         }
207         return validCheck;
208     }
209
210     protected Status validateUsername() {
211         if (user == null || user.isEmpty()) {
212             return new Status(StatusCode.BADREQUEST, "Username cannot be empty");
213         }
214
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 ./#%;?\\");
221         }
222
223         return new Status(StatusCode.SUCCESS);
224     }
225
226     public static Status validateClearTextPassword(String password) {
227         if (password == null || password.isEmpty()) {
228             return new Status(StatusCode.BADREQUEST, "Password cannot be empty");
229         }
230
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");
235         }
236         return new Status(StatusCode.SUCCESS);
237     }
238
239     protected Status validateRoles() {
240         if (roles == null || roles.isEmpty()) {
241             return new Status(StatusCode.BADREQUEST, "No role specified");
242         }
243         return new Status(StatusCode.SUCCESS);
244     }
245
246     public Status update(String currentPassword, String newPassword, List<String> newRoles) {
247
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");
252         }
253
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;
259
260         // Validate it
261         Status status = proposed.validate();
262         if (!status.isSuccess()) {
263             return status;
264         }
265
266         // Accept the modifications
267         this.user = proposed.user;
268         this.password = proposed.password;
269         this.roles = new ArrayList<String>(proposed.roles);
270
271         return status;
272     }
273
274     public boolean isPasswordMatch(String otherPass) {
275         return this.password.equals(hash(this.salt, otherPass));
276     }
277
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());
283         } else {
284             locResponse.setStatus(AuthResultEnum.AUTH_REJECT_LOC);
285         }
286         return locResponse;
287     }
288
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()) {
295                 buffer.append(" ");
296                 buffer.append(iter.next());
297             }
298         }
299         return buffer.toString();
300     }
301
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;
308     }
309
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");
313             return message;
314         }
315         if (salt == null || salt.length == 0) {
316             log.warn("Password hash requested but empty or no salt provided");
317             return message;
318         }
319
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);
325
326         UserConfig.oneWayFunction.reset();
327         return HexEncode.bytesToHexString(UserConfig.oneWayFunction.digest(concatenate(salt, message)));
328     }
329
330     /**
331      * Returns UserConfig instance populated with the passed parameters. It does
332      * not run any checks on the passed parameters.
333      *
334      * @param userName
335      *            the user name
336      * @param password
337      *            the plain text password
338      * @param roles
339      *            the list of roles
340      * @return the UserConfig object populated with the passed parameters. No
341      *         validity check is run on the input parameters.
342      */
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;
349         return config;
350     }
351 }