refactored User-related methods on SecurityService to avoid sending User objects through replication operations;

this way, no patching of the userGroupProvider property after
de-serialization is necessary, and no replacement of a User object in
the UserStore should ever happen.

Change-Id: I23a8ae53b6a53ca89a2a0b8a11dc779f4034db57
This commit is contained in:
Axel Uhl
2019-02-21 13:21:04 +01:00
parent e64e708008
commit 7bfddc6926
12 changed files with 277 additions and 243 deletions
@@ -34,7 +34,7 @@ public interface OperationWithResult<S, R> extends Operation<S>, Serializable {
* Tells whether this operation requires explicit transitive replication to other replicas when received by a
* replica. This is the case for all operations whose {@link #internalApplyTo(Object)} method will not trigger
* replication. An example for an operation that does <em>not</em> require explicit transitive replication is the
* insertion of a GPS fix into a competitor's track because the insertion of the track, as implemented by
* insertion of a GPS fix into a competitor's track because the insertion of the fix, as implemented by
* {@link #internalApplyTo} will trigger the replication.
* <p>
*
@@ -0,0 +1,170 @@
package com.sap.sse.security.shared;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.sap.sse.common.Named;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.security.shared.impl.Ownership;
import com.sap.sse.security.shared.impl.Role;
import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.shared.impl.UserGroup;
import com.sap.sse.security.shared.impl.UserGroupImpl;
/**
* Keeps track of all {@link User}, {@link UserGroupImpl} and {@link Role}
* objects persistently; furthermore, aspects such as user access tokens, preferences and
* settings are stored durably.<p>
*
* @author Axel Uhl (d043530)
*
*/
public interface BasicUserStore extends UserGroupProvider, Named {
Iterable<UserGroup> getUserGroups();
UserGroup getUserGroup(UUID groupId);
UserGroup getUserGroupByName(String name);
UserGroup createUserGroup(UUID groupId, String name) throws UserGroupManagementException;
void updateUserGroup(UserGroup userGroup);
void deleteUserGroup(UserGroup userGroup) throws UserGroupManagementException;
Iterable<User> getUsers();
boolean hasUsers();
/**
* The user with that {@link UserImpl#getName() name} or {@code null} if no such user exists
*/
User getUserByName(String username);
/**
* The user with that {@link UserImpl#getEmail() email} or {@code null} if no such user exists
*/
User getUserByEmail(String email);
User getUserByAccessToken(String accessToken);
User createUser(String name, String email, Account... accounts)
throws UserManagementException;
void updateUser(User user);
Iterable<Role> getRolesFromUser(String username) throws UserManagementException;
void addRoleForUser(String username, Role role) throws UserManagementException;
void removeRoleFromUser(String username, Role role) throws UserManagementException;
Iterable<WildcardPermission> getPermissionsFromUser(String username) throws UserManagementException;
void removePermissionFromUser(String username, WildcardPermission permission) throws UserManagementException;
void addPermissionForUser(String username, WildcardPermission permission) throws UserManagementException;
void deleteUser(String username) throws UserManagementException;
Iterable<RoleDefinition> getRoleDefinitions();
RoleDefinition getRoleDefinition(UUID roleDefinitionId);
RoleDefinition createRoleDefinition(UUID roleDefinitionId, String displayName, Iterable<WildcardPermission> permissions);
void setRoleDefinitionPermissions(UUID roleDefinitionId, Set<WildcardPermission> permissions);
void addRoleDefinitionPermission(UUID roleDefinitionId, WildcardPermission permission);
void removeRoleDefinitionPermission(UUID roleDefinitionId, WildcardPermission permission);
void setRoleDefinitionDisplayName(UUID roleDefinitionId, String displayName);
void removeRoleDefinition(RoleDefinition roleDefinition);
/**
* Registers a settings key together with its type. Calling this method is necessary for {@link #setSetting(String, Object)}
* to have an effect for <code>key</code>. Calls to {@link #setSetting(String, Object)} will only accept values whose type
* is compatible with <code>type</code>. Note that the store implementation may impose constraints on the types supported.
* All store implementations are required to support at least {@link String} and {@link UUID} as types.
*/
void addSetting(String key, Class<?> type);
void setPreference(String username, String key, String value);
/**
* Always returns a valid map which may be empty.
*/
Map<String, String> getAllPreferences(String username);
void unsetPreference(String username, String key);
String getPreference(String username, String key);
/**
* Sets a value for a key if that key was previously added to this store using {@link #addSetting(String, Class)}.
* For user store implementations that maintain their data persistently and make it available after a server
* restart, it is sufficient to register the settings key once because these registrations will be stored
* persistently, too.
* <p>
*
* If the <code>key</code> was not registered before by a call to {@link #addSetting(String, Class)}, or if the
* <code>setting</code> object does not conform with the type passed to {@link #addSetting(String, Class)}, a call
* to this method will have no effect and return <code>false</code>.
*
* @Return whether applying the setting was successful; <code>false</code> means that no update was performed to the
* setting because either the key was not registered before by {@link #addSetting(String, Class)} or the type of the
* <code>setting</code> object does not conform to the type used in {@link #addSetting(String, Class)}
*/
boolean setSetting(String key, Object setting);
<T> T getSetting(String key, Class<T> clazz);
Map<String, Object> getAllSettings();
Map<String, Class<?>> getAllSettingTypes();
/**
* Removes all users and all their preferences and all settings from this store's in-memory representation.
* For safety reasons and because a replica's DB state is undefined anyhow, leaves persistent content in place.
* Registered listeners will not be removed automatically.
* Use with due care.
*/
void clear();
/**
* Stores an access token that can be used to authenticate the user identified by <code>username</code>.
* If there is no user by that name, calling this method has no effect and it will return <code>false</code>.
*
* @return whether a user could be identified by <code>username</code>
*/
boolean setAccessToken(String username, String accessToken);
void removeAccessToken(String username);
/**
* The owner and any subject having the {@link DefaultRoles#ADMIN} role can retrieve an existing
* authentication token for the user. {@code null} may result in case for the user identified by
* {@code username} no access token has previously been {@link #setAccessToken(String, String) set}.
*/
String getAccessToken(String username);
/**
* If a valid default tenant name was passed to the constructor, this field will contain a valid
* {@link UserGroupImpl} object whose name equals that of the default tenant name. It will have been used
* during role migration where string-based roles are mapped to a corresponding {@link RoleDefinition}
* and the users with the original role will obtain a corresponding {@link Role} with this default
* tenant as the {@link Role#getQualifiedForTenant() tenant qualifier}.
*/
UserGroup getDefaultTenant();
/**
* Ensures that the predefined role definitions, particularly the "admin" and the "user" role, exist.
*/
void ensureDefaultRolesExist();
/**
* @return a pair with: <br/>
* If A is true, at least one user has an unqualified version of the {@link #roleToCheck} (without tenant or
* user qualification). In this case, B is null.<br/>
* If A is false, B contains all the ownerships of {@link #roleToCheck}
*/
Pair<Boolean, Set<Ownership>> getExistingQualificationsForRoleDefinition(RoleDefinition roleToCheck);
Set<Pair<User, Role>> getRolesQualifiedByUserGroup(UserGroup groupQualification);
}
@@ -5,6 +5,8 @@ import java.util.UUID;
import com.sap.sse.common.NamedWithID;
import com.sap.sse.common.Renamable;
import com.sap.sse.security.shared.impl.Ownership;
import com.sap.sse.security.shared.impl.Role;
/**
* A role definition provides an ID, a (changeable) name and a set of permissions. As such, it represents a group of
@@ -51,7 +51,12 @@ public interface User extends SecurityUser<RoleDefinition, Role, UserGroup> {
Map<AccountType, Account> getAllAccounts();
String setEmail(String email);
/**
* Sets an e-mail address for this user. The address is considered not yet validated, therefore the
* caller shall also ensure that {@link #startEmailValidation} is invoked hereafter, with a secret
* produced by {@link #createRandomSecret()}.
*/
void setEmail(String email);
/**
* When someone has requested a password reset, only the owner of the validated e-mail address is
@@ -61,9 +66,14 @@ public interface User extends SecurityUser<RoleDefinition, Role, UserGroup> {
*/
String getPasswordResetSecret();
String startPasswordReset();
void startPasswordReset(String randomSecret);
String startEmailValidation();
/**
* Resets the {@link #isEmailValidated()} property and stores the new {@code randomSecret} as the
* e-mail validation secret. The {@link #isEmailValidated()} method will return {@code true} only
* after {@link #validate(String)} has been called with the {@code randomSecret} passed here.
*/
void startEmailValidation(String randomSecret);
boolean validate(String validationSecret);
@@ -99,4 +109,6 @@ public interface User extends SecurityUser<RoleDefinition, Role, UserGroup> {
void setUserGroupProvider(UserGroupProvider userGroupProvider);
UserGroupProvider getUserGroupProvider();
String createRandomSecret();
}
@@ -225,15 +225,9 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
return email;
}
/**
* Sets an e-mail address for this user. The address is considered not yet validated, therefore the
* {@link #emailValidated} flag is reset, and a new {@link #validationSecret} is generated and returned which
* can be used in a call to {@link #validate(String)} to validate the e-mail address.
*/
@Override
public String setEmail(String email) {
public void setEmail(String email) {
this.email = email;
return startEmailValidation();
}
/**
@@ -242,10 +236,9 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
* can be used in a call to {@link #validate(String)} to validate the e-mail address.
*/
@Override
public String startEmailValidation() {
validationSecret = createRandomSecret();
public void startEmailValidation(String randomSecret) {
validationSecret = randomSecret;
emailValidated = false;
return validationSecret;
}
/**
@@ -254,9 +247,8 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
* a user's password in case the service can provide the correct password reset secret.
*/
@Override
public String startPasswordReset() {
passwordResetSecret = createRandomSecret();
return passwordResetSecret;
public void startPasswordReset(String randomSecret) {
passwordResetSecret = randomSecret;
}
@Override
@@ -264,7 +256,8 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
return passwordResetSecret;
}
private String createRandomSecret() {
@Override
public String createRandomSecret() {
final byte[] bytes1 = new byte[64];
new SecureRandom().nextBytes(bytes1);
final byte[] bytes2 = new byte[64];
@@ -273,9 +266,10 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
}
/**
* If the <code>validationSecret</code> passed matches {@link #validationSecret}, the e-mail is
* {@link #emailValidated marked as validated}, and <code>true</code> is returned. Otherwise, the validation secret
* on this user remains in place, and the e-mail address is not marked as validated.
* If the user's e-mail has already been {@link #isEmailValidated() validated}, or the <code>validationSecret</code>
* passed matches {@link #validationSecret}, the e-mail is {@link #emailValidated marked as validated}, and
* <code>true</code> is returned. Otherwise, the validation secret on this user remains in place, and the e-mail
* address is not marked as validated.
*/
@Override
public boolean validate(final String validationSecret) {
@@ -1,21 +1,10 @@
package com.sap.sse.security.interfaces;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.sap.sse.common.Named;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.security.shared.Account;
import com.sap.sse.security.shared.RoleDefinition;
import com.sap.sse.security.shared.BasicUserStore;
import com.sap.sse.security.shared.UserGroupManagementException;
import com.sap.sse.security.shared.UserGroupProvider;
import com.sap.sse.security.shared.UserManagementException;
import com.sap.sse.security.shared.WildcardPermission;
import com.sap.sse.security.shared.impl.Ownership;
import com.sap.sse.security.shared.impl.Role;
import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.shared.impl.UserGroup;
import com.sap.sse.security.shared.impl.UserGroupImpl;
/**
@@ -26,7 +15,7 @@ import com.sap.sse.security.shared.impl.UserGroupImpl;
* @author Axel Uhl (d043530)
*
*/
public interface UserStore extends UserGroupProvider, Named {
public interface UserStore extends BasicUserStore {
/**
* An instance of the bundle hosting this service may have a default tenant. If so, the default tenant's name is
* read from a system property whose name is provided by this constant.
@@ -34,82 +23,6 @@ public interface UserStore extends UserGroupProvider, Named {
String DEFAULT_TENANT_NAME_PROPERTY_NAME = "security.defaultTenantName";
String ADMIN_USERNAME = "admin";
Iterable<UserGroup> getUserGroups();
UserGroup getUserGroup(UUID groupId);
UserGroup getUserGroupByName(String name);
UserGroup createUserGroup(UUID groupId, String name) throws UserGroupManagementException;
void updateUserGroup(UserGroup userGroup);
void deleteUserGroup(UserGroup userGroup) throws UserGroupManagementException;
Iterable<User> getUsers();
boolean hasUsers();
/**
* The user with that {@link UserImpl#getName() name} or {@code null} if no such user exists
*/
User getUserByName(String username);
/**
* The user with that {@link UserImpl#getEmail() email} or {@code null} if no such user exists
*/
User getUserByEmail(String email);
User getUserByAccessToken(String accessToken);
User createUser(String name, String email, Account... accounts)
throws UserManagementException;
void updateUser(User user);
Iterable<Role> getRolesFromUser(String username) throws UserManagementException;
void addRoleForUser(String username, Role role) throws UserManagementException;
void removeRoleFromUser(String username, Role role) throws UserManagementException;
Iterable<WildcardPermission> getPermissionsFromUser(String username) throws UserManagementException;
void removePermissionFromUser(String username, WildcardPermission permission) throws UserManagementException;
void addPermissionForUser(String username, WildcardPermission permission) throws UserManagementException;
void deleteUser(String username) throws UserManagementException;
Iterable<RoleDefinition> getRoleDefinitions();
RoleDefinition getRoleDefinition(UUID roleDefinitionId);
RoleDefinition createRoleDefinition(UUID roleDefinitionId, String displayName, Iterable<WildcardPermission> permissions);
void setRoleDefinitionPermissions(UUID roleDefinitionId, Set<WildcardPermission> permissions);
void addRoleDefinitionPermission(UUID roleDefinitionId, WildcardPermission permission);
void removeRoleDefinitionPermission(UUID roleDefinitionId, WildcardPermission permission);
void setRoleDefinitionDisplayName(UUID roleDefinitionId, String displayName);
void removeRoleDefinition(RoleDefinition roleDefinition);
/**
* Registers a settings key together with its type. Calling this method is necessary for {@link #setSetting(String, Object)}
* to have an effect for <code>key</code>. Calls to {@link #setSetting(String, Object)} will only accept values whose type
* is compatible with <code>type</code>. Note that the store implementation may impose constraints on the types supported.
* All store implementations are required to support at least {@link String} and {@link UUID} as types.
*/
void addSetting(String key, Class<?> type);
void setPreference(String username, String key, String value);
/**
* Always returns a valid map which may be empty.
*/
Map<String, String> getAllPreferences(String username);
void unsetPreference(String username, String key);
String getPreference(String username, String key);
/**
* <p>
* In an OSGi environment, this shouldn't be called manually, but instead automatically managed by setting a
@@ -164,88 +77,16 @@ public interface UserStore extends UserGroupProvider, Named {
*/
String setPreferenceObject(String username, String key, Object preferenceObject) throws IllegalArgumentException;
/**
* Sets a value for a key if that key was previously added to this store using {@link #addSetting(String, Class)}.
* For user store implementations that maintain their data persistently and make it available after a server
* restart, it is sufficient to register the settings key once because these registrations will be stored
* persistently, too.
* <p>
*
* If the <code>key</code> was not registered before by a call to {@link #addSetting(String, Class)}, or if the
* <code>setting</code> object does not conform with the type passed to {@link #addSetting(String, Class)}, a call
* to this method will have no effect and return <code>false</code>.
*
* @Return whether applying the setting was successful; <code>false</code> means that no update was performed to the
* setting because either the key was not registered before by {@link #addSetting(String, Class)} or the type of the
* <code>setting</code> object does not conform to the type used in {@link #addSetting(String, Class)}
*/
boolean setSetting(String key, Object setting);
<T> T getSetting(String key, Class<T> clazz);
Map<String, Object> getAllSettings();
Map<String, Class<?>> getAllSettingTypes();
/**
* Removes all users and all their preferences and all settings from this store's in-memory representation.
* For safety reasons and because a replica's DB state is undefined anyhow, leaves persistent content in place.
* Registered listeners will not be removed automatically.
* Use with due care.
*/
void clear();
/**
* Replaces all existing contents by those provided by the <code>newUserStore</code>. This has no impact on the persistent
* representation of this store and is meant for use on a replica only; the replica's database state is undefined.
*/
void replaceContentsFrom(UserStore newUserStore);
/**
* Stores an access token that can be used to authenticate the user identified by <code>username</code>.
* If there is no user by that name, calling this method has no effect and it will return <code>false</code>.
*
* @return whether a user could be identified by <code>username</code>
*/
boolean setAccessToken(String username, String accessToken);
void removeAccessToken(String username);
/**
* The owner and any subject having the {@link DefaultRoles#ADMIN} role can retrieve an existing
* authentication token for the user. {@code null} may result in case for the user identified by
* {@code username} no access token has previously been {@link #setAccessToken(String, String) set}.
*/
String getAccessToken(String username);
void addPreferenceObjectListener(String key, PreferenceObjectListener<?> listener, boolean fireForAlreadyExistingPreferences);
void removePreferenceObjectListener(PreferenceObjectListener<?> listener);
/**
* If a valid default tenant name was passed to the constructor, this field will contain a valid
* {@link UserGroupImpl} object whose name equals that of the default tenant name. It will have been used
* during role migration where string-based roles are mapped to a corresponding {@link RoleDefinition}
* and the users with the original role will obtain a corresponding {@link Role} with this default
* tenant as the {@link Role#getQualifiedForTenant() tenant qualifier}.
*/
UserGroup getDefaultTenant();
/**
* Ensures that the predefined role definitions, particularly the "admin" and the "user" role, exist.
*/
void ensureDefaultRolesExist();
/**
* @return a pair with: <br/>
* If A is true, at least one user has an unqualified version of the {@link #roleToCheck} (without tenant or
* user qualification). In this case, B is null.<br/>
* If A is false, B contains all the ownerships of {@link #roleToCheck}
*/
Pair<Boolean, Set<Ownership>> getExistingQualificationsForRoleDefinition(RoleDefinition roleToCheck);
Set<Pair<User, Role>> getRolesQualifiedByUserGroup(UserGroup groupQualification);
/**
* Do not call this before the RolePrototypes are created/loaded, as else a migration cannot succeed. But do call
* this before the SecurityService is created, as else new defaults (eg admin user) will be created
@@ -694,10 +694,6 @@ public class UserStoreImpl implements UserStore {
@Override
public void updateUser(User user) {
logger.info("Updating user " + user + " in DB");
if (user.getUserGroupProvider() != this) {
logger.info("Adjusting user group provider for user "+user+"; probably after de-serialization.");
user.setUserGroupProvider(this);
}
users.put(user.getName(), user);
removeFromUsersByEmail(user);
addToUsersByEmail(user);
@@ -207,7 +207,6 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
dbPermissions.add(permission.toString());
}
dbUser.put(FieldNames.User.PERMISSIONS.name(), dbPermissions);
List<Object> defaultTennants = new BasicDBList();
for (Entry<String, UserGroup> entries : user.getDefaultTenantMap().entrySet()) {
Document tenant = new Document();
@@ -124,7 +124,7 @@ public class UserProxy implements User {
}
@Override
public String setEmail(String email) {
public void setEmail(String email) {
throw new UnsupportedOperationException();
}
@@ -134,12 +134,12 @@ public class UserProxy implements User {
}
@Override
public String startPasswordReset() {
public void startPasswordReset(String randomSecret) {
throw new UnsupportedOperationException();
}
@Override
public String startEmailValidation() {
public void startEmailValidation(String randomSecret) {
throw new UnsupportedOperationException();
}
@@ -202,4 +202,9 @@ public class UserProxy implements User {
public UserGroupProvider getUserGroupProvider() {
throw new UnsupportedOperationException();
}
@Override
public String createRandomSecret() {
throw new UnsupportedOperationException();
}
}
@@ -117,7 +117,6 @@ public class Activator implements BundleActivator {
if (cm instanceof ReplicatingCacheManager) {
((ReplicatingCacheManager) cm).clear();
}
UserStore userStore = userStoreTracker.waitForService(0);
AccessControlStore accessControlStore = accessControlStoreTracker.waitForService(0);
userStore.clear();
@@ -1,11 +1,13 @@
package com.sap.sse.security.impl;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import org.apache.shiro.session.Session;
import com.sap.sse.security.SecurityService;
import com.sap.sse.security.shared.Account;
import com.sap.sse.security.shared.QualifiedObjectIdentifier;
import com.sap.sse.security.shared.RoleDefinition;
import com.sap.sse.security.shared.UserGroupManagementException;
@@ -51,7 +53,19 @@ public interface ReplicableSecurityService extends SecurityService {
Void internalRemoveRoleDefinitionFromUserGroup(UUID groupId, UUID roleDefinitionId)
throws UserGroupManagementException;
Void internalStoreUser(User user);
/**
* Creates and stores a <em>new</em> user in the system. <em>Don't</em> use this to update an existing user.
* Use other {@code internal...} methods to update individual user properties instead.
*/
User internalCreateUser(String username, String email, Account... accounts) throws UserManagementException;
Void internalUpdateSimpleUserEmail(String username, String newEmail, String validationSecret);
Void internalUpdateSimpleUserPassword(String username, byte[] salt, String hashedPasswordBase64);
Void internalUpdateUserProperties(User user, String fullName, String company, Locale locale);
Boolean internalValidateEmail(String username, String validationSecret);
Void internalSetPreference(String username, String key, String value);
@@ -94,4 +108,6 @@ public interface ReplicableSecurityService extends SecurityService {
Void internalSetDefaultTenantForServerForUser(String username, UUID defaultTenantId, String serverName);
Void internalResetPassword(String username, String passwordResetSecret);
}
@@ -316,7 +316,7 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
if (store.getUserByName(SecurityService.ALL_USERNAME) == null) {
isInitialOrMigration = true;
logger.info(SecurityService.ALL_USERNAME + " not found -> creating it now");
User allUser = createUserInternal(SecurityService.ALL_USERNAME, null);
User allUser = apply(s->s.internalCreateUser(SecurityService.ALL_USERNAME, null));
// <all> user is explicitly not owned by itself because this would enable anybody to modify this user
setOwnership(allUser.getIdentifier(), null, getDefaultTenant());
@@ -369,8 +369,8 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
if (!user.isEmailValidated()) {
throw new UserManagementException(UserManagementException.CANNOT_RESET_PASSWORD_WITHOUT_VALIDATED_EMAIL);
}
final String passwordResetSecret = user.startPasswordReset();
apply(s->s.internalStoreUser(user)); // durably storing the password reset secret
final String passwordResetSecret = user.createRandomSecret();
apply(s->s.internalResetPassword(username, passwordResetSecret));
Map<String, String> urlParameters = new HashMap<>();
try {
urlParameters.put("u", URLEncoder.encode(user.getName(), "UTF-8"));
@@ -397,6 +397,12 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
}
}
@Override
public Void internalResetPassword(String username, String passwordResetSecret) {
getUserByName(username).startPasswordReset(passwordResetSecret);
return null;
}
@Override
public CachingSecurityManager getSecurityManager() {
return this.securityManager;
@@ -784,7 +790,7 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
@Override
public User createSimpleUser(final String username, final String email, String password, String fullName,
String company, Locale locale, final String validationBaseURL, UserGroup userOwner)
String company, Locale locale, final String validationBaseURL, UserGroup groupOwningUser)
throws UserManagementException, MailException, UserGroupManagementException {
logger.info("Creating user "+username);
if (store.getUserByName(username) != null) {
@@ -801,35 +807,15 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
byte[] salt = rng.nextBytes().getBytes();
String hashedPasswordBase64 = hashPassword(password, salt);
UsernamePasswordAccount upa = new UsernamePasswordAccount(username, hashedPasswordBase64, salt);
final User result = createUserInternal(username, email, upa);
final User result = apply(s->s.internalCreateUser(username, email, upa)); // This also replicated the user creation
addUserRoleToUser(result);
final UserGroup tenant = getOrCreateTenantForUser(result);
setDefaultTenantForCurrentServerForUser(username, tenant.getId());
// the new user becomes its owner to ensure the user role is correctly working
// the default tenant is the owning tenant to allow users having admin role for a specific server tenant to also be able to delete users
accessControlStore.setOwnership(result.getIdentifier(), result, userOwner, username);
result.setFullName(fullName);
result.setCompany(company);
result.setLocale(locale);
final String emailValidationSecret = result.startEmailValidation();
// don't replicate exception handling; replicate only the effect on the user store
apply(s->s.internalStoreUser(result));
if (validationBaseURL != null && email != null && !email.trim().isEmpty()) {
new Thread("e-mail validation for user " + username + " with e-mail address " + email) {
@Override
public void run() {
try {
startEmailValidation(result, emailValidationSecret, validationBaseURL);
} catch (MailException e) {
logger.log(Level.SEVERE, "Error sending mail for new account validation of user " + username
+ " to address " + email, e);
}
}
}.start();
}
apply(s->s.internalSetOwnership(result.getIdentifier(), username, groupOwningUser.getId(), username));
updateUserProperties(username, fullName, company, locale);
updateSimpleUserEmail(username, email, validationBaseURL);
return result;
}
@@ -858,13 +844,9 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
return tenant;
}
private User createUserInternal(String username, String email, Account... accounts)
throws UserManagementException {
final User result = store.createUser(username, email, accounts); // TODO: get the principal
// as owner
// now the user creation needs to be replicated so that when replicating role addition and group assignment
// the replica will be able to resolve the user correctly
apply(s -> s.internalStoreUser(result));
@Override
public User internalCreateUser(String username, String email, Account... accounts) throws UserManagementException {
final User result = store.createUser(username, email, accounts); // TODO: get the principal as owner
return result;
}
@@ -872,12 +854,6 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
return username + "-tenant";
}
@Override
public Void internalStoreUser(User user) {
store.updateUser(user);
return null;
}
@Override
public void updateSimpleUserPassword(String username, String newPassword) throws UserManagementException {
final User user = store.getUserByName(username);
@@ -892,14 +868,20 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
throw new UserManagementException(UserManagementException.PASSWORD_DOES_NOT_MEET_REQUIREMENTS);
}
// for non-admins, check that the old password is correct
final UsernamePasswordAccount account = (UsernamePasswordAccount) user.getAccount(AccountType.USERNAME_PASSWORD);
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
byte[] salt = rng.nextBytes().getBytes();
String hashedPasswordBase64 = hashPassword(newPassword, salt);
apply(s->s.internalUpdateSimpleUserPassword(user.getName(), salt, hashedPasswordBase64));
}
@Override
public Void internalUpdateSimpleUserPassword(String username, byte[] salt, String hashedPasswordBase64) {
final User user = getUserByName(username);
final UsernamePasswordAccount account = (UsernamePasswordAccount) user.getAccount(AccountType.USERNAME_PASSWORD);
account.setSalt(salt);
account.setSaltedPassword(hashedPasswordBase64);
user.passwordWasReset();
apply(s->s.internalStoreUser(user));
return null;
}
@Override
@@ -908,14 +890,16 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
if (user == null) {
throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST);
}
updateUserProperties(user, fullName, company, locale);
apply(s->s.internalUpdateUserProperties(user, fullName, company, locale));
}
private void updateUserProperties(User user, String fullName, String company, Locale locale) {
@Override
public Void internalUpdateUserProperties(User user, String fullName, String company, Locale locale) {
user.setFullName(fullName);
user.setCompany(company);
user.setLocale(locale);
apply(s->s.internalStoreUser(user));
store.updateUser(user);
return null;
}
@Override
@@ -945,7 +929,8 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST);
}
logger.info("Changing e-mail address of user "+username+" to "+newEmail);
final String validationSecret = user.setEmail(newEmail);
final String validationSecret = user.createRandomSecret();
apply(s->s.internalUpdateSimpleUserEmail(username, newEmail, validationSecret));
new Thread("e-mail validation after changing e-mail of user " + username + " to " + newEmail) {
@Override
public void run() {
@@ -957,7 +942,14 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
}
}
}.start();
apply(s->s.internalStoreUser(user));
}
@Override
public Void internalUpdateSimpleUserEmail(final String username, final String newEmail, final String validationSecret) {
final User user = getUserByName(username);
user.setEmail(newEmail);
user.startEmailValidation(validationSecret);
return null;
}
@Override
@@ -966,17 +958,25 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
if (user == null) {
throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST);
}
return apply(s->s.internalValidateEmail(username, validationSecret));
}
@Override
public Boolean internalValidateEmail(String username, String validationSecret) {
final User user = store.getUserByName(username);
final boolean result = user.validate(validationSecret);
apply(s->s.internalStoreUser(user));
if (result) {
store.updateUser(user);
}
return result;
}
/**
* {@link UserImpl#startEmailValidation() Triggers} e-mail validation for the <code>user</code> object and sends out a
* {@link UserImpl#startEmailValidation(String) Triggers} e-mail validation for the <code>user</code> object and sends out a
* URL to the user's e-mail that has the validation secret ready for validation by clicking.
*
* @param validationSecret
* the result of either {@link UserImpl#startEmailValidation()} or {@link UserImpl#setEmail(String)}.
* the result of either {@link UserImpl#startEmailValidation(String)} or {@link UserImpl#setEmail(String)}.
* @param baseURL
* the URL under which the user can reach the e-mail validation service; this URL is required to assemble
* a validation URL that is sent by e-mail to the user, to make the user return the validation secret to