mirror of
https://github.com/NationalSecurityAgency/timely.git
synced 2026-11-10 11:51:29 -10:00
Compare commits
3
Commits
fixHttpClient
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a420d9f89 | ||
|
|
2fbd306a0f | ||
|
|
ac841d3f75 |
No files matched your search
+25
-13
@@ -13,19 +13,31 @@
|
|||||||
<beans:property name="subjectDnRegex" value="CN=(.*?)(?:,|$)"/>
|
<beans:property name="subjectDnRegex" value="CN=(.*?)(?:,|$)"/>
|
||||||
</beans:bean>
|
</beans:bean>
|
||||||
|
|
||||||
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
|
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
|
||||||
<beans:property name="users">
|
<beans:property name="users">
|
||||||
<util:map key-type="java.lang.String">
|
<util:list>
|
||||||
<beans:entry key="CN=example.com">
|
<beans:bean class="timely.auth.TimelyUser">
|
||||||
<util:list>
|
<beans:constructor-arg name="subjectDn">
|
||||||
<beans:value>D</beans:value>
|
<beans:value>CN=example.com</beans:value>
|
||||||
<beans:value>E</beans:value>
|
</beans:constructor-arg>
|
||||||
<beans:value>F</beans:value>
|
<beans:constructor-arg name="auths">
|
||||||
</util:list>
|
<util:list>
|
||||||
</beans:entry>
|
<beans:value>D</beans:value>
|
||||||
</util:map>
|
<beans:value>E</beans:value>
|
||||||
</beans:property>
|
<beans:value>F</beans:value>
|
||||||
</beans:bean>
|
</util:list>
|
||||||
|
</beans:constructor-arg>
|
||||||
|
<beans:constructor-arg name="roles">
|
||||||
|
<util:list>
|
||||||
|
<beans:value>G</beans:value>
|
||||||
|
<beans:value>H</beans:value>
|
||||||
|
<beans:value>I</beans:value>
|
||||||
|
</util:list>
|
||||||
|
</beans:constructor-arg>
|
||||||
|
</beans:bean>
|
||||||
|
</util:list>
|
||||||
|
</beans:property>
|
||||||
|
</beans:bean>
|
||||||
|
|
||||||
<beans:bean id="x509AuthorizationProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
|
<beans:bean id="x509AuthorizationProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
|
||||||
<beans:property name="preAuthenticatedUserDetailsService" ref="timelyUserDetailsService" />
|
<beans:property name="preAuthenticatedUserDetailsService" ref="timelyUserDetailsService" />
|
||||||
|
|||||||
@@ -15,15 +15,27 @@
|
|||||||
|
|
||||||
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
|
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
|
||||||
<beans:property name="users">
|
<beans:property name="users">
|
||||||
<util:map key-type="java.lang.String">
|
<util:list>
|
||||||
<beans:entry key="CN=example.com">
|
<beans:bean class="timely.auth.TimelyUser">
|
||||||
<util:list>
|
<beans:constructor-arg name="subjectDn">
|
||||||
<beans:value>D</beans:value>
|
<beans:value>CN=example.com</beans:value>
|
||||||
<beans:value>E</beans:value>
|
</beans:constructor-arg>
|
||||||
<beans:value>F</beans:value>
|
<beans:constructor-arg name="auths">
|
||||||
</util:list>
|
<util:list>
|
||||||
</beans:entry>
|
<beans:value>D</beans:value>
|
||||||
</util:map>
|
<beans:value>E</beans:value>
|
||||||
|
<beans:value>F</beans:value>
|
||||||
|
</util:list>
|
||||||
|
</beans:constructor-arg>
|
||||||
|
<beans:constructor-arg name="roles">
|
||||||
|
<util:list>
|
||||||
|
<beans:value>G</beans:value>
|
||||||
|
<beans:value>H</beans:value>
|
||||||
|
<beans:value>I</beans:value>
|
||||||
|
</util:list>
|
||||||
|
</beans:constructor-arg>
|
||||||
|
</beans:bean>
|
||||||
|
</util:list>
|
||||||
</beans:property>
|
</beans:property>
|
||||||
</beans:bean>
|
</beans:bean>
|
||||||
|
|
||||||
@@ -46,4 +58,8 @@
|
|||||||
<authentication-provider ref="UserNamePasswordAuthenticationProvider" />
|
<authentication-provider ref="UserNamePasswordAuthenticationProvider" />
|
||||||
</authentication-manager>
|
</authentication-manager>
|
||||||
|
|
||||||
|
<beans:bean id="requiredRoles" class="Array">
|
||||||
|
|
||||||
|
</beans:bean>
|
||||||
|
|
||||||
</beans:beans>
|
</beans:beans>
|
||||||
@@ -6,6 +6,8 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ServiceConfigurationError;
|
import java.util.ServiceConfigurationError;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ import timely.api.request.AuthenticatedRequest;
|
|||||||
import timely.api.response.TimelyException;
|
import timely.api.response.TimelyException;
|
||||||
import timely.auth.util.DnUtils;
|
import timely.auth.util.DnUtils;
|
||||||
import timely.netty.http.auth.TimelyAuthenticationToken;
|
import timely.netty.http.auth.TimelyAuthenticationToken;
|
||||||
|
import timely.netty.http.auth.TimelyUserDetails;
|
||||||
|
|
||||||
public class AuthenticationService {
|
public class AuthenticationService {
|
||||||
|
|
||||||
@@ -35,12 +38,16 @@ public class AuthenticationService {
|
|||||||
private static SubjectDnX509PrincipalExtractor x509 = null;
|
private static SubjectDnX509PrincipalExtractor x509 = null;
|
||||||
private static ConcurrentHashMap<String, Object> fetchingEntity = new ConcurrentHashMap<>();
|
private static ConcurrentHashMap<String, Object> fetchingEntity = new ConcurrentHashMap<>();
|
||||||
private static ConcurrentHashMap<String, Object> fetchingPrincipal = new ConcurrentHashMap<>();
|
private static ConcurrentHashMap<String, Object> fetchingPrincipal = new ConcurrentHashMap<>();
|
||||||
|
private static Collection<String> requiredRoles;
|
||||||
|
private static Collection<String> requiredAuths;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
springContext = new ClassPathXmlApplicationContext("security.xml");
|
springContext = new ClassPathXmlApplicationContext("security.xml");
|
||||||
authManager = (AuthenticationManager) springContext.getBean("authenticationManager");
|
authManager = springContext.getBean("authenticationManager", AuthenticationManager.class);
|
||||||
x509 = (SubjectDnX509PrincipalExtractor) springContext.getBean("x509PrincipalExtractor");
|
x509 = springContext.getBean("x509PrincipalExtractor", SubjectDnX509PrincipalExtractor.class);
|
||||||
|
requiredRoles = springContext.getBean("requiredRoles", List.class);
|
||||||
|
requiredAuths = springContext.getBean("requiredAuths", List.class);
|
||||||
} catch (BeansException e) {
|
} catch (BeansException e) {
|
||||||
throw new ServiceConfigurationError("Error setting up Authentication objects: " + e.getMessage(),
|
throw new ServiceConfigurationError("Error setting up Authentication objects: " + e.getMessage(),
|
||||||
e.getRootCause());
|
e.getRootCause());
|
||||||
@@ -92,11 +99,13 @@ public class AuthenticationService {
|
|||||||
public static void enforceAccess(AuthenticatedRequest request) throws Exception {
|
public static void enforceAccess(AuthenticatedRequest request) throws Exception {
|
||||||
String sessionId = request.getSessionId();
|
String sessionId = request.getSessionId();
|
||||||
X509Certificate clientCert = request.getToken().getClientCert();
|
X509Certificate clientCert = request.getToken().getClientCert();
|
||||||
|
TimelyPrincipal verifiedPrincipal = null;
|
||||||
if (StringUtils.isBlank(sessionId) && clientCert == null) {
|
if (StringUtils.isBlank(sessionId) && clientCert == null) {
|
||||||
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "User must log in",
|
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "User must log in",
|
||||||
"Anonymous access is disabled. User must either send a client certificate or log in");
|
"Anonymous access is disabled. User must either send a client certificate or log in");
|
||||||
} else if (StringUtils.isNotBlank(sessionId)) {
|
} else if (StringUtils.isNotBlank(sessionId)) {
|
||||||
if (!AuthCache.containsKey(sessionId)) {
|
verifiedPrincipal = AuthCache.get(sessionId);
|
||||||
|
if (verifiedPrincipal == null) {
|
||||||
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "User must log in",
|
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "User must log in",
|
||||||
"Unknown session id was submitted, log in again");
|
"Unknown session id was submitted, log in again");
|
||||||
}
|
}
|
||||||
@@ -139,6 +148,27 @@ public class AuthenticationService {
|
|||||||
} else {
|
} else {
|
||||||
LOG.trace("Verified principal {} in AuthCache on 1st attempt", entity);
|
LOG.trace("Verified principal {} in AuthCache on 1st attempt", entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TimelyUser primaryUser = verifiedPrincipal.getPrimaryUser();
|
||||||
|
if (requiredRoles != null) {
|
||||||
|
if (!primaryUser.getRoles().containsAll(requiredRoles)) {
|
||||||
|
Set<String> missingRoles = new TreeSet<>();
|
||||||
|
missingRoles.addAll(requiredRoles);
|
||||||
|
missingRoles.removeAll(primaryUser.getRoles());
|
||||||
|
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "Access denied",
|
||||||
|
"User:" + primaryUser.getCommonName() + " is missing auth(s):" + missingRoles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requiredAuths != null) {
|
||||||
|
if (!primaryUser.getAuths().containsAll(requiredAuths)) {
|
||||||
|
Set<String> missingAuths = new TreeSet<>();
|
||||||
|
missingAuths.addAll(requiredRoles);
|
||||||
|
missingAuths.removeAll(primaryUser.getAuths());
|
||||||
|
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "Access denied",
|
||||||
|
"User:" + primaryUser.getCommonName() + " is missing role(s):" + missingAuths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error(e.getMessage(), e);
|
LOG.error(e.getMessage(), e);
|
||||||
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "Access denied", e.getMessage());
|
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "Access denied", e.getMessage());
|
||||||
@@ -193,7 +223,12 @@ public class AuthenticationService {
|
|||||||
: TimelyUser.UserType.USER;
|
: TimelyUser.UserType.USER;
|
||||||
Collection<String> auths = authentication.getAuthorities().stream().map(a -> a.getAuthority())
|
Collection<String> auths = authentication.getAuthorities().stream().map(a -> a.getAuthority())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
TimelyUser timelyUser = new TimelyUser(subjectIssuerDNPair, userType, auths, null, null,
|
Object o = authentication.getPrincipal();
|
||||||
|
Collection<String> roles = null;
|
||||||
|
if (o instanceof TimelyUserDetails) {
|
||||||
|
roles = ((TimelyUserDetails) o).getRoles();
|
||||||
|
}
|
||||||
|
TimelyUser timelyUser = new TimelyUser(subjectIssuerDNPair, userType, auths, roles, null,
|
||||||
System.currentTimeMillis());
|
System.currentTimeMillis());
|
||||||
return new TimelyPrincipal(Arrays.asList(timelyUser));
|
return new TimelyPrincipal(Arrays.asList(timelyUser));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,23 @@ public class TimelyUser implements Serializable {
|
|||||||
private final long creationTime;
|
private final long creationTime;
|
||||||
private final long expirationTime;
|
private final long expirationTime;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For Spring configuration
|
||||||
|
*/
|
||||||
|
public TimelyUser(String subjectDn, Collection<String> auths, Collection<String> roles) {
|
||||||
|
this.name = subjectDn;
|
||||||
|
this.commonName = DnUtils.getCommonName(subjectDn);
|
||||||
|
this.dn = SubjectIssuerDNPair.of(subjectDn);
|
||||||
|
this.userType = DnUtils.isServerDN(subjectDn) ? UserType.SERVER : UserType.USER;
|
||||||
|
this.auths = auths == null ? Collections.emptyList() : new LinkedHashSet<>(auths);
|
||||||
|
this.unmodifiableAuths = Collections.unmodifiableCollection(this.auths);
|
||||||
|
this.roles = roles == null ? Collections.emptyList() : new LinkedHashSet<>(roles);
|
||||||
|
this.unmodifiableRoles = Collections.unmodifiableCollection(this.roles);
|
||||||
|
this.roleToAuthMapping = null;
|
||||||
|
this.creationTime = System.currentTimeMillis();
|
||||||
|
this.expirationTime = -1L;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* For testing
|
* For testing
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package timely.auth;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -11,18 +10,20 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
||||||
|
import timely.netty.http.auth.TimelyUserDetails;
|
||||||
|
|
||||||
public class UserDetailsService implements
|
public class UserDetailsService implements
|
||||||
org.springframework.security.core.userdetails.AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
|
org.springframework.security.core.userdetails.AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
|
||||||
|
|
||||||
private HashMap<String, List<String>> users;
|
private HashMap<String, TimelyUser> users;
|
||||||
|
|
||||||
public Map<String, List<String>> getUsers() {
|
public Map<String, TimelyUser> getUsers() {
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUsers(LinkedHashMap<String, List<String>> users) {
|
public void setUsers(List<TimelyUser> users) {
|
||||||
this.users = new HashMap<>(users);
|
this.users = new HashMap<>();
|
||||||
|
users.forEach(u -> this.users.put(u.getName(), u));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -30,25 +31,32 @@ public class UserDetailsService implements
|
|||||||
throws UsernameNotFoundException {
|
throws UsernameNotFoundException {
|
||||||
// Determine if the user is allowed to access the system, if not throw
|
// Determine if the user is allowed to access the system, if not throw
|
||||||
// UsernameNotFoundException
|
// UsernameNotFoundException
|
||||||
String username = token.getName().toString();
|
String username = token.getName();
|
||||||
if (!users.containsKey(username)) {
|
if (!users.containsKey(username)) {
|
||||||
throw new UsernameNotFoundException(username + " not configured.");
|
throw new UsernameNotFoundException(username + " not configured.");
|
||||||
}
|
}
|
||||||
// If allowed, populate the user details object with the authorities for
|
// If allowed, populate the user details object with the authorities for
|
||||||
// the user.
|
// the user.
|
||||||
return new UserDetails() {
|
return new TimelyUserDetails() {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<SimpleGrantedAuthority> getAuthorities() {
|
public Collection<SimpleGrantedAuthority> getAuthorities() {
|
||||||
final Collection<SimpleGrantedAuthority> auths = new ArrayList<>();
|
final Collection<SimpleGrantedAuthority> auths = new ArrayList<>();
|
||||||
users.get(username).forEach(a -> {
|
users.get(username).getAuths().forEach(a -> {
|
||||||
auths.add(new SimpleGrantedAuthority(a));
|
auths.add(new SimpleGrantedAuthority(a));
|
||||||
});
|
});
|
||||||
return auths;
|
return auths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getRoles() {
|
||||||
|
final Collection<String> roles = new ArrayList<>();
|
||||||
|
roles.addAll(users.get(username).getRoles());
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package timely.netty.http.auth;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
public interface TimelyUserDetails extends UserDetails {
|
||||||
|
|
||||||
|
Collection<String> getRoles();
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package timely.auth;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.ServiceConfigurationError;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||||
|
|
||||||
|
public class UserDetailsServiceTest {
|
||||||
|
|
||||||
|
private static ApplicationContext springContext = null;
|
||||||
|
private static UserDetailsService userDetailsService = null;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setupClass() {
|
||||||
|
try {
|
||||||
|
springContext = new ClassPathXmlApplicationContext("security.xml");
|
||||||
|
userDetailsService = (UserDetailsService) springContext.getBean("timelyUserDetailsService");
|
||||||
|
} catch (BeansException e) {
|
||||||
|
throw new ServiceConfigurationError("Error initializing from spring: " + e.getMessage(), e.getRootCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuths() {
|
||||||
|
TimelyUser u = userDetailsService.getUsers().get("CN=example.com");
|
||||||
|
Assert.assertNotNull("TimelyUser not found", u);
|
||||||
|
Assert.assertEquals("3 auths expected", 3, u.getAuths().size());
|
||||||
|
Assert.assertTrue("Unexpected auths found " + u.getAuths(),
|
||||||
|
u.getAuths().containsAll(Arrays.asList("D", "E", "F")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRoles() {
|
||||||
|
TimelyUser u = userDetailsService.getUsers().get("CN=example.com");
|
||||||
|
Assert.assertNotNull("TimelyUser not found", u);
|
||||||
|
Assert.assertEquals("3 roles expected", 3, u.getRoles().size());
|
||||||
|
Assert.assertTrue("Unexpected roles found " + u.getRoles(),
|
||||||
|
u.getRoles().containsAll(Arrays.asList("G", "H", "I")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,19 +13,31 @@
|
|||||||
<beans:property name="subjectDnRegex" value="CN=(.*?)(?:,|$)"/>
|
<beans:property name="subjectDnRegex" value="CN=(.*?)(?:,|$)"/>
|
||||||
</beans:bean>
|
</beans:bean>
|
||||||
|
|
||||||
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
|
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
|
||||||
<beans:property name="users">
|
<beans:property name="users">
|
||||||
<util:map key-type="java.lang.String">
|
<util:list>
|
||||||
<beans:entry key="CN=example.com">
|
<beans:bean class="timely.auth.TimelyUser">
|
||||||
<util:list>
|
<beans:constructor-arg name="subjectDn">
|
||||||
<beans:value>D</beans:value>
|
<beans:value>CN=example.com</beans:value>
|
||||||
<beans:value>E</beans:value>
|
</beans:constructor-arg>
|
||||||
<beans:value>F</beans:value>
|
<beans:constructor-arg name="auths">
|
||||||
</util:list>
|
<util:list>
|
||||||
</beans:entry>
|
<beans:value>D</beans:value>
|
||||||
</util:map>
|
<beans:value>E</beans:value>
|
||||||
</beans:property>
|
<beans:value>F</beans:value>
|
||||||
</beans:bean>
|
</util:list>
|
||||||
|
</beans:constructor-arg>
|
||||||
|
<beans:constructor-arg name="roles">
|
||||||
|
<util:list>
|
||||||
|
<beans:value>G</beans:value>
|
||||||
|
<beans:value>H</beans:value>
|
||||||
|
<beans:value>I</beans:value>
|
||||||
|
</util:list>
|
||||||
|
</beans:constructor-arg>
|
||||||
|
</beans:bean>
|
||||||
|
</util:list>
|
||||||
|
</beans:property>
|
||||||
|
</beans:bean>
|
||||||
|
|
||||||
<beans:bean id="x509AuthorizationProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
|
<beans:bean id="x509AuthorizationProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
|
||||||
<beans:property name="preAuthenticatedUserDetailsService" ref="timelyUserDetailsService" />
|
<beans:property name="preAuthenticatedUserDetailsService" ref="timelyUserDetailsService" />
|
||||||
|
|||||||
Reference in new issue
Block a user