mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-17 11:19:15 +00:00
bug6097: apply same mechanism for user creation per IP locking as for failed bearer token-based authentication
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+6
-6
@@ -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);
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+18
-1
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user