diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/BaseLdapPrivilegeHandler.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/BaseLdapPrivilegeHandler.java index b422553c8..5110577a1 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/BaseLdapPrivilegeHandler.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/BaseLdapPrivilegeHandler.java @@ -13,6 +13,7 @@ import li.strolch.privilege.base.AccessDeniedException; import li.strolch.privilege.base.InvalidCredentialsException; import li.strolch.privilege.model.UserState; import li.strolch.privilege.model.internal.User; +import li.strolch.privilege.model.internal.UserHistory; import li.strolch.privilege.policy.PrivilegePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -139,7 +140,7 @@ public abstract class BaseLdapPrivilegeHandler extends DefaultPrivilegeHandler { Map properties = buildProperties(username, attrs, ldapGroups, strolchRoles); return new User(username, username, null, null, null, -1, -1, firstName, lastName, UserState.REMOTE, - strolchRoles, locale, properties); + strolchRoles, locale, properties, new UserHistory()); } protected abstract Map buildProperties(String username, Attributes attrs, Set ldapGroups, diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultPrivilegeHandler.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultPrivilegeHandler.java index 026d5d921..751d0b950 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultPrivilegeHandler.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultPrivilegeHandler.java @@ -21,7 +21,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.text.MessageFormat; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; @@ -29,10 +29,7 @@ import java.util.stream.Stream; import li.strolch.privilege.base.*; import li.strolch.privilege.model.*; -import li.strolch.privilege.model.internal.PrivilegeImpl; -import li.strolch.privilege.model.internal.Role; -import li.strolch.privilege.model.internal.User; -import li.strolch.privilege.model.internal.UserChallenge; +import li.strolch.privilege.model.internal.*; import li.strolch.privilege.policy.PrivilegePolicy; import li.strolch.privilege.xml.CertificateStubsDomWriter; import li.strolch.privilege.xml.CertificateStubsSaxReader; @@ -396,6 +393,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { throw new PrivilegeModelException(MessageFormat.format(msg, userRep.getUsername())); } + UserHistory history = new UserHistory(); byte[] passwordHash = null; byte[] salt = null; if (password != null) { @@ -408,10 +406,12 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // hash password passwordHash = this.encryptionHandler.hashPassword(password, salt); + + history.setLastPasswordChange(ZonedDateTime.now()); } // create new user - User newUser = createUser(userRep, passwordHash, salt); + User newUser = createUser(userRep, history, passwordHash, salt); // detect privilege conflicts assertNoPrivilegeConflict(newUser); @@ -458,6 +458,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { throw new PrivilegeModelException(MessageFormat.format(msg, userRep.getUsername())); } + UserHistory history = existingUser.getHistory().getClone(); byte[] passwordHash = null; byte[] salt = null; if (password != null) { @@ -470,9 +471,11 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // hash password passwordHash = this.encryptionHandler.hashPassword(password, salt); + + history.setLastPasswordChange(ZonedDateTime.now()); } - User newUser = createUser(userRep, passwordHash, salt); + User newUser = createUser(userRep, history, passwordHash, salt); // detect privilege conflicts assertNoPrivilegeConflict(newUser); @@ -503,7 +506,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { } } - private User createUser(UserRep userRep, byte[] passwordHash, byte[] salt) { + private User createUser(UserRep userRep, UserHistory history, byte[] passwordHash, byte[] salt) { String userId = userRep.getUserId(); String userName = userRep.getUsername(); String firstName = userRep.getFirstname(); @@ -514,7 +517,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { Map properties = userRep.getProperties(); return new User(userId, userName, passwordHash, salt, this.encryptionHandler.getAlgorithm(), this.encryptionHandler.getIterations(), this.encryptionHandler.getKeyLength(), firstName, lastName, - state, roles, locale, properties); + state, roles, locale, properties, history); } @Override @@ -573,7 +576,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // create new user User newUser = new User(userId, username, password, salt, hashAlgorithm, hashIterations, hashKeyLength, - firstName, lastName, userState, roles, locale, propertyMap); + firstName, lastName, userState, roles, locale, propertyMap, existingUser.getHistory().getClone()); // detect privilege conflicts assertNoPrivilegeConflict(newUser); @@ -654,7 +657,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { User newUser = new User(existingUser.getUserId(), existingUser.getUsername(), existingUser.getPassword(), existingUser.getSalt(), existingUser.getHashAlgorithm(), existingUser.getHashIterations(), existingUser.getHashKeyLength(), existingUser.getFirstname(), existingUser.getLastname(), - existingUser.getUserState(), newRoles, existingUser.getLocale(), existingUser.getProperties()); + existingUser.getUserState(), newRoles, existingUser.getLocale(), existingUser.getProperties(), + existingUser.getHistory().getClone()); // detect privilege conflicts assertNoPrivilegeConflict(newUser); @@ -701,7 +705,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { User newUser = new User(existingUser.getUserId(), existingUser.getUsername(), existingUser.getPassword(), existingUser.getSalt(), existingUser.getHashAlgorithm(), existingUser.getHashIterations(), existingUser.getHashKeyLength(), existingUser.getFirstname(), existingUser.getLastname(), - existingUser.getUserState(), newRoles, existingUser.getLocale(), existingUser.getProperties()); + existingUser.getUserState(), newRoles, existingUser.getLocale(), existingUser.getProperties(), + existingUser.getHistory().getClone()); // delegate user replacement to persistence handler this.persistenceHandler.replaceUser(newUser); @@ -731,7 +736,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { User newUser = new User(existingUser.getUserId(), existingUser.getUsername(), existingUser.getPassword(), existingUser.getSalt(), existingUser.getHashAlgorithm(), existingUser.getHashIterations(), existingUser.getHashKeyLength(), existingUser.getFirstname(), existingUser.getLastname(), - existingUser.getUserState(), existingUser.getRoles(), locale, existingUser.getProperties()); + existingUser.getUserState(), existingUser.getRoles(), locale, existingUser.getProperties(), + existingUser.getHistory().getClone()); // if the user is not setting their own locale, then make sure this user may set this user's locale if (!certificate.getUsername().equals(username)) { @@ -766,6 +772,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { MessageFormat.format("User {0} does not exist!", username)); //$NON-NLS-1$ } + UserHistory history = existingUser.getHistory().getClone(); + byte[] passwordHash = null; byte[] salt = null; if (password != null) { @@ -778,6 +786,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // hash password passwordHash = this.encryptionHandler.hashPassword(password, salt); + + history.setLastPasswordChange(ZonedDateTime.now()); } // create new user @@ -785,7 +795,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { this.encryptionHandler.getAlgorithm(), this.encryptionHandler.getIterations(), this.encryptionHandler.getKeyLength(), existingUser.getFirstname(), existingUser.getLastname(), existingUser.getUserState(), existingUser.getRoles(), existingUser.getLocale(), - existingUser.getProperties()); + existingUser.getProperties(), history); if (!certificate.getUsername().equals(username)) { // check that the user may change their own password @@ -821,15 +831,15 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // get User User existingUser = this.persistenceHandler.getUser(username); - if (existingUser == null) { + if (existingUser == null) throw new PrivilegeModelException(MessageFormat.format("User {0} does not exist!", username)); //$NON-NLS-1$ - } // create new user User newUser = new User(existingUser.getUserId(), existingUser.getUsername(), existingUser.getPassword(), existingUser.getSalt(), existingUser.getHashAlgorithm(), existingUser.getHashIterations(), existingUser.getHashKeyLength(), existingUser.getFirstname(), existingUser.getLastname(), state, - existingUser.getRoles(), existingUser.getLocale(), existingUser.getProperties()); + existingUser.getRoles(), existingUser.getLocale(), existingUser.getProperties(), + existingUser.getHistory().getClone()); // validate that this user may modify this user's state prvCtx.validateAction(new SimpleRestrictable(PRIVILEGE_SET_USER_STATE, new Tuple(existingUser, newUser))); @@ -1159,7 +1169,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // create a new certificate, with details of the user Usage usage = userChallenge.getUsage(); Certificate certificate = buildCertificate(usage, user, authToken, sessionId, userChallenge.getSource(), - LocalDateTime.now(), false); + ZonedDateTime.now(), false); PrivilegeContext privilegeContext = buildPrivilegeContext(certificate, user); this.privilegeContextMap.put(sessionId, privilegeContext); @@ -1197,10 +1207,9 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // validate user has at least one role Set userRoles = user.getRoles(); - if (userRoles.isEmpty()) { + if (userRoles.isEmpty()) throw new InvalidCredentialsException( MessageFormat.format("User {0} does not have any roles defined!", username)); //$NON-NLS-1$ - } // get 2 auth tokens String authToken = this.encryptionHandler.nextToken(); @@ -1209,7 +1218,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { String sessionId = UUID.randomUUID().toString(); // create a new certificate, with details of the user - Certificate certificate = buildCertificate(usage, user, authToken, sessionId, source, LocalDateTime.now(), + Certificate certificate = buildCertificate(usage, user, authToken, sessionId, source, ZonedDateTime.now(), keepAlive); PrivilegeContext privilegeContext = buildPrivilegeContext(certificate, user); @@ -1217,6 +1226,14 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { persistSessions(); + // save last login + if (user.getHistory().isFirstLoginEmpty()) + user.getHistory().setFirstLogin(ZonedDateTime.now()); + user.getHistory().setLastLogin(ZonedDateTime.now()); + this.persistenceHandler.replaceUser(user); + if (this.autoPersistOnUserChangesData) + this.persistenceHandler.persist(); + // log logger.info(MessageFormat.format("User {0} authenticated: {1}", username, certificate)); //$NON-NLS-1$ @@ -1249,13 +1266,17 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { User user = this.ssoHandler.authenticateSingleSignOn(data); DBC.PRE.assertEquals("SSO Users must have UserState.REMOTE!", UserState.REMOTE, user.getUserState()); + user.getHistory().setLastLogin(ZonedDateTime.now()); // persist this user User internalUser = this.persistenceHandler.getUser(user.getUsername()); - if (internalUser == null) + if (internalUser == null) { + user.getHistory().setFirstLogin(ZonedDateTime.now()); this.persistenceHandler.addUser(user); - else + } else { + user.getHistory().setFirstLogin(internalUser.getHistory().getFirstLogin()); this.persistenceHandler.replaceUser(user); + } if (this.autoPersistOnUserChangesData) this.persistenceHandler.persist(); @@ -1267,7 +1288,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { String sessionId = UUID.randomUUID().toString(); // create a new certificate, with details of the user - Certificate certificate = buildCertificate(Usage.ANY, user, authToken, sessionId, source, LocalDateTime.now(), + Certificate certificate = buildCertificate(Usage.ANY, user, authToken, sessionId, source, ZonedDateTime.now(), keepAlive); PrivilegeContext privilegeContext = buildPrivilegeContext(certificate, user); @@ -1311,7 +1332,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // create a new certificate, with details of the user Certificate refreshedCert = buildCertificate(certificate.getUsage(), user, authToken, sessionId, source, - LocalDateTime.now(), true); + ZonedDateTime.now(), true); PrivilegeContext privilegeContext = buildPrivilegeContext(refreshedCert, user); this.privilegeContextMap.put(sessionId, privilegeContext); @@ -1339,7 +1360,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { } private Certificate buildCertificate(Usage usage, User user, String authToken, String sessionId, String source, - LocalDateTime loginTime, boolean keepAlive) { + ZonedDateTime loginTime, boolean keepAlive) { DBC.PRE.assertNotEmpty("source must not be empty!", source); Set userRoles = user.getRoles(); return new Certificate(usage, sessionId, user.getUsername(), user.getFirstname(), user.getLastname(), @@ -1527,7 +1548,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { User newUser = new User(user.getUserId(), user.getUsername(), passwordHash, salt, this.encryptionHandler.getAlgorithm(), this.encryptionHandler.getIterations(), this.encryptionHandler.getKeyLength(), user.getFirstname(), user.getLastname(), user.getUserState(), - user.getRoles(), user.getLocale(), user.getProperties()); + user.getRoles(), user.getLocale(), user.getProperties(), user.getHistory().getClone()); // delegate user replacement to persistence handler this.persistenceHandler.replaceUser(newUser); @@ -1685,7 +1706,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { throw new PrivilegeException(msg); } - certificate.setLastAccess(LocalDateTime.now()); + certificate.setLastAccess(ZonedDateTime.now()); if (!certificate.getSource().equals(this.identifier)) throw new IllegalStateException( @@ -1717,14 +1738,14 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // validate that challenge certificate is not expired (1 hour only) if (sessionCertificate.getUsage() != Usage.ANY) { - LocalDateTime dateTime = sessionCertificate.getLoginTime(); - if (dateTime.plusHours(1).isBefore(LocalDateTime.now())) { + ZonedDateTime dateTime = sessionCertificate.getLoginTime(); + if (dateTime.plusHours(1).isBefore(ZonedDateTime.now())) { invalidate(sessionCertificate); throw new NotAuthenticatedException("Certificate has already expired!"); //$NON-NLS-1$ } } - certificate.setLastAccess(LocalDateTime.now()); + certificate.setLastAccess(ZonedDateTime.now()); // TODO decide if we want to assert source did not change! // if (!source.equals(SOURCE_UNKNOWN) && !certificate.getSource().equals(source)) { @@ -2150,7 +2171,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // create a new certificate, with details of the user Certificate systemUserCertificate = buildCertificate(Usage.ANY, user, authToken, sessionId, this.identifier, - LocalDateTime.now(), false); + ZonedDateTime.now(), false); // create and save a new privilege context PrivilegeContext privilegeContext = buildPrivilegeContext(systemUserCertificate, user); diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/XmlConstants.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/XmlConstants.java index f2a4ee4b1..c6252492c 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/XmlConstants.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/XmlConstants.java @@ -108,6 +108,26 @@ public class XmlConstants { */ public static final String XML_USER = "User"; + /** + * XML_USER = "User" + */ + public static final String XML_HISTORY = "History"; + + /** + * XML_USER = "User" + */ + public static final String XML_FIRST_LOGIN = "FirstLogin"; + + /** + * XML_USER = "User" + */ + public static final String XML_LAST_LOGIN = "LastLogin"; + + /** + * XML_USER = "User" + */ + public static final String XML_LAST_PASSWORD_CHANGE = "LastPasswordChange"; + /** * XML_PRIVILEGE = "Privilege" : */ diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/Certificate.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/Certificate.java index a50cfe273..6f48ead8c 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/Certificate.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/Certificate.java @@ -18,7 +18,7 @@ package li.strolch.privilege.model; import static li.strolch.privilege.base.PrivilegeConstants.*; import java.io.Serializable; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.Locale; import java.util.Map; @@ -47,14 +47,14 @@ public final class Certificate implements Serializable { private final UserState userState; private final String authToken; private final String source; - private final LocalDateTime loginTime; + private final ZonedDateTime loginTime; private final boolean keepAlive; private final Set userRoles; private final Map propertyMap; private Locale locale; - private LocalDateTime lastAccess; + private ZonedDateTime lastAccess; /** * Default constructor initializing with all information needed for this certificate @@ -85,7 +85,7 @@ public final class Certificate implements Serializable { * edited and can be used for the user to change settings of this session */ public Certificate(Usage usage, String sessionId, String username, String firstName, String lastName, - UserState userState, String authToken, String source, LocalDateTime loginTime, boolean keepAlive, + UserState userState, String authToken, String source, ZonedDateTime loginTime, boolean keepAlive, Locale locale, Set userRoles, Map propertyMap) { // validate arguments are not null @@ -131,7 +131,7 @@ public final class Certificate implements Serializable { this.propertyMap = Collections.unmodifiableMap(propertyMap); this.userRoles = Collections.unmodifiableSet(userRoles); - this.lastAccess = LocalDateTime.now(); + this.lastAccess = ZonedDateTime.now(); } public Usage getUsage() { @@ -239,7 +239,7 @@ public final class Certificate implements Serializable { return userState; } - public LocalDateTime getLoginTime() { + public ZonedDateTime getLoginTime() { return this.loginTime; } @@ -255,11 +255,11 @@ public final class Certificate implements Serializable { return this.source; } - public LocalDateTime getLastAccess() { + public ZonedDateTime getLastAccess() { return this.lastAccess; } - public void setLastAccess(LocalDateTime lastAccess) { + public void setLastAccess(ZonedDateTime lastAccess) { this.lastAccess = lastAccess; } diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/User.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/User.java index 735dbac2c..289476ccc 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/User.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/User.java @@ -50,14 +50,14 @@ public final class User { private final String firstname; private final String lastname; - private final UserState userState; - private final Set roles; + private final UserState userState; private final Map propertyMap; - private final Locale locale; + private final UserHistory history; + /** * Default constructor * @@ -90,26 +90,24 @@ public final class User { */ public User(String userId, String username, byte[] password, byte[] salt, String hashAlgorithm, int hashIterations, int hashKeyLength, String firstname, String lastname, UserState userState, Set roles, Locale locale, - Map propertyMap) { + Map propertyMap, UserHistory history) { - if (StringHelper.isEmpty(userId)) { + if (StringHelper.isEmpty(userId)) throw new PrivilegeException("No UserId defined!"); //$NON-NLS-1$ - } - if (userState == null) { + if (userState == null) throw new PrivilegeException("No userState defined!"); //$NON-NLS-1$ - } - if (StringHelper.isEmpty(username)) { + if (StringHelper.isEmpty(username)) throw new PrivilegeException("No username defined!"); //$NON-NLS-1$ - } if (userState != UserState.SYSTEM) { - if (StringHelper.isEmpty(lastname)) { + if (StringHelper.isEmpty(lastname)) throw new PrivilegeException("No lastname defined!"); //$NON-NLS-1$ - } - if (StringHelper.isEmpty(firstname)) { + if (StringHelper.isEmpty(firstname)) throw new PrivilegeException("No firstname defined!"); //$NON-NLS-1$ - } } + if (history == null) + throw new PrivilegeException("History must not be null!"); + // password, salt and hash* may be null, meaning not able to login // roles may be null, meaning not able to login and must be added later // locale may be null, meaning use system default @@ -133,7 +131,7 @@ public final class User { if (roles == null) this.roles = Collections.emptySet(); else - this.roles = Collections.unmodifiableSet(new HashSet<>(roles)); + this.roles = Set.copyOf(roles); if (locale == null) this.locale = Locale.getDefault(); @@ -143,7 +141,9 @@ public final class User { if (propertyMap == null) this.propertyMap = Collections.emptyMap(); else - this.propertyMap = Collections.unmodifiableMap(new HashMap<>(propertyMap)); + this.propertyMap = Map.copyOf(propertyMap); + + this.history = history; } /** @@ -252,6 +252,24 @@ public final class User { return this.locale; } + /** + * Returns the History object + * + * @return the History object + */ + public UserHistory getHistory() { + return this.history; + } + + /** + * Returns true if the history for this user is empty + * + * @return true if the history for this user is empty + */ + public boolean isHistoryEmpty() { + return this.history.isEmpty(); + } + /** * Returns the property with the given key * diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/UserHistory.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/UserHistory.java new file mode 100644 index 000000000..19f209ae2 --- /dev/null +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/UserHistory.java @@ -0,0 +1,66 @@ +package li.strolch.privilege.model.internal; + +import java.time.ZonedDateTime; + +import li.strolch.utils.iso8601.ISO8601; + +public class UserHistory { + + private ZonedDateTime firstLogin; + private ZonedDateTime lastLogin; + private ZonedDateTime lastPasswordChange; + + public UserHistory() { + this.firstLogin = ISO8601.EMPTY_VALUE_ZONED_DATE; + this.lastLogin = ISO8601.EMPTY_VALUE_ZONED_DATE; + this.lastPasswordChange = ISO8601.EMPTY_VALUE_ZONED_DATE; + } + + public ZonedDateTime getFirstLogin() { + return this.firstLogin; + } + + public boolean isFirstLoginEmpty() { + return this.firstLogin.equals(ISO8601.EMPTY_VALUE_ZONED_DATE); + } + + public void setFirstLogin(ZonedDateTime firstLogin) { + this.firstLogin = firstLogin; + } + + public ZonedDateTime getLastLogin() { + return this.lastLogin; + } + + public boolean isLastLoginEmpty() { + return this.lastLogin.equals(ISO8601.EMPTY_VALUE_ZONED_DATE); + } + + public void setLastLogin(ZonedDateTime lastLogin) { + this.lastLogin = lastLogin; + } + + public ZonedDateTime getLastPasswordChange() { + return this.lastPasswordChange; + } + + public boolean isLastPasswordChangeEmpty() { + return this.lastPasswordChange.equals(ISO8601.EMPTY_VALUE_ZONED_DATE); + } + + public void setLastPasswordChange(ZonedDateTime lastPasswordChange) { + this.lastPasswordChange = lastPasswordChange; + } + + public boolean isEmpty() { + return isFirstLoginEmpty() && isLastLoginEmpty() && isLastPasswordChangeEmpty(); + } + + public UserHistory getClone() { + UserHistory clone = new UserHistory(); + clone.firstLogin = this.firstLogin; + clone.lastLogin = this.lastLogin; + clone.lastPasswordChange = this.lastPasswordChange; + return clone; + } +} diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsSaxReader.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsSaxReader.java index 9f4557a40..2785a7a7e 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsSaxReader.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsSaxReader.java @@ -20,7 +20,7 @@ import static li.strolch.privilege.helper.XmlConstants.*; import static li.strolch.utils.helper.StringHelper.isEmpty; import java.io.InputStream; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -67,8 +67,8 @@ public class CertificateStubsSaxReader extends DefaultHandler { stub.authToken = attributes.getValue(XML_ATTR_AUTH_TOKEN); stub.source = attributes.getValue(XML_ATTR_SOURCE); stub.locale = Locale.forLanguageTag(attributes.getValue(XML_ATTR_LOCALE)); - stub.loginTime = ISO8601.parseToZdt(attributes.getValue(XML_ATTR_LOGIN_TIME)).toLocalDateTime(); - stub.lastAccess = ISO8601.parseToZdt(attributes.getValue(XML_ATTR_LAST_ACCESS)).toLocalDateTime(); + stub.loginTime = ISO8601.parseToZdt(attributes.getValue(XML_ATTR_LOGIN_TIME)); + stub.lastAccess = ISO8601.parseToZdt(attributes.getValue(XML_ATTR_LAST_ACCESS)); stub.keepAlive = Boolean.parseBoolean(attributes.getValue(XML_ATTR_KEEP_ALIVE)); DBC.INTERIM.assertNotEmpty("sessionId missing on sessions data!", stub.sessionId); @@ -93,8 +93,8 @@ public class CertificateStubsSaxReader extends DefaultHandler { private String authToken; private String source; private Locale locale; - private LocalDateTime loginTime; - private LocalDateTime lastAccess; + private ZonedDateTime loginTime; + private ZonedDateTime lastAccess; private boolean keepAlive; public Usage getUsage() { @@ -121,11 +121,11 @@ public class CertificateStubsSaxReader extends DefaultHandler { return locale; } - public LocalDateTime getLoginTime() { + public ZonedDateTime getLoginTime() { return loginTime; } - public LocalDateTime getLastAccess() { + public ZonedDateTime getLastAccess() { return lastAccess; } diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/PrivilegeUsersDomWriter.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/PrivilegeUsersDomWriter.java index 8f2d4bbec..be32b4a02 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/PrivilegeUsersDomWriter.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/PrivilegeUsersDomWriter.java @@ -16,16 +16,18 @@ package li.strolch.privilege.xml; import static java.util.Comparator.comparing; +import static li.strolch.privilege.helper.CryptHelper.buildPasswordString; import java.io.File; import java.util.List; import java.util.Map; -import li.strolch.privilege.helper.CryptHelper; import li.strolch.privilege.helper.XmlConstants; import li.strolch.privilege.model.internal.User; +import li.strolch.privilege.model.internal.UserHistory; import li.strolch.utils.helper.StringHelper; import li.strolch.utils.helper.XmlHelper; +import li.strolch.utils.iso8601.ISO8601; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -34,8 +36,8 @@ import org.w3c.dom.Element; */ public class PrivilegeUsersDomWriter { - private List users; - private File modelFile; + private final List users; + private final File modelFile; public PrivilegeUsersDomWriter(List users, File modelFile) { this.users = users; @@ -98,13 +100,37 @@ public class PrivilegeUsersDomWriter { if (!user.getProperties().isEmpty()) { Element parametersElement = doc.createElement(XmlConstants.XML_PROPERTIES); userElement.appendChild(parametersElement); - user.getProperties().entrySet().stream().sorted(comparing(Map.Entry::getKey)).forEach(entry -> { + user.getProperties().entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> { Element paramElement = doc.createElement(XmlConstants.XML_PROPERTY); paramElement.setAttribute(XmlConstants.XML_ATTR_NAME, entry.getKey()); paramElement.setAttribute(XmlConstants.XML_ATTR_VALUE, entry.getValue()); parametersElement.appendChild(paramElement); }); } + + if (!user.isHistoryEmpty()) { + UserHistory history = user.getHistory(); + Element historyElement = doc.createElement(XmlConstants.XML_HISTORY); + userElement.appendChild(historyElement); + + if (!history.isFirstLoginEmpty()) { + Element element = doc.createElement(XmlConstants.XML_FIRST_LOGIN); + element.setTextContent(ISO8601.toString(history.getFirstLogin())); + historyElement.appendChild(element); + } + + if (!history.isLastLoginEmpty()) { + Element element = doc.createElement(XmlConstants.XML_LAST_LOGIN); + element.setTextContent(ISO8601.toString(history.getLastLogin())); + historyElement.appendChild(element); + } + + if (!history.isLastPasswordChangeEmpty()) { + Element element = doc.createElement(XmlConstants.XML_LAST_PASSWORD_CHANGE); + element.setTextContent(ISO8601.toString(history.getLastPasswordChange())); + historyElement.appendChild(element); + } + } }); // write the container file to disk @@ -112,15 +138,10 @@ public class PrivilegeUsersDomWriter { } private void writePassword(User user, Element userElement) { - if (user.getPassword() != null && user.getSalt() != null && user.getHashAlgorithm() != null && user.getHashIterations() != -1 && user.getHashKeyLength() != -1) { - - String passwordS = CryptHelper.buildPasswordString(user); - userElement.setAttribute(XmlConstants.XML_ATTR_PASSWORD, passwordS); - + userElement.setAttribute(XmlConstants.XML_ATTR_PASSWORD, buildPasswordString(user)); } else { - if (user.getPassword() != null) userElement.setAttribute(XmlConstants.XML_ATTR_PASSWORD, StringHelper.toHexString(user.getPassword())); if (user.getSalt() != null) diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/PrivilegeUsersSaxReader.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/PrivilegeUsersSaxReader.java index 4f60b4a5e..b572290b2 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/PrivilegeUsersSaxReader.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/PrivilegeUsersSaxReader.java @@ -15,13 +15,16 @@ */ package li.strolch.privilege.xml; +import static li.strolch.privilege.helper.XmlConstants.*; + import java.text.MessageFormat; import java.util.*; -import li.strolch.privilege.helper.XmlConstants; import li.strolch.privilege.model.UserState; import li.strolch.privilege.model.internal.User; +import li.strolch.privilege.model.internal.UserHistory; import li.strolch.utils.helper.StringHelper; +import li.strolch.utils.iso8601.ISO8601; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; @@ -35,9 +38,9 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { protected static final Logger logger = LoggerFactory.getLogger(PrivilegeUsersSaxReader.class); - private Deque buildersStack = new ArrayDeque<>(); + private final Deque buildersStack = new ArrayDeque<>(); - private List users; + private final List users; public PrivilegeUsersSaxReader() { this.users = new ArrayList<>(); @@ -52,9 +55,9 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - if (qName.equals(XmlConstants.XML_USER)) { + if (qName.equals(XML_USER)) { this.buildersStack.push(new UserParser()); - } else if (qName.equals(XmlConstants.XML_PROPERTIES)) { + } else if (qName.equals(XML_PROPERTIES)) { this.buildersStack.push(new PropertyParser()); } @@ -75,9 +78,9 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { this.buildersStack.peek().endElement(uri, localName, qName); ElementParser elementParser = null; - if (qName.equals(XmlConstants.XML_USER)) { + if (qName.equals(XML_USER)) { elementParser = this.buildersStack.pop(); - } else if (qName.equals(XmlConstants.XML_PROPERTIES)) { + } else if (qName.equals(XML_PROPERTIES)) { elementParser = this.buildersStack.pop(); } @@ -98,6 +101,11 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { // // // +// +// 2021-02-19T15:32:09.592+01:00 +// 2021-02-19T15:32:09.592+01:00 +// 2021-02-19T15:32:09.592+01:00 +// // public class UserParser extends ElementParserAdapter { @@ -117,6 +125,7 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { Locale locale; Set userRoles; Map parameters; + UserHistory history; public UserParser() { this.userRoles = new HashSet<>(); @@ -128,13 +137,15 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { this.text = new StringBuilder(); - if (qName.equals(XmlConstants.XML_USER)) { - this.userId = attributes.getValue(XmlConstants.XML_ATTR_USER_ID); - this.username = attributes.getValue(XmlConstants.XML_ATTR_USERNAME); + if (qName.equals(XML_USER)) { + this.userId = attributes.getValue(XML_ATTR_USER_ID); + this.username = attributes.getValue(XML_ATTR_USERNAME); - String password = attributes.getValue(XmlConstants.XML_ATTR_PASSWORD); - String salt = attributes.getValue(XmlConstants.XML_ATTR_SALT); + String password = attributes.getValue(XML_ATTR_PASSWORD); + String salt = attributes.getValue(XML_ATTR_SALT); parsePassword(password, salt); + } else if (qName.equals(XML_HISTORY)) { + this.history = new UserHistory(); } } @@ -183,36 +194,54 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { public void endElement(String uri, String localName, String qName) throws SAXException { switch (qName) { - case XmlConstants.XML_FIRSTNAME: + case XML_FIRSTNAME: this.firstName = this.text.toString().trim(); break; - case XmlConstants.XML_LASTNAME: + case XML_LASTNAME: this.lastname = this.text.toString().trim(); break; - case XmlConstants.XML_STATE: + case XML_STATE: this.userState = UserState.valueOf(this.text.toString().trim()); break; - case XmlConstants.XML_LOCALE: + case XML_LOCALE: this.locale = Locale.forLanguageTag(this.text.toString().trim()); break; - case XmlConstants.XML_ROLE: + case XML_FIRST_LOGIN: + + this.history.setFirstLogin(ISO8601.parseToZdt(this.text.toString().trim())); + break; + + case XML_LAST_LOGIN: + + this.history.setLastLogin(ISO8601.parseToZdt(this.text.toString().trim())); + break; + + case XML_LAST_PASSWORD_CHANGE: + + this.history.setLastPasswordChange(ISO8601.parseToZdt(this.text.toString().trim())); + break; + + case XML_ROLE: this.userRoles.add(this.text.toString().trim()); break; - case XmlConstants.XML_USER: + case XML_USER: + + if (this.history == null) + this.history = new UserHistory(); User user = new User(this.userId, this.username, this.password, this.salt, this.hashAlgorithm, hashIterations, hashKeyLength, this.firstName, this.lastname, this.userState, this.userRoles, - this.locale, this.parameters); + this.locale, this.parameters, this.history); logger.info(MessageFormat.format("New User: {0}", user)); //$NON-NLS-1$ getUsers().add(user); @@ -220,9 +249,10 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { default: - if (!(qName.equals(XmlConstants.XML_ROLES) // - || qName.equals(XmlConstants.XML_PARAMETER) // - || qName.equals(XmlConstants.XML_PARAMETERS))) { + if (!(qName.equals(XML_ROLES) // + || qName.equals(XML_PARAMETER) // + || qName.equals(XML_HISTORY) // + || qName.equals(XML_PARAMETERS))) { throw new IllegalArgumentException("Unhandled tag " + qName); } @@ -238,7 +268,7 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { } } - class PropertyParser extends ElementParserAdapter { + static class PropertyParser extends ElementParserAdapter { // @@ -249,16 +279,16 @@ public class PrivilegeUsersSaxReader extends DefaultHandler { throws SAXException { switch (qName) { - case XmlConstants.XML_PROPERTY: + case XML_PROPERTY: - String key = attributes.getValue(XmlConstants.XML_ATTR_NAME); - String value = attributes.getValue(XmlConstants.XML_ATTR_VALUE); + String key = attributes.getValue(XML_ATTR_NAME); + String value = attributes.getValue(XML_ATTR_VALUE); this.parameterMap.put(key, value); break; default: - if (!qName.equals(XmlConstants.XML_PROPERTIES)) { + if (!qName.equals(XML_PROPERTIES)) { throw new IllegalArgumentException("Unhandled tag " + qName); } } diff --git a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/XmlTest.java b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/XmlTest.java index 520a4a1fe..3eebd7618 100644 --- a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/XmlTest.java +++ b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/XmlTest.java @@ -19,6 +19,9 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.*; import java.io.File; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.*; import li.strolch.privilege.handler.DefaultEncryptionHandler; @@ -27,10 +30,7 @@ import li.strolch.privilege.handler.PrivilegeHandler; import li.strolch.privilege.handler.XmlPersistenceHandler; import li.strolch.privilege.model.IPrivilege; import li.strolch.privilege.model.UserState; -import li.strolch.privilege.model.internal.PrivilegeContainerModel; -import li.strolch.privilege.model.internal.PrivilegeImpl; -import li.strolch.privilege.model.internal.Role; -import li.strolch.privilege.model.internal.User; +import li.strolch.privilege.model.internal.*; import li.strolch.privilege.test.model.DummySsoHandler; import li.strolch.privilege.xml.*; import li.strolch.utils.helper.FileHelper; @@ -316,16 +316,21 @@ public class XmlTest { propertyMap.put("prop1", "value1"); userRoles = new HashSet<>(); userRoles.add("role1"); + UserHistory history = new UserHistory(); + history.setFirstLogin(ZonedDateTime.of(LocalDateTime.of(2020, 1, 2, 2, 3, 4, 5), ZoneId.systemDefault())); User user1 = new User("1", "user1", "blabla".getBytes(), "blabla".getBytes(), "PBKDF2WithHmacSHA512", 10000, - 256, "Bob", "White", UserState.DISABLED, userRoles, Locale.ENGLISH, propertyMap); + 256, "Bob", "White", UserState.DISABLED, userRoles, Locale.ENGLISH, propertyMap, history); users.add(user1); propertyMap = new HashMap<>(); propertyMap.put("prop2", "value2"); userRoles = new HashSet<>(); userRoles.add("role2"); + history = new UserHistory(); + history.setFirstLogin(ZonedDateTime.of(LocalDateTime.of(2020, 1, 2, 2, 3, 4, 5), ZoneId.systemDefault())); + history.setLastLogin(ZonedDateTime.of(LocalDateTime.of(2020, 1, 5, 2, 3, 4, 5), ZoneId.systemDefault())); User user2 = new User("2", "user2", "haha".getBytes(), "haha".getBytes(), null, -1, -1, "Leonard", "Sheldon", - UserState.ENABLED, userRoles, Locale.ENGLISH, propertyMap); + UserState.ENABLED, userRoles, Locale.ENGLISH, propertyMap, history); users.add(user2); File modelFile = new File(TARGET_TEST + "PrivilegeUsersTest.xml"); diff --git a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/model/DummySsoHandler.java b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/model/DummySsoHandler.java index 04339433f..b5706dd63 100644 --- a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/model/DummySsoHandler.java +++ b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/model/DummySsoHandler.java @@ -7,6 +7,7 @@ import li.strolch.privilege.base.PrivilegeException; import li.strolch.privilege.handler.SingleSignOnHandler; import li.strolch.privilege.model.UserState; import li.strolch.privilege.model.internal.User; +import li.strolch.privilege.model.internal.UserHistory; public class DummySsoHandler implements SingleSignOnHandler { @@ -31,6 +32,6 @@ public class DummySsoHandler implements SingleSignOnHandler { Set roles = Arrays.stream(map.get("roles").split(",")).map(String::trim).collect(Collectors.toSet()); Map properties = new HashMap<>(); return new User(map.get("userId"), map.get("username"), null, null, null, -1, -1, map.get("firstName"), - map.get("lastName"), UserState.REMOTE, roles, Locale.ENGLISH, properties); + map.get("lastName"), UserState.REMOTE, roles, Locale.ENGLISH, properties, new UserHistory()); } } diff --git a/li.strolch.rest/src/main/java/li/strolch/rest/DefaultStrolchSessionHandler.java b/li.strolch.rest/src/main/java/li/strolch/rest/DefaultStrolchSessionHandler.java index 2e8600b73..2d189d24e 100644 --- a/li.strolch.rest/src/main/java/li/strolch/rest/DefaultStrolchSessionHandler.java +++ b/li.strolch.rest/src/main/java/li/strolch/rest/DefaultStrolchSessionHandler.java @@ -19,7 +19,7 @@ import static li.strolch.runtime.StrolchConstants.StrolchPrivilegeConstants.PRIV import static li.strolch.runtime.StrolchConstants.StrolchPrivilegeConstants.PRIVILEGE_INVALIDATE_SESSION; import java.text.MessageFormat; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.Future; @@ -313,8 +313,8 @@ public class DefaultStrolchSessionHandler extends StrolchComponent implements St certificateMap = new HashMap<>(this.certificateMap); } - LocalDateTime maxKeepAliveTime = LocalDateTime.now().minus(this.maxKeepAliveMinutes, ChronoUnit.MINUTES); - LocalDateTime timeOutTime = LocalDateTime.now().minus(this.sessionTtlMinutes, ChronoUnit.MINUTES); + ZonedDateTime maxKeepAliveTime = ZonedDateTime.now().minus(this.maxKeepAliveMinutes, ChronoUnit.MINUTES); + ZonedDateTime timeOutTime = ZonedDateTime.now().minus(this.sessionTtlMinutes, ChronoUnit.MINUTES); for (Certificate certificate : certificateMap.values()) { if (certificate.isKeepAlive()) { diff --git a/li.strolch.rest/src/main/java/li/strolch/rest/model/UserSession.java b/li.strolch.rest/src/main/java/li/strolch/rest/model/UserSession.java index 8c43c7d2d..c39aef21f 100644 --- a/li.strolch.rest/src/main/java/li/strolch/rest/model/UserSession.java +++ b/li.strolch.rest/src/main/java/li/strolch/rest/model/UserSession.java @@ -15,7 +15,7 @@ */ package li.strolch.rest.model; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.Locale; import java.util.Set; @@ -28,14 +28,14 @@ public class UserSession { private final boolean keepAlive; private final String sessionId; - private final LocalDateTime loginTime; + private final ZonedDateTime loginTime; private final String username; private final String firstName; private final String lastName; private final String source; private final Set userRoles; private final Locale locale; - private final LocalDateTime lastAccess; + private final ZonedDateTime lastAccess; public UserSession(Certificate certificate) { this.sessionId = certificate.getSessionId(); @@ -54,7 +54,7 @@ public class UserSession { return locale; } - public LocalDateTime getLastAccess() { + public ZonedDateTime getLastAccess() { return lastAccess; } @@ -62,7 +62,7 @@ public class UserSession { return sessionId; } - public LocalDateTime getLoginTime() { + public ZonedDateTime getLoginTime() { return loginTime; } diff --git a/li.strolch.service/src/test/java/li/strolch/service/test/ServiceTest.java b/li.strolch.service/src/test/java/li/strolch/service/test/ServiceTest.java index 943378ff8..08b52bb16 100644 --- a/li.strolch.service/src/test/java/li/strolch/service/test/ServiceTest.java +++ b/li.strolch.service/src/test/java/li/strolch/service/test/ServiceTest.java @@ -19,7 +19,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThrows; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.HashSet; import li.strolch.privilege.base.AccessDeniedException; @@ -45,7 +45,7 @@ public class ServiceTest extends AbstractServiceTest { assertThrows(PrivilegeException.class, () -> { TestService testService = new TestService(); getServiceHandler().doService( - new Certificate(null, null, null, null, null, null, null, null, LocalDateTime.now(), false, null, + new Certificate(null, null, null, null, null, null, null, null, ZonedDateTime.now(), false, null, new HashSet<>(), null), testService); }); } @@ -54,7 +54,7 @@ public class ServiceTest extends AbstractServiceTest { public void shouldFailInvalidCertificate2() { TestService testService = new TestService(); Certificate badCert = new Certificate(Usage.ANY, "1", "bob", "Bob", "Brown", UserState.ENABLED, "dsdf", "asd", - LocalDateTime.now(), false, null, new HashSet<>(), null); + ZonedDateTime.now(), false, null, new HashSet<>(), null); ServiceResult svcResult = getServiceHandler().doService(badCert, testService); assertThat(svcResult.getThrowable(), instanceOf(NotAuthenticatedException.class)); }