bug6097: apply same mechanism for user creation per IP locking as for failed bearer token-based authentication

This commit is contained in:
Axel Uhl
2025-03-19 14:17:35 +01:00
parent f61055f62e
commit b97ace3db0
15 changed files with 175 additions and 63 deletions
@@ -63,7 +63,7 @@ public class LeagueEventHierarchyOwnershipChangeTest {
securityService = new SecurityBundleTestWrapper().initializeSecurityServiceForTesting();
Mockito.doReturn(securityService).when(service).getSecurityService();
securityService.createSimpleUser(USERNAME, "a@b.c", PASSWORD, "The User", "SAP SE",
/* validation URL */ Locale.ENGLISH, null, null);
/* validation URL */ Locale.ENGLISH, null, null, /* clientIP */ null);
}
@Before
@@ -96,7 +96,7 @@ public class TaggingServiceTest {
securityService = new SecurityBundleTestWrapper().initializeSecurityServiceForTesting();
// create & login user
securityService.createSimpleUser(username, email, password, fullName, company, Locale.ENGLISH, null,
securityService.getDefaultTenantForCurrentUser());
securityService.getDefaultTenantForCurrentUser(), /* clientIP */ null);
ThreadContext.unbindSubject(); // ensure that a new subject is created that knows the current security manager
subject = SecurityUtils.getSubject(); // this also binds the Subject to the ThreadContext
subject.login(new UsernamePasswordToken(username, password));
@@ -9,7 +9,6 @@ import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.mail.Message.RecipientType;
@@ -107,8 +106,7 @@ public class MailServiceImpl extends AbstractReplicableWithObjectInputStream<Rep
ts.close();
logger.info("mail sent to " + Arrays.toString(toAddresses) + " with subject " + subject);
} catch (MessagingException e) {
logger.log(Level.SEVERE, "Error trying to send mail to " + Arrays.toString(toAddresses), e);
throw new MailException(e.getMessage(), e);
logger.severe("Error trying to send mail to " + Arrays.toString(toAddresses)+": "+e.getMessage());
} finally {
Thread.currentThread().setContextClassLoader(oldClassLoader);
}
@@ -120,7 +120,7 @@ public class SecurityReplicationLeadingToEmailReplicationTest extends AbstractSe
final String password = "BertMyFriend";
final String validationBaseURL = null; //so that validation email is not sent
masterSecurityService.createSimpleUser(username, email, password,
/* fullName */ null, /* company */ null, Locale.ENGLISH, validationBaseURL, null);
/* fullName */ null, /* company */ null, Locale.ENGLISH, validationBaseURL, null, /* clientIP */ null);
masterSecurityService.sendMail(username, "subject", "body");
securitySetUp.getReplicaReplicator().waitUntilQueueIsEmpty();
mailSetUp.getReplicaReplicator().waitUntilQueueIsEmpty();
@@ -146,7 +146,7 @@ public class SecurityReplicationLeadingToEmailReplicationTest extends AbstractSe
final String password = "BertMyFriend";
final String validationBaseURL = null; //so that validation email is not sent
replicaSecurityService.createSimpleUser(username, email, password,
/* fullName */ null, /* company */ null, Locale.ENGLISH, validationBaseURL, null);
/* fullName */ null, /* company */ null, Locale.ENGLISH, validationBaseURL, null, /* clientIP */ null);
replicaSecurityService.sendMail(username, "subject", "body");
securitySetUp.getReplicaReplicator().waitUntilQueueIsEmpty();
mailSetUp.getReplicaReplicator().waitUntilQueueIsEmpty();
@@ -44,7 +44,7 @@ public class SecurityServiceInitialLoadTest extends AbstractServerWithSingleServ
final SecurityServiceImpl newMaster = new SecurityServiceImpl(null, /* corsFilterConfigurationTracker */ null, userStore,
accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances);
newMaster.createSimpleUser(username, email, password, fullName, company,
/* validationBaseURL */ Locale.ENGLISH, null, null);
/* validationBaseURL */ Locale.ENGLISH, null, null, /* clientIP */ null);
accessToken = newMaster.createAccessToken(username);
return newMaster;
}
@@ -40,7 +40,7 @@ public class SimpleSecurityReplicationTest extends AbstractSecurityReplicationTe
public void testSimpleReplicationOfUserCreation() throws InterruptedException, UserManagementException, MailException, IllegalAccessException, UserGroupManagementException {
assertNull(master.getUserByName(ERNIE));
User user = master.createSimpleUser(ERNIE, ERNIE_SESAME_STREET_COM, BERT_MY_FRIEND, ERNIE_S_FULL_NAME, ERNIE_S_COMPANY, Locale.ENGLISH,
HTTP_ME_TO_BACK_COM, null);
HTTP_ME_TO_BACK_COM, null, /* clientIP */ null);
assertNotNull(user);
assertSame(user, master.getUserByName(ERNIE));
assertTrue(master.checkPassword(ERNIE, BERT_MY_FRIEND));
@@ -65,7 +65,7 @@ public class SimpleSecurityReplicationTest extends AbstractSecurityReplicationTe
@Test
public void testSimpleReplicationOfUserEmailChange() throws InterruptedException, UserManagementException, MailException, IllegalAccessException, UserGroupManagementException {
User user = master.createSimpleUser(ERNIE, ERNIE_SESAME_STREET_COM, BERT_MY_FRIEND, ERNIE_S_FULL_NAME, ERNIE_S_COMPANY, Locale.ENGLISH,
HTTP_ME_TO_BACK_COM, master.getDefaultTenantForCurrentUser());
HTTP_ME_TO_BACK_COM, master.getDefaultTenantForCurrentUser(), /* clientIP */ null);
user.setFullName(ERNIE_S_FULL_NAME);
user.setCompany(ERNIE_S_COMPANY);
final String emailValidationSecretAfterCreation = user.getValidationSecret();
@@ -89,7 +89,7 @@ public class SimpleSecurityReplicationTest extends AbstractSecurityReplicationTe
final String newPassword = "ErnieAndBert";
master.createSimpleUser(ERNIE, ERNIE_SESAME_STREET_COM, BERT_MY_FRIEND,
/* fullName */ null, /* company */ null, Locale.ENGLISH, HTTP_ME_TO_BACK_COM,
null);
null, /* clientIP */ null);
master.updateSimpleUserPassword(ERNIE, newPassword);
assertTrue(master.checkPassword(ERNIE, newPassword));
replicaReplicator.waitUntilQueueIsEmpty();
@@ -110,7 +110,7 @@ public class SimpleSecurityReplicationTest extends AbstractSecurityReplicationTe
final String passwordResetBaseURL = "http://me.to.back.com/passwordreset";
User user = master.createSimpleUser(ERNIE, ERNIE_SESAME_STREET_COM, BERT_MY_FRIEND,
/* fullName */ null, /* company */ null, Locale.ENGLISH, validationBaseURL,
null);
null, /* clientIP */ null);
master.validateEmail(ERNIE, user.getValidationSecret());
assertTrue(user.isEmailValidated());
master.resetPassword(ERNIE, passwordResetBaseURL);
@@ -131,7 +131,7 @@ public class SimpleSecurityReplicationTest extends AbstractSecurityReplicationTe
final Set<WildcardPermission> permissions = Collections.singleton(permission);
master.createSimpleUser(ERNIE, ERNIE_SESAME_STREET_COM, BERT_MY_FRIEND,
/* fullName */ null, /* company */ null, Locale.ENGLISH, null,
null);
null, /* clientIP */ null);
final RoleDefinition roleDefinition = master.createRoleDefinition(roleDefinitionUUID, roleDefinitionName);
roleDefinition.setPermissions(permissions);
master.updateRoleDefinition(roleDefinition);
@@ -151,7 +151,7 @@ public class SimpleSecurityReplicationTest extends AbstractSecurityReplicationTe
final Set<WildcardPermission> permissions = Collections.singleton(permission);
master.createSimpleUser(ERNIE, ERNIE_SESAME_STREET_COM, BERT_MY_FRIEND,
/* fullName */ null, /* company */ null, Locale.ENGLISH, null,
null);
null, /* clientIP */ null);
final RoleDefinition roleDefinition = master.createRoleDefinition(roleDefinitionUUID, roleDefinitionName);
roleDefinition.setPermissions(permissions);
master.updateRoleDefinition(roleDefinition);
@@ -73,7 +73,7 @@ public class SecurityResourceTest {
Activator.setSecurityService(service);
SecurityUtils.setSecurityManager(service.getSecurityManager());
service.createSimpleUser(USERNAME, "a@b.c", PASSWORD, "The User", "SAP SE",
/* validation URL */ Locale.ENGLISH, null, null);
/* validation URL */ Locale.ENGLISH, null, null, /* clientIP */ null);
authenticatedAdmin = SecurityUtils.getSubject();
authenticatedAdmin.login(new UsernamePasswordToken(USERNAME, PASSWORD));
Session session = authenticatedAdmin.getSession();
@@ -94,11 +94,11 @@ public class SecurityResourceTest {
@Test
public void testNullClientIP() {
assertFalse(service.isClientIPAndUserAgentLocked(null)); // ensure there is no exception being thrown
assertFalse(service.isClientIPLockedForBearerTokenAuthentication(null)); // ensure there is no exception being thrown
service.failedBearerTokenAuthentication(null);
assertTrue(service.isClientIPAndUserAgentLocked(null));
assertTrue(service.isClientIPLockedForBearerTokenAuthentication(null));
service.successfulBearerTokenAuthentication(null);
assertFalse(service.isClientIPAndUserAgentLocked(null));
assertFalse(service.isClientIPLockedForBearerTokenAuthentication(null));
}
@Test
@@ -92,7 +92,7 @@ public class LoginTest {
final String username = "TheNewUser";
final String specialUserGroupName1 = "TheSpecialUserGroup1";
final String specialUserGroupName2 = "TheSpecialUserGroup2";
final User user = securityService.createSimpleUser(username, "u@a.b", "Humba", "The New User", /* company */ null, /* locale */ null, /* validationBaseURL */ null, /* owning group */ null);
final User user = securityService.createSimpleUser(username, "u@a.b", "Humba", "The New User", /* company */ null, /* locale */ null, /* validationBaseURL */ null, /* owning group */ null, /* clientIP */ null);
final TypeRelativeObjectIdentifier myServerTypeRelativeObjectIdentifier = new TypeRelativeObjectIdentifier("myserver");
final WildcardPermission createObjectPermissionOnMyserver = SecuredSecurityTypes.SERVER.getPermissionForTypeRelativeIdentifier(ServerActions.CREATE_OBJECT,
myServerTypeRelativeObjectIdentifier);
@@ -123,7 +123,7 @@ public class LoginTest {
final String username = "TheNewUser";
final String specialUserGroupName1 = "TheSpecialUserGroup1";
final String specialUserGroupName2 = "TheSpecialUserGroup2";
final User user = securityService.createSimpleUser(username, "u@a.b", "Humba", "The New User", /* company */ null, /* locale */ null, /* validationBaseURL */ null, /* owning group */ null);
final User user = securityService.createSimpleUser(username, "u@a.b", "Humba", "The New User", /* company */ null, /* locale */ null, /* validationBaseURL */ null, /* owning group */ null, /* clientIP */ null);
final UserGroup defaultUserGroup = securityService.getUserGroupByName(username+SecurityService.TENANT_SUFFIX);
final UserGroup specialUserGroup1 = securityService.createUserGroup(UUID.randomUUID(), specialUserGroupName1);
final UserGroup specialUserGroup2 = securityService.createUserGroup(UUID.randomUUID(), specialUserGroupName2);
@@ -141,7 +141,7 @@ public class LoginTest {
public void testAclAnonUserGroup() throws UserManagementException, MailException, UserGroupManagementException {
final String username = "TheNewUser";
securityService.createSimpleUser(username, "u@a.b", "Humba", username, /* company */ null,
/* locale */ null, /* validationBaseURL */ null, /* owning group */ null);
/* locale */ null, /* validationBaseURL */ null, /* owning group */ null, /* clientIP */ null);
final UserGroup defaultUserGroup = securityService.getUserGroupByName(username + SecurityService.TENANT_SUFFIX);
Map<UserGroup, Set<String>> permissionMap = new HashMap<>();
permissionMap.put(defaultUserGroup, new HashSet<>(Arrays.asList(new String[] { "!READ", "UPDATE" })));
@@ -180,7 +180,7 @@ public class LoginTest {
final RoleDefinition adminRoleDefinition = securityService.getOrCreateRoleDefinitionFromPrototype(AdminRole.getInstance(), /* makeReadableForAll */ true);
final UserGroup adminTenant = securityService.getUserGroupByName(admin.getName()+SecurityService.TENANT_SUFFIX);
securityService.createSimpleUser(username, "u@a.b", password, username, /* company */ null,
/* locale */ null, /* validationBaseURL */ null, /* owning group */ null);
/* locale */ null, /* validationBaseURL */ null, /* owning group */ null, /* clientIP */ null);
final UserGroup defaultUserGroup = securityService.getUserGroupByName(username + SecurityService.TENANT_SUFFIX);
final QualifiedObjectIdentifier myId = my.getIdentifier();
// grant admin role to user unqualified, implying READ on all objects including the "my" SERVER
@@ -51,6 +51,7 @@ import com.sap.sse.security.ui.client.UserManagementWriteService;
import com.sap.sse.security.ui.oauth.client.CredentialDTO;
import com.sap.sse.security.ui.oauth.shared.OAuthException;
import com.sap.sse.security.ui.shared.SuccessInfo;
import com.sap.sse.util.HttpRequestUtils;
public class UserManagementWriteServiceImpl extends UserManagementServiceImpl implements UserManagementWriteService {
private static final long serialVersionUID = -8123229851467370537L;
@@ -266,7 +267,8 @@ public class UserManagementWriteServiceImpl extends UserManagementServiceImpl im
public UserDTO createSimpleUser(final String username, final String email, final String password,
final String fullName, final String company, final String localeName, final String validationBaseURL)
throws UserManagementException, MailException, UnauthorizedException {
User user = getSecurityService().checkPermissionForObjectCreationAndRevertOnErrorForUserCreation(username,
final String clientIP = HttpRequestUtils.getClientIP(getThreadLocalRequest());
User user = getSecurityService().checkPermissionForUserCreationAndRevertOnErrorForUserCreation(username,
new Callable<User>() {
@Override
public User call() throws Exception {
@@ -277,7 +279,7 @@ public class UserManagementWriteServiceImpl extends UserManagementServiceImpl im
try {
User newUser = getSecurityService().createSimpleUser(username, email, password, fullName,
company, getLocaleFromLocaleName(localeName), validationBaseURL,
getSecurityService().getDefaultTenantForCurrentUser());
getSecurityService().getDefaultTenantForCurrentUser(), clientIP);
return newUser;
} catch (UserManagementException | UserGroupManagementException e) {
logger.log(Level.SEVERE, "Error creating user " + username, e);
@@ -40,7 +40,7 @@ public class BearerTokenRealm extends AbstractCompositeAuthorizingRealm {
try {
final SecurityService mySecurityService = securityService == null ? null : securityService.get();
if (mySecurityService != null) {
if (mySecurityService.isClientIPAndUserAgentLocked(accessToken.getClientIP())) {
if (mySecurityService.isClientIPLockedForBearerTokenAuthentication(accessToken.getClientIP())) {
throw new LockedAccountException("Authentication for client IP "+accessToken.getClientIP()
+" with user agent "+accessToken.getUserAgent()
+" is currently locked");
@@ -199,9 +199,11 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
*
* @param validationBaseURL
* if <code>null</code>, no validation will be attempted
* @param requestClientIP
* used for throttling user creation requests coming from the same IP address
*/
User createSimpleUser(String username, String email, String password, String fullName, String company,
Locale locale, String validationBaseURL, UserGroup userOwner)
Locale locale, String validationBaseURL, UserGroup userOwner, String requestClientIP)
throws UserManagementException, MailException, UserGroupManagementException;
void updateSimpleUserPassword(String name, String newPassword) throws UserManagementException;
@@ -609,8 +611,8 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
void copyUsersAndRoleAssociations(UserGroup source, UserGroup destination, RoleCopyListener callback);
User checkPermissionForObjectCreationAndRevertOnErrorForUserCreation(String username,
Callable<User> createActionReturningCreatedObject);
User checkPermissionForUserCreationAndRevertOnErrorForUserCreation(String username,
Callable<User> createActionReturningCreatedObject) throws UserManagementException;
/**
* Do only use this, if it is not possible to get the actual instance of the object to delete using the
@@ -871,7 +873,7 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
* (typically one second). After the locking duration expires, the record of the {@code clientIP/userAgent}
* combination is kept around for another locking duration, so that if another call to this method with an equal
* combination of {@code clientIP} and {@code userAgent} is made, that combination will remain
* {@link #isClientIPAndUserAgentLocked(String) locked}, and the locking duration is increased.
* {@link #isClientIPLockedForBearerTokenAuthentication(String) locked}, and the locking duration is increased.
* <p>
*
* If two locking durations have expired without this method being invoked for equal {@cod eclientIP} and
@@ -883,7 +885,7 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
/**
* Call this when the combination of {@code clientIP} and {@code userAgent} was not
* {@link #isClientIPAndUserAgentLocked(String) locked} and a successful bearer token-based
* {@link #isClientIPLockedForBearerTokenAuthentication(String) locked} and a successful bearer token-based
* authentication attempt was made. This will remove the locking record for the combination,
* and in case of an unsuccessful future attempt the locking duration will start at
* the low default.
@@ -898,5 +900,5 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
* of {@code clientIP} and {@code userAgent}. Invoking {@link #failedBearerTokenAuthentication(String)}
* will establish (if not yet locked) or extend the locking duration for the combination.
*/
boolean isClientIPAndUserAgentLocked(String clientIP);
boolean isClientIPLockedForBearerTokenAuthentication(String clientIP);
}
@@ -129,4 +129,6 @@ public interface ReplicableSecurityService extends SecurityService {
Void internalSuccessfulBearerTokenAuthentication(String clientIP);
LockingAndBanning internalFailedBearerTokenAuthentication(String clientIP);
LockingAndBanning internalRecordUserCreationFromClientIP(String clientIP);
}
@@ -58,6 +58,7 @@ import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.env.BasicIniEnvironment;
import org.apache.shiro.env.Environment;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.env.IniWebEnvironment;
@@ -83,6 +84,8 @@ import org.scribe.model.Token;
import org.scribe.oauth.OAuthService;
import com.sap.sse.ServerInfo;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.http.HttpHeaderUtil;
@@ -189,6 +192,7 @@ import com.sap.sse.security.util.RemoteServerUtil;
import com.sap.sse.shared.classloading.ClassLoaderRegistry;
import com.sap.sse.shared.util.impl.ApproximateTime;
import com.sap.sse.util.ClearStateTestSupport;
import com.sap.sse.util.HttpRequestUtils;
import com.sap.sse.util.ThreadPoolUtil;
public class SecurityServiceImpl
@@ -253,9 +257,39 @@ implements ReplicableSecurityService, ClearStateTestSupport {
private final ClassLoaderRegistry initialLoadClassLoaderRegistry = ClassLoaderRegistry.createInstance();
private final ConcurrentMap<String, LockingAndBanning> clientIPBasedLockingAndBanning;
/**
* Contains locking objects keyed by client IP addresses that describe which client IPs are currently locked
* from bearer token-based authentication due to previously failing requests. Requests for which a client
* IP address could not be determined are keyed with the {@link #CLIENT_IP_NULL_ESCAPE} key.<p>
*
* When entering values into this map, the method entering it is responsible for also scheduling a background
* task that a while after lock expiry the record is expunged again from the map to avoid garbage piling up.
*
* @see #failedBearerTokenAuthentication(String)
* @see #successfulBearerTokenAuthentication(String)
* @see #isClientIPLockedForBearerTokenAuthentication(String)
*/
private final ConcurrentMap<String, LockingAndBanning> clientIPBasedLockingAndBanningForBearerTokenAuthentication;
private final static String CLIENT_IP_NULL_ESCAPE = UUID.randomUUID().toString();
/**
* Contains locking objects keyed by client IP addresses ({@code null} not allowed) that describe which client IPs
* are locked for {@link User} creation, e.g., through the
* {@link #createSimpleUser(String, String, String, String, String, Locale, String, UserGroup, String)
* createSimpleUser} method.<p>
*
* When entering values into this map, the method entering it is responsible for also scheduling a background
* task that a while after lock expiry the record is expunged again from the map to avoid garbage piling up.
*/
private final ConcurrentMap<String, LockingAndBanning> clientIPBasedLockingAndBanningForUserCreation;
/**
* The default locking duration per client IP address for user creation.
*
* @see HttpRequestUtils#getClientIP(HttpServletRequest)
*/
private final static Duration DEFAULT_CLIENT_IP_BASED_USER_CREATION_LOCKING_DURATION = Duration.ONE_MINUTE;
/**
* When working with a user's subscriptions, such as first reading, then changing and updating a user's subscription
* based on what was read, a user-specific write lock must be obtained to ensure that no writes can cut in between.
@@ -303,7 +337,8 @@ implements ReplicableSecurityService, ClearStateTestSupport {
throw new IllegalArgumentException("No HasPermissionsProvider defined");
}
logger.info("Initializing Security Service with user store " + userStore);
this.clientIPBasedLockingAndBanning = new ConcurrentHashMap<>();
this.clientIPBasedLockingAndBanningForBearerTokenAuthentication = new ConcurrentHashMap<>();
this.clientIPBasedLockingAndBanningForUserCreation = new ConcurrentHashMap<>();
this.permissionChangeListeners = new PermissionChangeListeners(this);
this.sharedAcrossSubdomainsOf = sharedAcrossSubdomainsOf;
this.subscriptionPlanProvider = subscriptionPlanProvider;
@@ -427,7 +462,7 @@ implements ReplicableSecurityService, ClearStateTestSupport {
final User adminUser = createSimpleUser(UserStore.ADMIN_USERNAME, "nobody@sapsailing.com",
ADMIN_DEFAULT_PASSWORD,
/* fullName */ null, /* company */ null, Locale.ENGLISH, /* validationBaseURL */ null,
null);
null, /* clientIP */ null);
setOwnership(adminUser.getIdentifier(), adminUser, null);
Role adminRole = new Role(adminRoleDefinition, /* transitive */ true);
addRoleForUserAndSetUserAsOwner(adminUser, adminRole);
@@ -1041,8 +1076,11 @@ implements ReplicableSecurityService, ClearStateTestSupport {
@Override
public User createSimpleUser(final String username, final String email, String password, String fullName,
String company, Locale locale, final String validationBaseURL, UserGroup groupOwningUser)
throws UserManagementException, MailException, UserGroupManagementException {
String company, Locale locale, final String validationBaseURL, UserGroup groupOwningUser,
String requestClientIP) throws UserManagementException, MailException, UserGroupManagementException {
if (requestClientIP != null) {
checkAndRecordUserCreationFromClientIP(requestClientIP);
}
logger.info("Creating user "+username);
if (store.getUserByName(username) != null) {
logger.warning("User "+username+" already exists");
@@ -1069,6 +1107,37 @@ implements ReplicableSecurityService, ClearStateTestSupport {
updateSimpleUserEmail(username, email, validationBaseURL);
return result;
}
/**
* Checks if the {@code clientIP} is currently blocked for user creation, and if so, throws a
* {@link UserManagementException}. If the check is successful, it records a locking record for
* this client IP, similar to the locking/banning that happens for failer bearer token
* authentication requests (see {@link #failedBearerTokenAuthentication(String)}).
*/
private void checkAndRecordUserCreationFromClientIP(String clientIP) throws UserManagementException {
assert clientIP != null;
// synchronize to ensure that no two threads can enter values into the map concurrently;
// still the use of a ConcurrentMap is justified because there may be concurrent write access
// through replication
synchronized (clientIPBasedLockingAndBanningForUserCreation) {
final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanningForUserCreation.get(clientIP);
if (lockingAndBanning == null || !lockingAndBanning.isAuthenticationLocked()) {
apply(s->s.internalRecordUserCreationFromClientIP(clientIP));
} else {
throw new UserManagementException("Client IP "+clientIP+" locked for user creation: "+lockingAndBanning);
}
}
}
@Override
public LockingAndBanning internalRecordUserCreationFromClientIP(String clientIP) {
final LockingAndBanning result = new LockingAndBanningImpl(TimePoint.now().plus(DEFAULT_CLIENT_IP_BASED_USER_CREATION_LOCKING_DURATION),
DEFAULT_CLIENT_IP_BASED_USER_CREATION_LOCKING_DURATION);
clientIPBasedLockingAndBanningForUserCreation.put(clientIP, result);
scheduleCleanUpTask(clientIP, result, clientIPBasedLockingAndBanningForUserCreation,
"client IPs locked for user creation");
return result;
}
private void addUserRoleToUser(final User user) {
addRoleForUserAndSetUserAsOwner(user, new Role(store.getRoleDefinitionByPrototype(UserRole.getInstance()),
@@ -1226,32 +1295,43 @@ implements ReplicableSecurityService, ClearStateTestSupport {
@Override
public LockingAndBanning internalFailedBearerTokenAuthentication(String clientIP) {
final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanning.computeIfAbsent(escapeNullClientIP(clientIP), key->new LockingAndBanningImpl());
final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanningForBearerTokenAuthentication.computeIfAbsent(escapeNullClientIP(clientIP), key->new LockingAndBanningImpl());
lockingAndBanning.failedPasswordAuthentication();
// schedule a clean-up task to avoid leaking memory for the LockingAndBanning objects;
// schedule it in two times the locking expiry because if no authentication failure occurs for that IP/user agent
// combination, we will entirely remove the LockingAndBanning from the map, resetting that IP/user agent combination
// to a short default locking duration again; this way, if during the double expiration time another failed authentication
// attempt is registered, we can still grow the locking duration because we have kept the LockingAndBanning
// object available for a bit longer. Furthermore, the BearerTokenRealm will let authentication requests get to here
// only if not locked, so if we were to expunge entries immediately as they unlock, the locking duration could never grow.
scheduleCleanUpTask(clientIP, lockingAndBanning, clientIPBasedLockingAndBanningForBearerTokenAuthentication,
"client IPs locked for bearer token authentication");
return lockingAndBanning;
}
/**
* Schedule a clean-up task to avoid leaking memory for the LockingAndBanning objects; schedule it in two times the
* locking expiry pf {@code lockingAndBanning} because if no authentication failure occurs for that IP/user agent
* combination, we will entirely remove the {@link LockingAndBanning} from the map, effectively resetting that IP to
* a short default locking duration again; this way, if during the double expiration time another failed attempt is
* registered, we can still grow the locking duration because we have kept the {@link LockingAndBanning} object
* available for a bit longer. Furthermore, for authentication requests, the responsible {@link Realm} will let
* authentication requests get to here only if not locked, so if we were to expunge entries immediately as they
* unlock, the locking duration could never grow.
*/
private void scheduleCleanUpTask(final String clientIPOrNull,
final LockingAndBanning lockingAndBanning,
final ConcurrentMap<String, LockingAndBanning> mapToRemoveFrom,
final String nameOfMapForLog) {
final long millisUntilLockingExpiry = 2*ApproximateTime.approximateNow().until(lockingAndBanning.getLockedUntil()).asMillis();
if (millisUntilLockingExpiry > 0) {
ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().schedule(
()->{
final LockingAndBanning lab = clientIPBasedLockingAndBanning.get(escapeNullClientIP(clientIP));
final LockingAndBanning lab = mapToRemoveFrom.get(escapeNullClientIP(clientIPOrNull));
if (lab != null && !lab.isAuthenticationLocked()) {
clientIPBasedLockingAndBanning.remove(escapeNullClientIP(clientIP));
logger.info("Removed client IP authentication lock for "+clientIP+"; "
+clientIPBasedLockingAndBanning.size()
+" locked client IPs remaining");
mapToRemoveFrom.remove(escapeNullClientIP(clientIPOrNull));
logger.info("Removed "+clientIPOrNull+" from "+nameOfMapForLog+"; "
+mapToRemoveFrom.size()
+" locked client IP(s) remaining");
}
},
millisUntilLockingExpiry, TimeUnit.MILLISECONDS);
} else { // a bit weird because we just locked it; suggests very slow execution; yet, let's clean up...
clientIPBasedLockingAndBanning.remove(escapeNullClientIP(clientIP));
mapToRemoveFrom.remove(escapeNullClientIP(clientIPOrNull));
}
return lockingAndBanning;
}
private String escapeNullClientIP(String clientIP) {
@@ -1265,7 +1345,7 @@ implements ReplicableSecurityService, ClearStateTestSupport {
@Override
public Void internalSuccessfulBearerTokenAuthentication(String clientIP) {
final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanning.remove(escapeNullClientIP(clientIP));
final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanningForBearerTokenAuthentication.remove(escapeNullClientIP(clientIP));
if (lockingAndBanning != null) {
logger.info("Unlocked bearer token authentication from "+clientIP+"; last locking state was "+lockingAndBanning);
}
@@ -1273,8 +1353,8 @@ implements ReplicableSecurityService, ClearStateTestSupport {
}
@Override
public boolean isClientIPAndUserAgentLocked(String clientIP) {
final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanning.get(escapeNullClientIP(clientIP));
public boolean isClientIPLockedForBearerTokenAuthentication(String clientIP) {
final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanningForBearerTokenAuthentication.get(escapeNullClientIP(clientIP));
return lockingAndBanning != null && lockingAndBanning.isAuthenticationLocked();
}
@@ -2125,12 +2205,10 @@ implements ReplicableSecurityService, ClearStateTestSupport {
/**
* Special case for user creation, as no currentUser might exist when registering anonymous, and since a user always
* should own itself as userOwner
*
* @return
*/
@Override
public User checkPermissionForObjectCreationAndRevertOnErrorForUserCreation(String username,
Callable<User> createActionReturningCreatedObject) {
public User checkPermissionForUserCreationAndRevertOnErrorForUserCreation(String username,
Callable<User> createActionReturningCreatedObject) throws UserManagementException {
QualifiedObjectIdentifier identifier = SecuredSecurityTypes.USER
.getQualifiedObjectIdentifier(UserImpl.getTypeRelativeObjectIdentifier(username));
User result = null;
@@ -2140,6 +2218,8 @@ implements ReplicableSecurityService, ClearStateTestSupport {
} catch (AuthorizationException e) {
logger.warning("Unauthorized request to create user with name \""+username+"\": "+e.getMessage());
throw e;
} catch (UserManagementException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -2374,6 +2454,8 @@ implements ReplicableSecurityService, ClearStateTestSupport {
store.clear();
accessControlStore.clear();
corsFilterConfigurationsByReplicaSetName.clear();
clientIPBasedLockingAndBanningForBearerTokenAuthentication.clear();
clientIPBasedLockingAndBanningForUserCreation.clear();
}
@Override
@@ -2455,6 +2537,8 @@ implements ReplicableSecurityService, ClearStateTestSupport {
final SecurityServiceInitialLoadExtensionsDTO initialLoadExtensions = (SecurityServiceInitialLoadExtensionsDTO) is.readObject();
final ConcurrentMap<String, Pair<Boolean, Set<String>>> newCORSFilterConfigurations = initialLoadExtensions.getCorsFilterConfigurationsByReplicaSetName();
corsFilterConfigurationsByReplicaSetName.putAll(newCORSFilterConfigurations);
clientIPBasedLockingAndBanningForBearerTokenAuthentication.putAll(initialLoadExtensions.getClientIPBasedLockingAndBanningForBearerTokenAuthentication());
clientIPBasedLockingAndBanningForUserCreation.putAll(initialLoadExtensions.getClientIPBasedLockingAndBanningForUserCreation());
logger.info("Triggering SecurityInitializationCustomizers upon replication ...");
customizers.forEach(c -> c.customizeSecurityService(this));
logger.info("Done filling SecurityService");
@@ -2467,7 +2551,10 @@ implements ReplicableSecurityService, ClearStateTestSupport {
objectOutputStream.writeObject(accessControlStore);
objectOutputStream.writeObject(sharedAcrossSubdomainsOf);
objectOutputStream.writeObject(baseUrlForCrossDomainStorage);
objectOutputStream.writeObject(new SecurityServiceInitialLoadExtensionsDTO(corsFilterConfigurationsByReplicaSetName));
objectOutputStream.writeObject(new SecurityServiceInitialLoadExtensionsDTO(
corsFilterConfigurationsByReplicaSetName,
clientIPBasedLockingAndBanningForBearerTokenAuthentication,
clientIPBasedLockingAndBanningForUserCreation));
}
@Override
@@ -9,6 +9,7 @@ import java.util.concurrent.ConcurrentMap;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.replication.Replicable;
import com.sap.sse.security.SecurityService;
import com.sap.sse.security.shared.impl.LockingAndBanning;
/**
* Starting with the CORS filter configurations, this and future extensions of the {@link SecurityService}'s
@@ -29,12 +30,28 @@ public class SecurityServiceInitialLoadExtensionsDTO implements Serializable {
private final ConcurrentMap<String, Pair<Boolean, Set<String>>> corsFilterConfigurationsByReplicaSetName;
public SecurityServiceInitialLoadExtensionsDTO(ConcurrentMap<String, Pair<Boolean, Set<String>>> corsFilterConfigurationsByReplicaSetName) {
private final ConcurrentMap<String, LockingAndBanning> clientIPBasedLockingAndBanningForBearerTokenAuthentication;
private final ConcurrentMap<String, LockingAndBanning> clientIPBasedLockingAndBanningForUserCreation;
public SecurityServiceInitialLoadExtensionsDTO(ConcurrentMap<String, Pair<Boolean, Set<String>>> corsFilterConfigurationsByReplicaSetName,
ConcurrentMap<String, LockingAndBanning> clientIPBasedLockingAndBanningForBearerTokenAuthentication,
ConcurrentMap<String, LockingAndBanning> clientIPBasedLockingAndBanningForUserCreation) {
super();
this.corsFilterConfigurationsByReplicaSetName = corsFilterConfigurationsByReplicaSetName;
this.clientIPBasedLockingAndBanningForBearerTokenAuthentication = clientIPBasedLockingAndBanningForBearerTokenAuthentication;
this.clientIPBasedLockingAndBanningForUserCreation = clientIPBasedLockingAndBanningForUserCreation;
}
ConcurrentMap<String, Pair<Boolean, Set<String>>> getCorsFilterConfigurationsByReplicaSetName() {
return corsFilterConfigurationsByReplicaSetName;
}
ConcurrentMap<String, LockingAndBanning> getClientIPBasedLockingAndBanningForBearerTokenAuthentication() {
return clientIPBasedLockingAndBanningForBearerTokenAuthentication;
}
ConcurrentMap<String, LockingAndBanning> getClientIPBasedLockingAndBanningForUserCreation() {
return clientIPBasedLockingAndBanningForUserCreation;
}
}
@@ -6,6 +6,7 @@ import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
@@ -43,6 +44,7 @@ import com.sap.sse.security.shared.impl.Role;
import com.sap.sse.security.shared.impl.SecuredSecurityTypes;
import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.shared.impl.UserGroup;
import com.sap.sse.util.HttpRequestUtils;
import com.sun.jersey.api.client.ClientResponse.Status;
@Path(SecurityResource.RESTSECURITY)
@@ -185,9 +187,11 @@ public class SecurityResource extends AbstractSecurityResource {
@QueryParam(EMAIL) String queryEmail, @FormParam(EMAIL) String formEmail,
@QueryParam(PASSWORD) String queryPassword, @FormParam(PASSWORD) String formPassword,
@QueryParam(FULL_NAME) String queryFullName, @FormParam(FULL_NAME) String formFullName,
@QueryParam(COMPANY) String queryCompany, @FormParam(COMPANY) String formCompany) {
@QueryParam(COMPANY) String queryCompany, @FormParam(COMPANY) String formCompany,
@Context HttpServletRequest request) {
try {
User user = getSecurityService().checkPermissionForObjectCreationAndRevertOnErrorForUserCreation(queryUsername,
final String clientIP = HttpRequestUtils.getClientIP(request);
User user = getSecurityService().checkPermissionForUserCreationAndRevertOnErrorForUserCreation(queryUsername,
new Callable<User>() {
@Override
public User call() throws Exception {
@@ -198,7 +202,7 @@ public class SecurityResource extends AbstractSecurityResource {
final String fullNameToUse = preferFirstIfNotNullOrElseSecond(formFullName, queryFullName);
final String companyToUse = preferFirstIfNotNullOrElseSecond(formCompany, queryCompany);
User newUser = getSecurityService().createSimpleUser(usernameToUse, emailToUse, passwordToUse, fullNameToUse, companyToUse,
Locale.ENGLISH, validationBaseURL, getSecurityService().getDefaultTenantForCurrentUser());
Locale.ENGLISH, validationBaseURL, getSecurityService().getDefaultTenantForCurrentUser(), clientIP);
SecurityUtils.getSubject().login(new UsernamePasswordToken(usernameToUse, passwordToUse));
return newUser;
}