83532a1012f6a9431cd4f75a3f1b201e653ec395
[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.sal.authorization.AuthResultEnum;
28 import org.opendaylight.controller.sal.packet.BitBufferHelper;
29 import org.opendaylight.controller.sal.utils.HexEncode;
30 import org.opendaylight.controller.sal.utils.Status;
31 import org.opendaylight.controller.sal.utils.StatusCode;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * Configuration Java Object which represents a Local AAA user configuration
37  * information for User Manager.
38  */
39 @XmlRootElement
40 @XmlAccessorType(XmlAccessType.NONE)
41 public class UserConfig implements Serializable {
42     private static final long serialVersionUID = 1L;
43     private static Logger log = LoggerFactory.getLogger(UserConfig.class);
44     private static final boolean strongPasswordCheck = Boolean.getBoolean("enableStrongPasswordCheck");
45     private static final String DIGEST_ALGORITHM = "SHA-384";
46     private static final String BAD_PASSWORD = "Bad Password";
47     private static final int USERNAME_MAXLENGTH = 32;
48     protected static final String PASSWORD_REGEX = "(?=.*[^a-zA-Z0-9])(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,256}$";
49     private static final Pattern INVALID_USERNAME_CHARACTERS = Pattern.compile("([/\\s\\.\\?#%;\\\\]+)");
50     private static MessageDigest oneWayFunction;
51     private static SecureRandom randomGenerator;
52
53     static {
54         try {
55             UserConfig.oneWayFunction = MessageDigest.getInstance(DIGEST_ALGORITHM);
56         } catch (NoSuchAlgorithmException e) {
57             log.error(String.format("Implementation of %s digest algorithm not found: %s", DIGEST_ALGORITHM,
58                     e.getMessage()));
59         }
60         UserConfig.randomGenerator = new SecureRandom(BitBufferHelper.toByteArray(System.currentTimeMillis()));
61     }
62
63     /**
64      * User Id
65      */
66     @XmlElement
67     protected String user;
68
69     /**
70      * List of roles a user can have
71      * example
72      * System-Admin
73      * Network-Admin
74      * Network-Operator
75      */
76     @XmlElement
77     protected List<String> roles;
78
79     /**
80      * Password
81      * Should be 8 to 256 characters long,
82      * contain both upper and lower case letters, at least one number,
83      * and at least one non alphanumeric character.
84      */
85     @XmlElement
86     private String password;
87
88     private byte[] salt;
89
90
91
92     public UserConfig() {
93     }
94
95     /**
96      * Construct a UserConfig object and takes care of hashing the user password
97      *
98      * @param user
99      *            the user name
100      * @param password
101      *            the plain text password
102      * @param roles
103      *            the list of roles
104      */
105     public UserConfig(String user, String password, List<String> roles) {
106         this.user = user;
107
108         /*
109          * Password validation to be done on clear text password. If fails, mark
110          * the password with a well known label, so that object validation can
111          * report the proper error. Only if password is a valid one, generate
112          * salt, concatenate it with clear text password and hash the
113          * resulting string. Hash result is going to be our stored password.
114          */
115         if (validateClearTextPassword(password).isSuccess()) {
116             this.salt = BitBufferHelper.toByteArray(randomGenerator.nextLong());
117             this.password = hash(salt, password);
118         } else {
119             this.salt = null;
120             this.password = BAD_PASSWORD;
121         }
122
123         this.roles = (roles == null) ? Collections.<String>emptyList() : new ArrayList<String>(roles);
124     }
125
126     public String getUser() {
127         return user;
128     }
129
130     public String getPassword() {
131         return password;
132     }
133
134     public List<String> getRoles() {
135         return new ArrayList<String>(roles);
136     }
137
138     @Override
139     public int hashCode() {
140         final int prime = 31;
141         int result = 1;
142         result = prime * result
143                 + ((password == null) ? 0 : password.hashCode());
144         result = prime * result + ((roles == null) ? 0 : roles.hashCode());
145         result = prime * result + ((user == null) ? 0 : user.hashCode());
146         return result;
147     }
148
149     @Override
150     public boolean equals(Object obj) {
151         if (this == obj) {
152             return true;
153         }
154         if (obj == null) {
155             return false;
156         }
157         if (getClass() != obj.getClass()) {
158             return false;
159         }
160         UserConfig other = (UserConfig) obj;
161         if (password == null) {
162             if (other.password != null) {
163                 return false;
164             }
165         } else if (!password.equals(other.password)) {
166             return false;
167         }
168         if (roles == null) {
169             if (other.roles != null) {
170                 return false;
171             }
172         } else if (!roles.equals(other.roles)) {
173             return false;
174         }
175         if (user == null) {
176             if (other.user != null) {
177                 return false;
178             }
179         } else if (!user.equals(other.user)) {
180             return false;
181         }
182         return true;
183     }
184
185     @Override
186     public String toString() {
187         return "UserConfig[user=" + user + ", password=" + password + ", roles=" + roles +"]";
188     }
189
190     public Status validate() {
191         Status validCheck = validateUsername();
192         if (validCheck.isSuccess()) {
193             // Password validation was run at object construction time
194             validCheck = (!password.equals(BAD_PASSWORD)) ? new Status(StatusCode.SUCCESS) : new Status(
195                     StatusCode.BADREQUEST,
196                     "Password should be 8 to 256 characters long, contain both upper and lower case letters, "
197                             + "at least one number and at least one non alphanumeric character");
198         }
199         if (validCheck.isSuccess()) {
200             validCheck = validateRoles();
201         }
202         return validCheck;
203     }
204
205     protected Status validateUsername() {
206         if (user == null || user.isEmpty()) {
207             return new Status(StatusCode.BADREQUEST, "Username cannot be empty");
208         }
209
210         Matcher mUser = UserConfig.INVALID_USERNAME_CHARACTERS.matcher(user);
211         if (user.length() > UserConfig.USERNAME_MAXLENGTH || mUser.find() == true) {
212             return new Status(StatusCode.BADREQUEST,
213                     "Username can have 1-32 non-whitespace "
214                             + "alphanumeric characters and any special "
215                             + "characters except ./#%;?\\");
216         }
217
218         return new Status(StatusCode.SUCCESS);
219     }
220
221     private Status validateClearTextPassword(String password) {
222         if (password == null || password.isEmpty()) {
223             return new Status(StatusCode.BADREQUEST, "Password cannot be empty");
224         }
225
226         if (strongPasswordCheck && !password.matches(UserConfig.PASSWORD_REGEX)) {
227             return new Status(StatusCode.BADREQUEST, "Password should be 8 to 256 characters long, "
228                     + "contain both upper and lower case letters, at least one number "
229                     + "and at least one non alphanumeric character");
230         }
231         return new Status(StatusCode.SUCCESS);
232     }
233
234     protected Status validateRoles() {
235         if (roles == null || roles.isEmpty()) {
236             return new Status(StatusCode.BADREQUEST, "No role specified");
237         }
238         return new Status(StatusCode.SUCCESS);
239     }
240
241     public Status update(String currentPassword, String newPassword, List<String> newRoles) {
242
243         // To make any changes to a user configured profile, current password
244         // must always be provided
245         if (!this.password.equals(hash(this.salt, currentPassword))) {
246             return new Status(StatusCode.BADREQUEST, "Current password is incorrect");
247         }
248
249         // Create a new object with the proposed modifications
250         UserConfig proposed = new UserConfig();
251         proposed.user = this.user;
252         proposed.password = (newPassword == null)? this.password : hash(this.salt, newPassword);
253         proposed.roles = (newRoles == null)? this.roles : newRoles;
254
255         // Validate it
256         Status status = proposed.validate();
257         if (!status.isSuccess()) {
258             return status;
259         }
260
261         // Accept the modifications
262         this.user = proposed.user;
263         this.password = proposed.password;
264         this.roles = new ArrayList<String>(proposed.roles);
265
266         return status;
267     }
268
269     public AuthResponse authenticate(String clearTextPassword) {
270         AuthResponse locResponse = new AuthResponse();
271         if (password.equals(hash(this.salt, clearTextPassword))) {
272             locResponse.setStatus(AuthResultEnum.AUTH_ACCEPT_LOC);
273             locResponse.addData(getRolesString());
274         } else {
275             locResponse.setStatus(AuthResultEnum.AUTH_REJECT_LOC);
276         }
277         return locResponse;
278     }
279
280     protected String getRolesString() {
281         StringBuffer buffer = new StringBuffer();
282         if (!roles.isEmpty()) {
283             Iterator<String> iter = roles.iterator();
284             buffer.append(iter.next());
285             while (iter.hasNext()) {
286                 buffer.append(" ");
287                 buffer.append(iter.next());
288             }
289         }
290         return buffer.toString();
291     }
292
293     private static byte[] concatenate(byte[] salt, String password) {
294         byte[] messageArray = password.getBytes();
295         byte[] concatenation = new byte[salt.length + password.length()];
296         System.arraycopy(salt, 0, concatenation, 0, salt.length);
297         System.arraycopy(messageArray, 0, concatenation, salt.length, messageArray.length);
298         return concatenation;
299     }
300
301     private static String hash(byte[] salt, String message) {
302         if (message == null) {
303             log.warn("Password hash requested but empty or no password provided");
304             return message;
305         }
306         if (salt == null || salt.length == 0) {
307             log.warn("Password hash requested but empty or no salt provided");
308             return message;
309         }
310
311         // Concatenate salt and password
312         byte[] messageArray = message.getBytes();
313         byte[] concatenation = new byte[salt.length + message.length()];
314         System.arraycopy(salt, 0, concatenation, 0, salt.length);
315         System.arraycopy(messageArray, 0, concatenation, salt.length, messageArray.length);
316
317         UserConfig.oneWayFunction.reset();
318         return HexEncode.bytesToHexString(UserConfig.oneWayFunction.digest(concatenate(salt, message)));
319     }
320
321     /**
322      * Returns UserConfig instance populated with the passed parameters. It does
323      * not run any checks on the passed parameters.
324      *
325      * @param userName
326      *            the user name
327      * @param password
328      *            the plain text password
329      * @param roles
330      *            the list of roles
331      * @return the UserConfig object populated with the passed parameters. No
332      *         validity check is run on the input parameters.
333      */
334     public static UserConfig getUncheckedUserConfig(String userName, String password, List<String> roles) {
335         UserConfig config = new UserConfig();
336         config.user = userName;
337         config.salt = BitBufferHelper.toByteArray(randomGenerator.nextLong());
338         config.password = hash(config.salt, password);
339         config.roles = roles;
340         return config;
341     }
342 }