Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public enum ClientExceptionType {
RealmContains(Response.Status.BAD_REQUEST),
RequiredValuesMissing(Response.Status.BAD_REQUEST),
RESTValidation(Response.Status.BAD_REQUEST),
TooManyRequests(Response.Status.TOO_MANY_REQUESTS),
Management(Response.Status.BAD_REQUEST),
InUse(Response.Status.BAD_REQUEST),
Scheduling(Response.Status.BAD_REQUEST),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@

import static org.mockito.Mockito.mock;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
import org.apache.syncope.common.keymaster.client.api.DomainOps;
import org.apache.syncope.common.keymaster.client.api.ServiceOps;
Expand All @@ -37,6 +39,8 @@
import org.apache.syncope.core.provisioning.api.ImplementationLookup;
import org.apache.syncope.core.provisioning.java.ProvisioningContext;
import org.apache.syncope.core.spring.security.SecurityContext;
import org.apache.syncope.core.spring.security.throttle.PasswordResetRequestThrottler;
import org.apache.syncope.core.spring.security.throttle.ThrottlerAttempts;
import org.apache.syncope.core.workflow.java.WorkflowContext;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.ConfigurableApplicationContext;
Expand Down Expand Up @@ -105,4 +109,9 @@ public ServiceOps serviceOps() {
public CacheManager cacheManager() {
return Caching.getCachingProvider().getCacheManager();
}

@Bean(name = PasswordResetRequestThrottler.CACHE)
public Cache<String, ThrottlerAttempts> passwordResetRequestThrottlerCache(final CacheManager cacheManager) {
return cacheManager.createCache(PasswordResetRequestThrottler.CACHE, new MutableConfiguration<>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import dev.samstevens.totp.secret.DefaultSecretGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import jakarta.validation.Validator;
import javax.cache.Cache;
import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
import org.apache.syncope.common.keymaster.client.api.DomainOps;
import org.apache.syncope.core.logic.init.ClassPathScanImplementationLookup;
Expand Down Expand Up @@ -101,8 +102,11 @@
import org.apache.syncope.core.provisioning.api.rules.RuleProvider;
import org.apache.syncope.core.provisioning.java.job.SyncopeTaskScheduler;
import org.apache.syncope.core.spring.security.SecurityProperties;
import org.apache.syncope.core.spring.security.throttle.PasswordResetRequestThrottler;
import org.apache.syncope.core.spring.security.throttle.ThrottlerAttempts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -523,7 +527,10 @@ public UserSelfLogic userSelfLogic(
final DelegationDAO delegationDAO,
final AccessTokenDAO accessTokenDAO,
final ExternalResourceDAO resourceDAO,
final RuleProvider ruleProvider) {
final RuleProvider ruleProvider,
final SecurityProperties securityProperties,
@Qualifier(PasswordResetRequestThrottler.CACHE)
final Cache<String, ThrottlerAttempts> passwordResetRequestCache) {

return new UserSelfLogic(
realmSearchDAO,
Expand All @@ -537,7 +544,9 @@ public UserSelfLogic userSelfLogic(
delegationDAO,
accessTokenDAO,
resourceDAO,
ruleProvider);
ruleProvider,
securityProperties,
passwordResetRequestCache);
}

@ConditionalOnMissingBean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.cache.Cache;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
import org.apache.syncope.common.keymaster.client.api.StandardConfParams;
Expand Down Expand Up @@ -62,6 +63,9 @@
import org.apache.syncope.core.spring.policy.AccountPolicyException;
import org.apache.syncope.core.spring.policy.PasswordPolicyException;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.apache.syncope.core.spring.security.SecurityProperties;
import org.apache.syncope.core.spring.security.throttle.PasswordResetRequestThrottler;
import org.apache.syncope.core.spring.security.throttle.ThrottlerAttempts;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -85,6 +89,8 @@ protected static void throwMfaWasSet(final String username) {

protected final RuleProvider ruleProvider;

protected final PasswordResetRequestThrottler passwordResetRequestThrottler;

public UserSelfLogic(
final RealmSearchDAO realmSearchDAO,
final AnyTypeDAO anyTypeDAO,
Expand All @@ -97,7 +103,9 @@ public UserSelfLogic(
final DelegationDAO delegationDAO,
final AccessTokenDAO accessTokenDAO,
final ExternalResourceDAO resourceDAO,
final RuleProvider ruleProvider) {
final RuleProvider ruleProvider,
final SecurityProperties securityProperties,
final Cache<String, ThrottlerAttempts> passwordResetRequestCache) {

super(realmSearchDAO,
anyTypeDAO,
Expand All @@ -111,6 +119,9 @@ public UserSelfLogic(
this.accessTokenDAO = accessTokenDAO;
this.resourceDAO = resourceDAO;
this.ruleProvider = ruleProvider;
this.passwordResetRequestThrottler = new PasswordResetRequestThrottler(
securityProperties,
passwordResetRequestCache);
}

@PreAuthorize("isAuthenticated() "
Expand Down Expand Up @@ -265,7 +276,11 @@ public void compliance(final ComplianceQuery query) {
}

@PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
public void requestPasswordReset(final String username, final String securityAnswer) {
public void requestPasswordReset(
final String username,
final String securityAnswer,
final String clientAddress) {

if (!confParamOps.get(
AuthContextUtils.getDomain(), StandardConfParams.PASSWORD_RESET_ALLOWED, false, boolean.class)) {

Expand All @@ -274,8 +289,10 @@ public void requestPasswordReset(final String username, final String securityAns
throw sce;
}

passwordResetRequestThrottler.recordAndCheck(AuthContextUtils.getDomain(), username, clientAddress);

String key = userDAO.findKey(username).
orElseThrow(() -> new NotFoundException("User " + username));
orElseThrow(() -> new NotFoundException("User"));

if (confParamOps.get(
AuthContextUtils.getDomain(), StandardConfParams.PASSWORD_RESET_SECURITY_QUESTION, false, boolean.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@

import static org.mockito.Mockito.mock;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
import org.apache.syncope.common.keymaster.client.api.DomainOps;
import org.apache.syncope.common.keymaster.client.api.ServiceOps;
Expand All @@ -37,6 +39,8 @@
import org.apache.syncope.core.provisioning.api.ImplementationLookup;
import org.apache.syncope.core.provisioning.java.ProvisioningContext;
import org.apache.syncope.core.spring.security.SecurityContext;
import org.apache.syncope.core.spring.security.throttle.PasswordResetRequestThrottler;
import org.apache.syncope.core.spring.security.throttle.ThrottlerAttempts;
import org.apache.syncope.core.workflow.java.WorkflowContext;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.ConfigurableApplicationContext;
Expand Down Expand Up @@ -104,4 +108,9 @@ public ServiceOps serviceOps() {
public CacheManager cacheManager() {
return Caching.getCachingProvider().getCacheManager();
}

@Bean(name = PasswordResetRequestThrottler.CACHE)
public Cache<String, ThrottlerAttempts> passwordResetRequestThrottlerCache(final CacheManager cacheManager) {
return cacheManager.createCache(PasswordResetRequestThrottler.CACHE, new MutableConfiguration<>());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.syncope.core.logic;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.Optional;
import javax.cache.Cache;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
import org.apache.syncope.common.keymaster.client.api.StandardConfParams;
import org.apache.syncope.core.persistence.api.EncryptorManager;
import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
import org.apache.syncope.core.provisioning.api.rules.RuleProvider;
import org.apache.syncope.core.spring.security.SecurityProperties;
import org.apache.syncope.core.spring.security.throttle.PasswordResetThrottleException;
import org.apache.syncope.core.spring.security.throttle.ThrottlerAttempts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class UserSelfLogicPasswordResetTest {

private static final Cache<String, ThrottlerAttempts> CACHE =
Caching.getCachingProvider().getCacheManager().createCache(
UserSelfLogicPasswordResetTest.class.getName(),
new MutableConfiguration<>());

private UserDAO userDAO;

private SecurityProperties securityProperties;

private UserSelfLogic logic;

@BeforeEach
void setUp() {
ConfParamOps confParamOps = mock(ConfParamOps.class);
userDAO = mock(UserDAO.class);
securityProperties = new SecurityProperties();

when(confParamOps.get(any(), eq(StandardConfParams.PASSWORD_RESET_ALLOWED), eq(false), eq(boolean.class))).
thenReturn(true);

logic = new UserSelfLogic(
mock(RealmSearchDAO.class),
mock(AnyTypeDAO.class),
mock(TemplateUtils.class),
userDAO,
mock(UserDataBinder.class),
mock(UserProvisioningManager.class),
mock(EncryptorManager.class),
confParamOps,
mock(DelegationDAO.class),
mock(AccessTokenDAO.class),
mock(ExternalResourceDAO.class),
mock(RuleProvider.class),
securityProperties,
CACHE);
}

@Test
void passwordResetRequestsAreThrottled() {
securityProperties.getPasswordResetThrottle().setMaxAttempts(1);
when(userDAO.findKey("missing")).thenReturn(Optional.empty());

assertThrows(NotFoundException.class, () -> logic.requestPasswordReset("missing", "answer", "192.0.2.1"));
assertThrows(
PasswordResetThrottleException.class,
() -> logic.requestPasswordReset("missing", "answer", "192.0.2.1"));
assertThrows(NotFoundException.class, () -> logic.requestPasswordReset("missing", "answer", "192.0.2.2"));
}

@Test
void passwordResetThrottlingCanBeDisabled() {
securityProperties.getPasswordResetThrottle().setEnabled(false);
securityProperties.getPasswordResetThrottle().setMaxAttempts(1);
when(userDAO.findKey("missing")).thenReturn(Optional.empty());

assertThrows(NotFoundException.class, () -> logic.requestPasswordReset("missing", "answer", "192.0.2.1"));
assertThrows(NotFoundException.class, () -> logic.requestPasswordReset("missing", "answer", "192.0.2.1"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ public Set<String> getTrustedProxies() {
@NestedConfigurationProperty
private final ExecutorProperties batchExecutor = new ExecutorProperties();

@NestedConfigurationProperty
private final RateLimitProperties rateLimitProperties = new RateLimitProperties();

public ExecutorProperties getBatchExecutor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.entity.PlainAttr;
import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
import org.apache.syncope.core.spring.security.throttle.PasswordResetThrottleException;
import org.apache.syncope.core.workflow.api.WorkflowException;
import org.identityconnectors.framework.common.exceptions.ConfigurationException;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
Expand Down Expand Up @@ -136,6 +137,15 @@ public Response toResponse(final Exception ex) {
builder = sce.isComposite()
? getSyncopeClientCompositeExceptionResponse(sce.asComposite())
: getSyncopeClientExceptionResponse(sce);
} else if (ex instanceof PasswordResetThrottleException
|| ExceptionUtils.getRootCause(ex) instanceof PasswordResetThrottleException) {

PasswordResetThrottleException prte = ex instanceof PasswordResetThrottleException
? (PasswordResetThrottleException) ex
: (PasswordResetThrottleException) ExceptionUtils.getRootCause(ex);

builder = builder(ClientExceptionType.TooManyRequests, prte.getMessage()).
header(HttpHeaders.RETRY_AFTER, prte.getRetryAfterSeconds());
} else if (ex instanceof DelegatedAdministrationException
|| ExceptionUtils.getRootCause(ex) instanceof DelegatedAdministrationException) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public void compliance(final ComplianceQuery query) {

@Override
public void requestPasswordReset(final String username, final String securityAnswer) {
logic.requestPasswordReset(username, securityAnswer);
logic.requestPasswordReset(username, securityAnswer, messageContext.getHttpServletRequest().getRemoteAddr());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.syncope.core.rest.cxf;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;

import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.rest.api.RESTHeaders;
import org.apache.syncope.core.spring.security.throttle.PasswordResetThrottleException;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.Environment;

public class RestServiceExceptionMapperTest {

@Test
public void passwordResetThrottleReturnsTooManyRequests() {
Response response = new RestServiceExceptionMapper(mock(Environment.class)).
toResponse(new PasswordResetThrottleException(42));

assertEquals(Response.Status.TOO_MANY_REQUESTS.getStatusCode(), response.getStatus());
assertEquals("42", response.getHeaderString(HttpHeaders.RETRY_AFTER));
assertEquals(ClientExceptionType.TooManyRequests.name(), response.getHeaderString(RESTHeaders.ERROR_CODE));
}
}
Loading