mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-07 19:22:14 +00:00
Flatten ServerHttpSecurity.oauth2()
Fixes: gh-5712
This commit is contained in:
parent
59cdfc7d6e
commit
0dc80aed40
@ -207,7 +207,9 @@ public class ServerHttpSecurity {
|
|||||||
|
|
||||||
private OAuth2LoginSpec oauth2Login;
|
private OAuth2LoginSpec oauth2Login;
|
||||||
|
|
||||||
private OAuth2Spec oauth2;
|
private OAuth2ResourceServerSpec resourceServer;
|
||||||
|
|
||||||
|
private OAuth2ClientSpec client;
|
||||||
|
|
||||||
private LogoutSpec logout = new LogoutSpec();
|
private LogoutSpec logout = new LogoutSpec();
|
||||||
|
|
||||||
@ -573,240 +575,188 @@ public class ServerHttpSecurity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures OAuth2 support. An example configuration is provided below:
|
* Configures the OAuth2 client.
|
||||||
*
|
*
|
||||||
* <pre class="code">
|
* <pre class="code">
|
||||||
* @Bean
|
* @Bean
|
||||||
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||||
* http
|
* http
|
||||||
* // ...
|
* // ...
|
||||||
* .oauth2()
|
* .oauth2Client()
|
||||||
* .resourceServer()
|
* .clientRegistrationRepository(clientRegistrationRepository)
|
||||||
* .jwt()
|
* .authorizedClientRepository(authorizedClientRepository);
|
||||||
* .jwkSetUri(jwkSetUri);
|
|
||||||
* return http.build();
|
* return http.build();
|
||||||
* }
|
* }
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* @return the {@link HttpBasicSpec} to customize
|
*
|
||||||
|
* @return the {@link OAuth2ClientSpec} to customize
|
||||||
*/
|
*/
|
||||||
public OAuth2Spec oauth2() {
|
public OAuth2ClientSpec oauth2Client() {
|
||||||
if (this.oauth2 == null) {
|
if (this.client == null) {
|
||||||
this.oauth2 = new OAuth2Spec();
|
this.client = new OAuth2ClientSpec();
|
||||||
}
|
}
|
||||||
return this.oauth2;
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OAuth2ClientSpec {
|
||||||
|
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
||||||
|
|
||||||
|
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the {@link ReactiveClientRegistrationRepository}. Default is to look the value up as a Bean.
|
||||||
|
* @param clientRegistrationRepository the repository to use
|
||||||
|
* @return the {@link OAuth2ClientSpec} to customize
|
||||||
|
*/
|
||||||
|
public OAuth2ClientSpec clientRegistrationRepository(ReactiveClientRegistrationRepository clientRegistrationRepository) {
|
||||||
|
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the {@link ReactiveClientRegistrationRepository}. Default is to look the value up as a Bean.
|
||||||
|
* @param authorizedClientRepository the repository to use
|
||||||
|
* @return the {@link OAuth2ClientSpec} to customize
|
||||||
|
*/
|
||||||
|
public OAuth2ClientSpec authorizedClientRepository(ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||||
|
this.authorizedClientRepository = authorizedClientRepository;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void configure(ServerHttpSecurity http) {
|
||||||
|
ReactiveClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository();
|
||||||
|
ServerOAuth2AuthorizedClientRepository authorizedClientRepository = getAuthorizedClientRepository();
|
||||||
|
ReactiveAuthenticationManager authenticationManager = new OAuth2AuthorizationCodeReactiveAuthenticationManager(new WebClientReactiveAuthorizationCodeTokenResponseClient());
|
||||||
|
OAuth2AuthorizationCodeGrantWebFilter codeGrantWebFilter = new OAuth2AuthorizationCodeGrantWebFilter(authenticationManager,
|
||||||
|
clientRegistrationRepository,
|
||||||
|
authorizedClientRepository);
|
||||||
|
|
||||||
|
OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = new OAuth2AuthorizationRequestRedirectWebFilter(
|
||||||
|
clientRegistrationRepository);
|
||||||
|
http.addFilterAt(codeGrantWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||||
|
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReactiveClientRegistrationRepository getClientRegistrationRepository() {
|
||||||
|
if (this.clientRegistrationRepository != null) {
|
||||||
|
return this.clientRegistrationRepository;
|
||||||
|
}
|
||||||
|
return getBeanOrNull(ReactiveClientRegistrationRepository.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
|
||||||
|
if (this.authorizedClientRepository != null) {
|
||||||
|
return this.authorizedClientRepository;
|
||||||
|
}
|
||||||
|
ServerOAuth2AuthorizedClientRepository result = getBeanOrNull(ServerOAuth2AuthorizedClientRepository.class);
|
||||||
|
if (result == null) {
|
||||||
|
ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService();
|
||||||
|
if (authorizedClientService != null) {
|
||||||
|
result = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(
|
||||||
|
authorizedClientService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReactiveOAuth2AuthorizedClientService getAuthorizedClientService() {
|
||||||
|
ReactiveOAuth2AuthorizedClientService service = getBeanOrNull(ReactiveOAuth2AuthorizedClientService.class);
|
||||||
|
if (service == null) {
|
||||||
|
service = new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository());
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2ClientSpec() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2ResourceServerSpec oauth2ResourceServer() {
|
||||||
|
if (this.resourceServer == null) {
|
||||||
|
this.resourceServer = new OAuth2ResourceServerSpec();
|
||||||
|
}
|
||||||
|
return this.resourceServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures OAuth2 Support
|
* Configures OAuth2 Resource Server Support
|
||||||
*
|
|
||||||
* @since 5.1
|
|
||||||
*/
|
*/
|
||||||
public class OAuth2Spec {
|
public class OAuth2ResourceServerSpec {
|
||||||
private OAuth2ResourceServerSpec resourceServer;
|
private JwtSpec jwt;
|
||||||
|
|
||||||
private OAuth2ClientSpec client;
|
public JwtSpec jwt() {
|
||||||
|
if (this.jwt == null) {
|
||||||
/**
|
this.jwt = new JwtSpec();
|
||||||
* Configures the OAuth2 client.
|
|
||||||
*
|
|
||||||
* <pre class="code">
|
|
||||||
* @Bean
|
|
||||||
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
|
||||||
* http
|
|
||||||
* // ...
|
|
||||||
* .oauth2()
|
|
||||||
* .client()
|
|
||||||
* .clientRegistrationRepository(clientRegistrationRepository)
|
|
||||||
* .authorizedClientRepository(authorizedClientRepository);
|
|
||||||
* return http.build();
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return the {@link OAuth2ClientSpec} to customize
|
|
||||||
*/
|
|
||||||
public OAuth2ClientSpec client() {
|
|
||||||
if (this.client == null) {
|
|
||||||
this.client = new OAuth2ClientSpec();
|
|
||||||
}
|
}
|
||||||
return this.client;
|
return this.jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OAuth2ClientSpec {
|
protected void configure(ServerHttpSecurity http) {
|
||||||
private ReactiveClientRegistrationRepository clientRegistrationRepository;
|
if (this.jwt != null) {
|
||||||
|
this.jwt.configure(http);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
/**
|
||||||
|
* Configures JWT Resource Server Support
|
||||||
|
*/
|
||||||
|
public class JwtSpec {
|
||||||
|
private ReactiveJwtDecoder jwtDecoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the {@link ReactiveClientRegistrationRepository}. Default is to look the value up as a Bean.
|
* Configures the {@link ReactiveJwtDecoder} to use
|
||||||
* @param clientRegistrationRepository the repository to use
|
* @param jwtDecoder the decoder to use
|
||||||
* @return the {@link OAuth2ClientSpec} to customize
|
* @return the {@code JwtSpec} for additional configuration
|
||||||
*/
|
*/
|
||||||
public OAuth2ClientSpec clientRegistrationRepository(ReactiveClientRegistrationRepository clientRegistrationRepository) {
|
public JwtSpec jwtDecoder(ReactiveJwtDecoder jwtDecoder) {
|
||||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
this.jwtDecoder = jwtDecoder;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the {@link ReactiveClientRegistrationRepository}. Default is to look the value up as a Bean.
|
* Configures a {@link ReactiveJwtDecoder} that leverages the provided {@link RSAPublicKey}
|
||||||
* @param authorizedClientRepository the repository to use
|
*
|
||||||
* @return the {@link OAuth2ClientSpec} to customize
|
* @param publicKey the public key to use.
|
||||||
|
* @return the {@code JwtSpec} for additional configuration
|
||||||
*/
|
*/
|
||||||
public OAuth2ClientSpec authorizedClientRepository(ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
public JwtSpec publicKey(RSAPublicKey publicKey) {
|
||||||
this.authorizedClientRepository = authorizedClientRepository;
|
this.jwtDecoder = new NimbusReactiveJwtDecoder(publicKey);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void configure(ServerHttpSecurity http) {
|
|
||||||
ReactiveClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository();
|
|
||||||
ServerOAuth2AuthorizedClientRepository authorizedClientRepository = getAuthorizedClientRepository();
|
|
||||||
ReactiveAuthenticationManager authenticationManager = new OAuth2AuthorizationCodeReactiveAuthenticationManager(new WebClientReactiveAuthorizationCodeTokenResponseClient());
|
|
||||||
OAuth2AuthorizationCodeGrantWebFilter codeGrantWebFilter = new OAuth2AuthorizationCodeGrantWebFilter(authenticationManager,
|
|
||||||
clientRegistrationRepository,
|
|
||||||
authorizedClientRepository);
|
|
||||||
|
|
||||||
OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = new OAuth2AuthorizationRequestRedirectWebFilter(
|
|
||||||
clientRegistrationRepository);
|
|
||||||
http.addFilterAt(codeGrantWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
|
|
||||||
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ReactiveClientRegistrationRepository getClientRegistrationRepository() {
|
|
||||||
if (this.clientRegistrationRepository != null) {
|
|
||||||
return this.clientRegistrationRepository;
|
|
||||||
}
|
|
||||||
return getBeanOrNull(ReactiveClientRegistrationRepository.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
|
|
||||||
if (this.authorizedClientRepository != null) {
|
|
||||||
return this.authorizedClientRepository;
|
|
||||||
}
|
|
||||||
ServerOAuth2AuthorizedClientRepository result = getBeanOrNull(ServerOAuth2AuthorizedClientRepository.class);
|
|
||||||
if (result == null) {
|
|
||||||
ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService();
|
|
||||||
if (authorizedClientService != null) {
|
|
||||||
result = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(
|
|
||||||
authorizedClientService);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ReactiveOAuth2AuthorizedClientService getAuthorizedClientService() {
|
|
||||||
ReactiveOAuth2AuthorizedClientService service = getBeanOrNull(ReactiveOAuth2AuthorizedClientService.class);
|
|
||||||
if (service == null) {
|
|
||||||
service = new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository());
|
|
||||||
}
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
|
|
||||||
private OAuth2ClientSpec() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public OAuth2ResourceServerSpec resourceServer() {
|
|
||||||
if (this.resourceServer == null) {
|
|
||||||
this.resourceServer = new OAuth2ResourceServerSpec();
|
|
||||||
}
|
|
||||||
return this.resourceServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures OAuth2 Resource Server Support
|
|
||||||
*/
|
|
||||||
public class OAuth2ResourceServerSpec {
|
|
||||||
private JwtSpec jwt;
|
|
||||||
|
|
||||||
public JwtSpec jwt() {
|
|
||||||
if (this.jwt == null) {
|
|
||||||
this.jwt = new JwtSpec();
|
|
||||||
}
|
|
||||||
return this.jwt;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void configure(ServerHttpSecurity http) {
|
|
||||||
if (this.jwt != null) {
|
|
||||||
this.jwt.configure(http);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures JWT Resource Server Support
|
* Configures a {@link ReactiveJwtDecoder} using
|
||||||
|
* <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a> URL
|
||||||
|
* @param jwkSetUri the URL to use.
|
||||||
|
* @return the {@code JwtSpec} for additional configuration
|
||||||
*/
|
*/
|
||||||
public class JwtSpec {
|
public JwtSpec jwkSetUri(String jwkSetUri) {
|
||||||
private ReactiveJwtDecoder jwtDecoder;
|
this.jwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUri);
|
||||||
|
return this;
|
||||||
/**
|
|
||||||
* Configures the {@link ReactiveJwtDecoder} to use
|
|
||||||
* @param jwtDecoder the decoder to use
|
|
||||||
* @return the {@code JwtSpec} for additional configuration
|
|
||||||
*/
|
|
||||||
public JwtSpec jwtDecoder(ReactiveJwtDecoder jwtDecoder) {
|
|
||||||
this.jwtDecoder = jwtDecoder;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures a {@link ReactiveJwtDecoder} that leverages the provided {@link RSAPublicKey}
|
|
||||||
*
|
|
||||||
* @param publicKey the public key to use.
|
|
||||||
* @return the {@code JwtSpec} for additional configuration
|
|
||||||
*/
|
|
||||||
public JwtSpec publicKey(RSAPublicKey publicKey) {
|
|
||||||
this.jwtDecoder = new NimbusReactiveJwtDecoder(publicKey);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures a {@link ReactiveJwtDecoder} using
|
|
||||||
* <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a> URL
|
|
||||||
* @param jwkSetUri the URL to use.
|
|
||||||
* @return the {@code JwtSpec} for additional configuration
|
|
||||||
*/
|
|
||||||
public JwtSpec jwkSetUri(String jwkSetUri) {
|
|
||||||
this.jwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUri);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OAuth2ResourceServerSpec and() {
|
|
||||||
return OAuth2ResourceServerSpec.this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void configure(ServerHttpSecurity http) {
|
|
||||||
BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint();
|
|
||||||
JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager(
|
|
||||||
this.jwtDecoder);
|
|
||||||
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
|
||||||
oauth2.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
|
|
||||||
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
|
|
||||||
http
|
|
||||||
.exceptionHandling()
|
|
||||||
.authenticationEntryPoint(entryPoint)
|
|
||||||
.and()
|
|
||||||
.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public OAuth2Spec and() {
|
public OAuth2ResourceServerSpec and() {
|
||||||
return OAuth2Spec.this;
|
return OAuth2ResourceServerSpec.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void configure(ServerHttpSecurity http) {
|
||||||
|
BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint();
|
||||||
|
JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager(
|
||||||
|
this.jwtDecoder);
|
||||||
|
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
||||||
|
oauth2.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
|
||||||
|
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
|
||||||
|
http
|
||||||
|
.exceptionHandling()
|
||||||
|
.authenticationEntryPoint(entryPoint)
|
||||||
|
.and()
|
||||||
|
.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerHttpSecurity and() {
|
public ServerHttpSecurity and() {
|
||||||
return ServerHttpSecurity.this;
|
return ServerHttpSecurity.this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void configure(ServerHttpSecurity http) {
|
|
||||||
if (this.resourceServer != null) {
|
|
||||||
this.resourceServer.configure(http);
|
|
||||||
}
|
|
||||||
if (this.client != null) {
|
|
||||||
this.client.configure(http);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private OAuth2Spec() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1009,8 +959,11 @@ public class ServerHttpSecurity {
|
|||||||
if (this.oauth2Login != null) {
|
if (this.oauth2Login != null) {
|
||||||
this.oauth2Login.configure(this);
|
this.oauth2Login.configure(this);
|
||||||
}
|
}
|
||||||
if (this.oauth2 != null) {
|
if (this.resourceServer != null) {
|
||||||
this.oauth2.configure(this);
|
this.resourceServer.configure(this);
|
||||||
|
}
|
||||||
|
if (this.client != null) {
|
||||||
|
this.client.configure(this);
|
||||||
}
|
}
|
||||||
this.loginPage.configure(this);
|
this.loginPage.configure(this);
|
||||||
if(this.logout != null) {
|
if(this.logout != null) {
|
||||||
|
@ -95,8 +95,7 @@ public class OAuth2ClientSpecTests {
|
|||||||
@Bean
|
@Bean
|
||||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||||
http
|
http
|
||||||
.oauth2()
|
.oauth2Client();
|
||||||
.client();
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,8 +40,7 @@ public class SecurityConfig {
|
|||||||
.and()
|
.and()
|
||||||
.formLogin()
|
.formLogin()
|
||||||
.and()
|
.and()
|
||||||
.oauth2()
|
.oauth2Client();
|
||||||
.client();
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,10 +45,9 @@ public class SecurityConfig {
|
|||||||
.authorizeExchange()
|
.authorizeExchange()
|
||||||
.anyExchange().authenticated()
|
.anyExchange().authenticated()
|
||||||
.and()
|
.and()
|
||||||
.oauth2()
|
.oauth2ResourceServer()
|
||||||
.resourceServer()
|
.jwt()
|
||||||
.jwt()
|
.jwkSetUri(jwkSetUri);
|
||||||
.jwkSetUri(jwkSetUri);
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,10 +58,9 @@ public class SecurityConfig {
|
|||||||
.authorizeExchange()
|
.authorizeExchange()
|
||||||
.anyExchange().authenticated()
|
.anyExchange().authenticated()
|
||||||
.and()
|
.and()
|
||||||
.oauth2()
|
.oauth2ResourceServer()
|
||||||
.resourceServer()
|
.jwt()
|
||||||
.jwt()
|
.publicKey(publicKey());
|
||||||
.publicKey(publicKey());
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user