FINERACT-1724: Web security adjustments
This commit is contained in:
committed by
Arnold Gálovics
parent
c437c5a48b
commit
d87d483665
+31
-28
@@ -23,6 +23,7 @@ import static org.apache.fineract.infrastructure.security.vote.SelfServiceUserAu
|
|||||||
import static org.springframework.security.authorization.AuthenticatedAuthorizationManager.fullyAuthenticated;
|
import static org.springframework.security.authorization.AuthenticatedAuthorizationManager.fullyAuthenticated;
|
||||||
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
|
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
|
||||||
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
|
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
|
||||||
|
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
|
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
|
||||||
@@ -32,6 +33,7 @@ import org.apache.fineract.infrastructure.core.exceptionmapper.OAuth2ExceptionEn
|
|||||||
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
|
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
|
||||||
import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
|
import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
|
||||||
import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
|
import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
|
||||||
|
import org.apache.fineract.infrastructure.security.filter.InsecureTwoFactorAuthenticationFilter;
|
||||||
import org.apache.fineract.infrastructure.security.filter.TenantAwareTenantIdentifierFilter;
|
import org.apache.fineract.infrastructure.security.filter.TenantAwareTenantIdentifierFilter;
|
||||||
import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
|
import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
|
||||||
import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService;
|
import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService;
|
||||||
@@ -40,6 +42,7 @@ import org.apache.fineract.infrastructure.security.service.TwoFactorService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
@@ -73,7 +76,7 @@ public class OAuth2SecurityConfig {
|
|||||||
private ServerProperties serverProperties;
|
private ServerProperties serverProperties;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TwoFactorService twoFactorService;
|
private FineractProperties fineractProperties;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BasicAuthTenantDetailsService basicAuthTenantDetailsService;
|
private BasicAuthTenantDetailsService basicAuthTenantDetailsService;
|
||||||
@@ -89,45 +92,40 @@ public class OAuth2SecurityConfig {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BusinessDateReadPlatformService businessDateReadPlatformService;
|
private BusinessDateReadPlatformService businessDateReadPlatformService;
|
||||||
|
@Autowired
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
private static final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
private static final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain authorizationFilterChain(HttpSecurity http) throws Exception {
|
|
||||||
http //
|
|
||||||
.securityMatcher("/api/**").authorizeHttpRequests((auth) -> {
|
|
||||||
auth.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/echo").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/authentication").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/self/authentication").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/self/registration").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/self/registration/user").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/twofactor/validate").fullyAuthenticated() //
|
|
||||||
.requestMatchers("/api/*/twofactor").fullyAuthenticated() //
|
|
||||||
.requestMatchers("/api/**")
|
|
||||||
.access(allOf(fullyAuthenticated(), hasAuthority("TWOFACTOR_AUTHENTICATED"), selfServiceUserAuthManager())); //
|
|
||||||
});
|
|
||||||
|
|
||||||
if (serverProperties.getSsl().isEnabled()) {
|
|
||||||
http.requiresChannel(channel -> channel.requestMatchers("/api/**").requiresSecure());
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http //
|
http //
|
||||||
.csrf((csrf) -> csrf.disable()) // NOSONAR only creating a service that is used by non-browser clients
|
.securityMatcher(antMatcher("/api/**")).authorizeHttpRequests((auth) -> {
|
||||||
|
auth.requestMatchers(antMatcher(HttpMethod.OPTIONS, "/api/**")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/echo")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/authentication")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/authentication")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration/user")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated() //
|
||||||
|
.requestMatchers(antMatcher("/api/*/twofactor")).fullyAuthenticated() //
|
||||||
|
.requestMatchers(antMatcher("/api/**"))
|
||||||
|
.access(allOf(fullyAuthenticated(), hasAuthority("TWOFACTOR_AUTHENTICATED"), selfServiceUserAuthManager())); //
|
||||||
|
}).csrf((csrf) -> csrf.disable()) // NOSONAR only creating a service that is used by non-browser clients
|
||||||
.exceptionHandling((ehc) -> ehc.authenticationEntryPoint(new OAuth2ExceptionEntryPoint()))
|
.exceptionHandling((ehc) -> ehc.authenticationEntryPoint(new OAuth2ExceptionEntryPoint()))
|
||||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(authenticationConverter()))
|
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(authenticationConverter()))
|
||||||
.authenticationEntryPoint(new OAuth2ExceptionEntryPoint())) //
|
.authenticationEntryPoint(new OAuth2ExceptionEntryPoint())) //
|
||||||
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //
|
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //
|
||||||
.addFilterAfter(tenantAwareTenantIdentifierFilter(), SecurityContextHolderFilter.class) //
|
.addFilterAfter(tenantAwareTenantIdentifierFilter(), SecurityContextHolderFilter.class);
|
||||||
.addFilterAfter(twoFactorAuthenticationFilter(), BasicAuthenticationFilter.class); //
|
|
||||||
|
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
|
||||||
|
http.addFilterAfter(twoFactorAuthenticationFilter(), BasicAuthenticationFilter.class);
|
||||||
|
} else {
|
||||||
|
http.addFilterAfter(insecureTwoFactorAuthenticationFilter(), BasicAuthenticationFilter.class);
|
||||||
|
}
|
||||||
|
|
||||||
if (serverProperties.getSsl().isEnabled()) {
|
if (serverProperties.getSsl().isEnabled()) {
|
||||||
http.requiresChannel(channel -> channel.requestMatchers("/api/**").requiresSecure());
|
http.requiresChannel(channel -> channel.requestMatchers(antMatcher("/api/**")).requiresSecure());
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
@@ -139,9 +137,14 @@ public class OAuth2SecurityConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() {
|
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() {
|
||||||
|
TwoFactorService twoFactorService = applicationContext.getBean(TwoFactorService.class);
|
||||||
return new TwoFactorAuthenticationFilter(twoFactorService);
|
return new TwoFactorAuthenticationFilter(twoFactorService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InsecureTwoFactorAuthenticationFilter insecureTwoFactorAuthenticationFilter() {
|
||||||
|
return new InsecureTwoFactorAuthenticationFilter();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||||
|
|||||||
+23
-30
@@ -22,6 +22,7 @@ package org.apache.fineract.infrastructure.core.config;
|
|||||||
import static org.springframework.security.authorization.AuthenticatedAuthorizationManager.fullyAuthenticated;
|
import static org.springframework.security.authorization.AuthenticatedAuthorizationManager.fullyAuthenticated;
|
||||||
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
|
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
|
||||||
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
|
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
|
||||||
|
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||||
|
|
||||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||||
import org.apache.fineract.commands.service.CommandSourceService;
|
import org.apache.fineract.commands.service.CommandSourceService;
|
||||||
@@ -67,7 +68,6 @@ import org.springframework.security.web.SecurityFilterChain;
|
|||||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||||
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
|
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
|
||||||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty("fineract.security.basicauth.enabled")
|
@ConditionalOnProperty("fineract.security.basicauth.enabled")
|
||||||
@@ -114,48 +114,41 @@ public class SecurityConfig {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IdempotencyStoreHelper idempotencyStoreHelper;
|
private IdempotencyStoreHelper idempotencyStoreHelper;
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain authorizationFilterChain(HttpSecurity http) throws Exception {
|
|
||||||
http //
|
|
||||||
.securityMatcher("/api/**").authorizeHttpRequests((auth) -> {
|
|
||||||
auth.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/echo").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/authentication").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/self/authentication").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/self/registration").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/self/registration/user").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.PUT, "/api/*/instance-mode").permitAll() //
|
|
||||||
.requestMatchers(HttpMethod.POST, "/api/*/twofactor/validate").fullyAuthenticated() //
|
|
||||||
.requestMatchers("/api/*/twofactor").fullyAuthenticated() //
|
|
||||||
.requestMatchers("/api/**").access(allOf(fullyAuthenticated(), hasAuthority("TWOFACTOR_AUTHENTICATED"))); //
|
|
||||||
}) //
|
|
||||||
.httpBasic((httpBasic) -> httpBasic.authenticationEntryPoint(basicAuthenticationEntryPoint()));
|
|
||||||
|
|
||||||
if (serverProperties.getSsl().isEnabled()) {
|
|
||||||
http.requiresChannel(channel -> channel.requestMatchers("/api/**").requiresSecure());
|
|
||||||
}
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http //
|
http //
|
||||||
|
.securityMatcher(antMatcher("/api/**")).authorizeHttpRequests((auth) -> {
|
||||||
|
auth.requestMatchers(antMatcher(HttpMethod.OPTIONS, "/api/**")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/echo")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/authentication")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/authentication")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/self/registration/user")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.PUT, "/api/*/instance-mode")).permitAll() //
|
||||||
|
.requestMatchers(antMatcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated() //
|
||||||
|
.requestMatchers(antMatcher("/api/*/twofactor")).fullyAuthenticated() //
|
||||||
|
.requestMatchers(antMatcher("/api/**"))
|
||||||
|
.access(allOf(fullyAuthenticated(), hasAuthority("TWOFACTOR_AUTHENTICATED"))); //
|
||||||
|
}).httpBasic((httpBasic) -> httpBasic.authenticationEntryPoint(basicAuthenticationEntryPoint())) //
|
||||||
.csrf((csrf) -> csrf.disable()) // NOSONAR only creating a service that is used by non-browser clients
|
.csrf((csrf) -> csrf.disable()) // NOSONAR only creating a service that is used by non-browser clients
|
||||||
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //
|
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //
|
||||||
.addFilterBefore(tenantAwareBasicAuthenticationFilter(), SecurityContextHolderFilter.class)
|
.addFilterBefore(tenantAwareBasicAuthenticationFilter(), SecurityContextHolderFilter.class) //
|
||||||
.addFilterAfter(requestResponseFilter(), ExceptionTranslationFilter.class)
|
.addFilterAfter(requestResponseFilter(), ExceptionTranslationFilter.class) //
|
||||||
.addFilterAfter(correlationHeaderFilter(), RequestResponseFilter.class)
|
.addFilterAfter(correlationHeaderFilter(), RequestResponseFilter.class) //
|
||||||
.addFilterAfter(responseCorsFilter(), CorrelationHeaderFilter.class) //
|
.addFilterAfter(responseCorsFilter(), CorrelationHeaderFilter.class) //
|
||||||
.addFilterAfter(fineractInstanceModeApiFilter(), ResponseCorsFilter.class) //
|
.addFilterAfter(fineractInstanceModeApiFilter(), ResponseCorsFilter.class) //
|
||||||
.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class) //
|
.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class) //
|
||||||
.addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); //
|
.addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); //
|
||||||
|
|
||||||
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
|
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
|
||||||
http.addFilterAfter(twoFactorAuthenticationFilter(), FineractInstanceModeApiFilter.class);
|
http.addFilterAfter(twoFactorAuthenticationFilter(), ResponseCorsFilter.class);
|
||||||
} else {
|
} else {
|
||||||
http.addFilterAfter(insecureTwoFactorAuthenticationFilter(), FineractInstanceModeApiFilter.class);
|
http.addFilterAfter(insecureTwoFactorAuthenticationFilter(), ResponseCorsFilter.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverProperties.getSsl().isEnabled()) {
|
||||||
|
http.requiresChannel(channel -> channel.requestMatchers(antMatcher("/api/**")).requiresSecure());
|
||||||
|
}
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +189,7 @@ public class SecurityConfig {
|
|||||||
TenantAwareBasicAuthenticationFilter filter = new TenantAwareBasicAuthenticationFilter(authenticationManagerBean(),
|
TenantAwareBasicAuthenticationFilter filter = new TenantAwareBasicAuthenticationFilter(authenticationManagerBean(),
|
||||||
basicAuthenticationEntryPoint(), toApiJsonSerializer, configurationDomainService, cacheWritePlatformService,
|
basicAuthenticationEntryPoint(), toApiJsonSerializer, configurationDomainService, cacheWritePlatformService,
|
||||||
userNotificationService, basicAuthTenantDetailsService, businessDateReadPlatformService);
|
userNotificationService, basicAuthTenantDetailsService, businessDateReadPlatformService);
|
||||||
filter.setRequestMatcher(AntPathRequestMatcher.antMatcher("/api/**"));
|
filter.setRequestMatcher(antMatcher("/api/**"));
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
@@ -106,6 +106,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil
|
|||||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||||
// ignore to allow 'preflight' requests from AJAX applications
|
// ignore to allow 'preflight' requests from AJAX applications
|
||||||
// in different origin (domain name)
|
// in different origin (domain name)
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
} else {
|
} else {
|
||||||
if (requestMatcher.matches(request)) {
|
if (requestMatcher.matches(request)) {
|
||||||
String tenantIdentifier = request.getHeader(this.tenantRequestHeader);
|
String tenantIdentifier = request.getHeader(this.tenantRequestHeader);
|
||||||
|
|||||||
-4
@@ -42,9 +42,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
|||||||
import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
|
import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
|
||||||
import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentifierException;
|
import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentifierException;
|
||||||
import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService;
|
import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.filter.GenericFilterBean;
|
import org.springframework.web.filter.GenericFilterBean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,8 +56,6 @@ import org.springframework.web.filter.GenericFilterBean;
|
|||||||
*
|
*
|
||||||
* Used to support Oauth2 authentication and the service is loaded only when "oauth" profile is active.
|
* Used to support Oauth2 authentication and the service is loaded only when "oauth" profile is active.
|
||||||
*/
|
*/
|
||||||
@Service
|
|
||||||
@ConditionalOnProperty("fineract.security.oauth.enabled")
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TenantAwareTenantIdentifierFilter extends GenericFilterBean {
|
public class TenantAwareTenantIdentifierFilter extends GenericFilterBean {
|
||||||
|
|||||||
Reference in New Issue
Block a user