]> WPIA git - gigi.git/blob - src/org/cacert/gigi/dbObjects/User.java
Merge "Update notes about password security"
[gigi.git] / src / org / cacert / gigi / dbObjects / User.java
1 package org.cacert.gigi.dbObjects;
2
3 import java.util.ArrayList;
4 import java.util.Collections;
5 import java.util.HashSet;
6 import java.util.LinkedList;
7 import java.util.List;
8 import java.util.Locale;
9 import java.util.Set;
10
11 import org.cacert.gigi.GigiApiException;
12 import org.cacert.gigi.database.GigiPreparedStatement;
13 import org.cacert.gigi.database.GigiResultSet;
14 import org.cacert.gigi.localisation.Language;
15 import org.cacert.gigi.output.DateSelector;
16 import org.cacert.gigi.pages.PasswordResetPage;
17 import org.cacert.gigi.util.CalendarUtil;
18 import org.cacert.gigi.util.DayDate;
19 import org.cacert.gigi.util.Notary;
20 import org.cacert.gigi.util.PasswordHash;
21 import org.cacert.gigi.util.PasswordStrengthChecker;
22
23 /**
24  * Represents an acting, assurable, user. Synchronizing on user means: no
25  * name-change and no assurance.
26  */
27 public class User extends CertificateOwner {
28
29     private Name name = new Name(null, null, null, null);
30
31     private DayDate dob;
32
33     private String email;
34
35     private Assurance[] receivedAssurances;
36
37     private Assurance[] madeAssurances;
38
39     private Locale locale;
40
41     private final Set<Group> groups = new HashSet<>();
42
43     public static final int MINIMUM_AGE = 16;
44
45     public static final int POJAM_AGE = 14;
46
47     public static final int ADULT_AGE = 18;
48
49     public static final boolean POJAM_ENABLED = false;
50
51     protected User(GigiResultSet rs) {
52         super(rs.getInt("id"));
53         updateName(rs);
54     }
55
56     private void updateName(GigiResultSet rs) {
57         name = new Name(rs.getString("fname"), rs.getString("lname"), rs.getString("mname"), rs.getString("suffix"));
58         dob = new DayDate(rs.getDate("dob"));
59         email = rs.getString("email");
60
61         String localeStr = rs.getString("language");
62         if (localeStr == null || localeStr.equals("")) {
63             locale = Locale.getDefault();
64         } else {
65             locale = Language.getLocaleFromString(localeStr);
66         }
67
68         try (GigiPreparedStatement psg = new GigiPreparedStatement("SELECT `permission` FROM `user_groups` WHERE `user`=? AND `deleted` is NULL")) {
69             psg.setInt(1, rs.getInt("id"));
70
71             try (GigiResultSet rs2 = psg.executeQuery()) {
72                 while (rs2.next()) {
73                     groups.add(Group.getByString(rs2.getString(1)));
74                 }
75             }
76         }
77     }
78
79     public User(String email, String password, Name name, DayDate dob, Locale locale) throws GigiApiException {
80         this.email = email;
81         this.dob = dob;
82         this.name = name;
83         this.locale = locale;
84         try (GigiPreparedStatement query = new GigiPreparedStatement("INSERT INTO `users` SET `email`=?, `password`=?, " + "`fname`=?, `mname`=?, `lname`=?, " + "`suffix`=?, `dob`=?, `language`=?, id=?")) {
85             query.setString(1, email);
86             query.setString(2, PasswordHash.hash(password));
87             query.setString(3, name.getFname());
88             query.setString(4, name.getMname());
89             query.setString(5, name.getLname());
90             query.setString(6, name.getSuffix());
91             query.setDate(7, dob.toSQLDate());
92             query.setString(8, locale.toString());
93             query.setInt(9, getId());
94             query.execute();
95         }
96         new EmailAddress(this, email, locale);
97     }
98
99     public Name getName() {
100         return name;
101     }
102
103     public DayDate getDoB() {
104         return dob;
105     }
106
107     public void setDoB(DayDate dob) {
108         this.dob = dob;
109     }
110
111     public String getEmail() {
112         return email;
113     }
114
115     public void changePassword(String oldPass, String newPass) throws GigiApiException {
116         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `password` FROM `users` WHERE `id`=?")) {
117             ps.setInt(1, getId());
118             try (GigiResultSet rs = ps.executeQuery()) {
119                 if ( !rs.next()) {
120                     throw new GigiApiException("User not found... very bad.");
121                 }
122                 if (PasswordHash.verifyHash(oldPass, rs.getString(1)) == null) {
123                     throw new GigiApiException("Old password does not match.");
124                 }
125             }
126         }
127         setPassword(newPass);
128     }
129
130     private void setPassword(String newPass) throws GigiApiException {
131         PasswordStrengthChecker.assertStrongPassword(newPass, getName(), getEmail());
132         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE users SET `password`=? WHERE id=?")) {
133             ps.setString(1, PasswordHash.hash(newPass));
134             ps.setInt(2, getId());
135             ps.executeUpdate();
136         }
137     }
138
139     public void setName(Name name) {
140         this.name = name;
141     }
142
143     public boolean canAssure() {
144         if (POJAM_ENABLED) {
145             if ( !CalendarUtil.isOfAge(dob, POJAM_AGE)) { // PoJAM
146                 return false;
147             }
148         } else {
149             if ( !CalendarUtil.isOfAge(dob, ADULT_AGE)) {
150                 return false;
151             }
152         }
153         if (getAssurancePoints() < 100) {
154             return false;
155         }
156
157         return hasPassedCATS();
158
159     }
160
161     public boolean hasPassedCATS() {
162         try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT 1 FROM `cats_passed` where `user_id`=? AND `variant_id`=?")) {
163             query.setInt(1, getId());
164             query.setInt(2, CATS.ASSURER_CHALLENGE_ID);
165             try (GigiResultSet rs = query.executeQuery()) {
166                 if (rs.next()) {
167                     return true;
168                 } else {
169                     return false;
170                 }
171             }
172         }
173     }
174
175     public int getAssurancePoints() {
176         try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT sum(points) FROM `notary` where `to`=? AND `deleted` is NULL AND (`expire` IS NULL OR `expire` > CURRENT_TIMESTAMP)")) {
177             query.setInt(1, getId());
178
179             GigiResultSet rs = query.executeQuery();
180             int points = 0;
181
182             if (rs.next()) {
183                 points = rs.getInt(1);
184             }
185
186             return points;
187         }
188     }
189
190     public int getExperiencePoints() {
191         try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT count(*) FROM `notary` where `from`=? AND `deleted` is NULL")) {
192             query.setInt(1, getId());
193
194             GigiResultSet rs = query.executeQuery();
195             int points = 0;
196
197             if (rs.next()) {
198                 points = rs.getInt(1) * 2;
199             }
200
201             return points;
202         }
203     }
204
205     /**
206      * Gets the maximum allowed points NOW. Note that an assurance needs to
207      * re-check PoJam as it has taken place in the past.
208      * 
209      * @return the maximal points @
210      */
211     public int getMaxAssurePoints() {
212         if ( !CalendarUtil.isOfAge(dob, ADULT_AGE) && POJAM_ENABLED) {
213             return 10; // PoJAM
214         }
215
216         int exp = getExperiencePoints();
217         int points = 10;
218
219         if (exp >= 10) {
220             points += 5;
221         }
222         if (exp >= 20) {
223             points += 5;
224         }
225         if (exp >= 30) {
226             points += 5;
227         }
228         if (exp >= 40) {
229             points += 5;
230         }
231         if (exp >= 50) {
232             points += 5;
233         }
234
235         return points;
236     }
237
238     public boolean isValidName(String name) {
239         return getName().matches(name);
240     }
241
242     public void updateDefaultEmail(EmailAddress newMail) throws GigiApiException {
243         for (EmailAddress email : getEmails()) {
244             if (email.getAddress().equals(newMail.getAddress())) {
245                 if ( !email.isVerified()) {
246                     throw new GigiApiException("Email not verified.");
247                 }
248
249                 try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE users SET email=? WHERE id=?")) {
250                     ps.setString(1, newMail.getAddress());
251                     ps.setInt(2, getId());
252                     ps.execute();
253                 }
254
255                 this.email = newMail.getAddress();
256                 return;
257             }
258         }
259
260         throw new GigiApiException("Given address not an address of the user.");
261     }
262
263     public void deleteEmail(EmailAddress delMail) throws GigiApiException {
264         if (getEmail().equals(delMail.getAddress())) {
265             throw new GigiApiException("Can't delete user's default e-mail.");
266         }
267
268         for (EmailAddress email : getEmails()) {
269             if (email.getId() == delMail.getId()) {
270                 try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `emails` SET `deleted`=CURRENT_TIMESTAMP WHERE `id`=?")) {
271                     ps.setInt(1, delMail.getId());
272                     ps.execute();
273                 }
274                 return;
275             }
276         }
277         throw new GigiApiException("Email not one of user's email addresses.");
278     }
279
280     public synchronized Assurance[] getReceivedAssurances() {
281         if (receivedAssurances == null) {
282             try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT * FROM `notary` WHERE `to`=? AND `deleted` IS NULL")) {
283                 query.setInt(1, getId());
284
285                 GigiResultSet res = query.executeQuery();
286                 List<Assurance> assurances = new LinkedList<Assurance>();
287
288                 while (res.next()) {
289                     assurances.add(assuranceByRes(res));
290                 }
291
292                 this.receivedAssurances = assurances.toArray(new Assurance[0]);
293             }
294         }
295
296         return receivedAssurances;
297     }
298
299     public synchronized Assurance[] getMadeAssurances() {
300         if (madeAssurances == null) {
301             try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT * FROM notary WHERE `from`=? AND deleted is NULL")) {
302                 query.setInt(1, getId());
303
304                 try (GigiResultSet res = query.executeQuery()) {
305                     List<Assurance> assurances = new LinkedList<Assurance>();
306
307                     while (res.next()) {
308                         assurances.add(assuranceByRes(res));
309                     }
310
311                     this.madeAssurances = assurances.toArray(new Assurance[0]);
312                 }
313             }
314         }
315
316         return madeAssurances;
317     }
318
319     public synchronized void invalidateMadeAssurances() {
320         madeAssurances = null;
321     }
322
323     public synchronized void invalidateReceivedAssurances() {
324         receivedAssurances = null;
325     }
326
327     public void updateUserData() throws GigiApiException {
328         synchronized (Notary.class) {
329             if (getReceivedAssurances().length != 0) {
330                 throw new GigiApiException("No change after assurance allowed.");
331             }
332             rawUpdateUserData();
333         }
334     }
335
336     protected void rawUpdateUserData() {
337         try (GigiPreparedStatement update = new GigiPreparedStatement("UPDATE users SET fname=?, lname=?, mname=?, suffix=?, dob=? WHERE id=?")) {
338             update.setString(1, name.getFname());
339             update.setString(2, name.getLname());
340             update.setString(3, name.getMname());
341             update.setString(4, name.getSuffix());
342             update.setDate(5, getDoB().toSQLDate());
343             update.setInt(6, getId());
344             update.executeUpdate();
345         }
346     }
347
348     public Locale getPreferredLocale() {
349         return locale;
350     }
351
352     public void setPreferredLocale(Locale locale) {
353         this.locale = locale;
354
355     }
356
357     public boolean wantsDirectoryListing() {
358         try (GigiPreparedStatement get = new GigiPreparedStatement("SELECT listme FROM users WHERE id=?")) {
359             get.setInt(1, getId());
360             GigiResultSet exec = get.executeQuery();
361             return exec.next() && exec.getBoolean("listme");
362         }
363     }
364
365     public String getContactInformation() {
366         try (GigiPreparedStatement get = new GigiPreparedStatement("SELECT contactinfo FROM users WHERE id=?")) {
367             get.setInt(1, getId());
368
369             GigiResultSet exec = get.executeQuery();
370             exec.next();
371             return exec.getString("contactinfo");
372         }
373     }
374
375     public void setDirectoryListing(boolean on) {
376         try (GigiPreparedStatement update = new GigiPreparedStatement("UPDATE users SET listme = ? WHERE id = ?")) {
377             update.setBoolean(1, on);
378             update.setInt(2, getId());
379             update.executeUpdate();
380         }
381     }
382
383     public void setContactInformation(String contactInfo) {
384         try (GigiPreparedStatement update = new GigiPreparedStatement("UPDATE users SET contactinfo = ? WHERE id = ?")) {
385             update.setString(1, contactInfo);
386             update.setInt(2, getId());
387             update.executeUpdate();
388         }
389     }
390
391     public boolean isInGroup(Group g) {
392         return groups.contains(g);
393     }
394
395     public Set<Group> getGroups() {
396         return Collections.unmodifiableSet(groups);
397     }
398
399     public void grantGroup(User granter, Group toGrant) {
400         groups.add(toGrant);
401         try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `user_groups` SET `user`=?, `permission`=?::`userGroup`, `grantedby`=?")) {
402             ps.setInt(1, getId());
403             ps.setString(2, toGrant.getDatabaseName());
404             ps.setInt(3, granter.getId());
405             ps.execute();
406         }
407     }
408
409     public void revokeGroup(User revoker, Group toRevoke) {
410         groups.remove(toRevoke);
411         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `user_groups` SET `deleted`=CURRENT_TIMESTAMP, `revokedby`=? WHERE `deleted` IS NULL AND `permission`=?::`userGroup` AND `user`=?")) {
412             ps.setInt(1, revoker.getId());
413             ps.setString(2, toRevoke.getDatabaseName());
414             ps.setInt(3, getId());
415             ps.execute();
416         }
417     }
418
419     public List<Organisation> getOrganisations() {
420         return getOrganisations(false);
421     }
422
423     public List<Organisation> getOrganisations(boolean isAdmin) {
424         List<Organisation> orgas = new ArrayList<>();
425         try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT `orgid` FROM `org_admin` WHERE `memid`=? AND `deleted` IS NULL" + (isAdmin ? " AND master='y'" : ""))) {
426             query.setInt(1, getId());
427             try (GigiResultSet res = query.executeQuery()) {
428                 while (res.next()) {
429                     orgas.add(Organisation.getById(res.getInt(1)));
430                 }
431
432                 return orgas;
433             }
434         }
435     }
436
437     public static synchronized User getById(int id) {
438         CertificateOwner co = CertificateOwner.getById(id);
439         if (co instanceof User) {
440             return (User) co;
441         }
442
443         return null;
444     }
445
446     public static User getByEmail(String mail) {
447         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `users`.`id` FROM `users` INNER JOIN `certOwners` ON `certOwners`.`id` = `users`.`id` WHERE `email`=? AND `deleted` IS NULL")) {
448             ps.setString(1, mail);
449             GigiResultSet rs = ps.executeQuery();
450             if ( !rs.next()) {
451                 return null;
452             }
453
454             return User.getById(rs.getInt(1));
455         }
456     }
457
458     public static User[] findByEmail(String mail) {
459         LinkedList<User> results = new LinkedList<User>();
460         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `users`.`id` FROM `users` INNER JOIN `certOwners` ON `certOwners`.`id` = `users`.`id` WHERE `users`.`email` LIKE ? AND `deleted` IS NULL GROUP BY `users`.`id` LIMIT 100")) {
461             ps.setString(1, mail);
462             GigiResultSet rs = ps.executeQuery();
463             while (rs.next()) {
464                 results.add(User.getById(rs.getInt(1)));
465             }
466             return results.toArray(new User[results.size()]);
467         }
468     }
469
470     public EmailAddress[] getEmails() {
471         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `id` FROM `emails` WHERE `memid`=? AND `deleted` IS NULL")) {
472             ps.setInt(1, getId());
473
474             GigiResultSet rs = ps.executeQuery();
475             LinkedList<EmailAddress> data = new LinkedList<EmailAddress>();
476
477             while (rs.next()) {
478                 data.add(EmailAddress.getById(rs.getInt(1)));
479             }
480
481             return data.toArray(new EmailAddress[0]);
482         }
483     }
484
485     @Override
486     public boolean isValidEmail(String email) {
487         for (EmailAddress em : getEmails()) {
488             if (em.getAddress().equals(email)) {
489                 return em.isVerified();
490             }
491         }
492
493         return false;
494     }
495
496     public String[] getTrainings() {
497         try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT `pass_date`, `type_text` FROM `cats_passed` LEFT JOIN `cats_type` ON `cats_type`.`id`=`cats_passed`.`variant_id`  WHERE `user_id`=? ORDER BY `pass_date` ASC")) {
498             prep.setInt(1, getId());
499             GigiResultSet res = prep.executeQuery();
500             List<String> entries = new LinkedList<String>();
501
502             while (res.next()) {
503
504                 entries.add(DateSelector.getDateFormat().format(res.getTimestamp(1)) + " (" + res.getString(2) + ")");
505             }
506
507             return entries.toArray(new String[0]);
508         }
509
510     }
511
512     public int generatePasswordResetTicket(User actor, String token, String privateToken) {
513         try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `passwordResetTickets` SET `memid`=?, `creator`=?, `token`=?, `private_token`=?")) {
514             ps.setInt(1, getId());
515             ps.setInt(2, getId());
516             ps.setString(3, token);
517             ps.setString(4, PasswordHash.hash(privateToken));
518             ps.execute();
519             return ps.lastInsertId();
520         }
521     }
522
523     public static User getResetWithToken(int id, String token) {
524         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid` FROM `passwordResetTickets` WHERE `id`=? AND `token`=? AND `used` IS NULL AND `created` > CURRENT_TIMESTAMP - interval '1 hours' * ?")) {
525             ps.setInt(1, id);
526             ps.setString(2, token);
527             ps.setInt(3, PasswordResetPage.HOUR_MAX);
528             GigiResultSet res = ps.executeQuery();
529             if ( !res.next()) {
530                 return null;
531             }
532             return User.getById(res.getInt(1));
533         }
534     }
535
536     public synchronized void consumePasswordResetTicket(int id, String private_token, String newPassword) throws GigiApiException {
537         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `private_token` FROM `passwordResetTickets` WHERE `id`=? AND `memid`=? AND `used` IS NULL")) {
538             ps.setInt(1, id);
539             ps.setInt(2, getId());
540             GigiResultSet rs = ps.executeQuery();
541             if ( !rs.next()) {
542                 throw new GigiApiException("Token could not be found, has already been used, or is expired.");
543             }
544             if (PasswordHash.verifyHash(private_token, rs.getString(1)) == null) {
545                 throw new GigiApiException("Private token does not match.");
546             }
547             setPassword(newPassword);
548         }
549         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `passwordResetTickets` SET  `used` = CURRENT_TIMESTAMP WHERE `id`=?")) {
550             ps.setInt(1, id);
551             ps.executeUpdate();
552         }
553     }
554
555     private Assurance assuranceByRes(GigiResultSet res) {
556         return new Assurance(res.getInt("id"), User.getById(res.getInt("from")), User.getById(res.getInt("to")), res.getString("location"), res.getString("method"), res.getInt("points"), res.getString("date"));
557     }
558 }