mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-16 12:32:10 +01:00
Authentication metrics (#1595)
Expose metrics about: - User login attempts - Failed user logins - User logouts - General successful accesses to SCM-Manager via any authentication realm - General failed accesses to SCM-Manager Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
2
gradle/changelog/authentication_metrics.yaml
Normal file
2
gradle/changelog/authentication_metrics.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Authentication and access metrics ([#1595](https://github.com/scm-manager/scm-manager/pull/1595))
|
||||
@@ -56,5 +56,4 @@ public final class Metrics {
|
||||
Collections.singleton(Tag.of("type", type))
|
||||
).bindTo(registry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,15 +21,12 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.legacy;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
@@ -37,17 +34,12 @@ import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
|
||||
import org.apache.shiro.crypto.hash.Sha1Hash;
|
||||
import org.apache.shiro.realm.AuthenticatingRealm;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
import sonia.scm.security.DAORealmHelper;
|
||||
import sonia.scm.security.DAORealmHelperFactory;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@@ -59,10 +51,8 @@ import javax.inject.Singleton;
|
||||
*/
|
||||
@Extension
|
||||
@Singleton
|
||||
public class LegacyRealm extends AuthenticatingRealm
|
||||
{
|
||||
public class LegacyRealm extends AuthenticatingRealm {
|
||||
|
||||
/** Field description */
|
||||
@VisibleForTesting
|
||||
static final String REALM = "LegacyRealm";
|
||||
|
||||
@@ -70,7 +60,7 @@ public class LegacyRealm extends AuthenticatingRealm
|
||||
.inRange('0', '9')
|
||||
.or(CharMatcher.inRange('a', 'f'))
|
||||
.or(CharMatcher.inRange('A', 'F')
|
||||
);
|
||||
);
|
||||
|
||||
/**
|
||||
* the logger for LegacyRealm
|
||||
@@ -78,9 +68,7 @@ public class LegacyRealm extends AuthenticatingRealm
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LegacyRealm.class);
|
||||
|
||||
private final DAORealmHelper helper;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
@@ -99,8 +87,6 @@ public class LegacyRealm extends AuthenticatingRealm
|
||||
setCredentialsMatcher(helper.wrapCredentialsMatcher(matcher));
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
|
||||
Preconditions.checkArgument(token instanceof UsernamePasswordToken, "unsupported token");
|
||||
|
||||
@@ -21,65 +21,60 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.legacy;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.crypto.hash.Sha1Hash;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.security.BearerToken;
|
||||
import sonia.scm.security.DAORealmHelperFactory;
|
||||
import sonia.scm.security.LoginAttemptHandler;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserDAO;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Before;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
import sonia.scm.security.DAORealmHelperFactory;
|
||||
import sonia.scm.security.LoginAttemptHandler;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LegacyRealmTest
|
||||
{
|
||||
public class LegacyRealmTest {
|
||||
|
||||
/** Field description */
|
||||
private static final String NEW_PASSWORD =
|
||||
"$shiro1$SHA-512$8192$$yrNahBVDa4Gz+y5gat4msdjyvjtHlVE+N5nTl4WIDhtBFwhSIib13mKJt1sWmVqgHDWi3VwX7fkdkJ2+WToTbw==";
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@Mock
|
||||
private LoginAttemptHandler loginAttemptHandler;
|
||||
|
||||
@Mock
|
||||
private UserDAO userDAO;
|
||||
|
||||
@Mock
|
||||
private GroupDAO groupDAO;
|
||||
|
||||
@InjectMocks
|
||||
private DAORealmHelperFactory helperFactory;
|
||||
|
||||
private LegacyRealm realm;
|
||||
|
||||
/**
|
||||
* Prepare object under test.
|
||||
*/
|
||||
@Before
|
||||
public void prepareObjectUnderTest() {
|
||||
this.realm = new LegacyRealm(helperFactory);
|
||||
realm = new LegacyRealm(helperFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void testDoGetAuthenticationInfo()
|
||||
{
|
||||
public void testDoGetAuthenticationInfo() {
|
||||
User user = UserTestData.createTrillian();
|
||||
|
||||
user.setPassword(new Sha1Hash("secret").toHex());
|
||||
@@ -92,31 +87,21 @@ public class LegacyRealmTest
|
||||
assertEquals("tricia", authInfo.getPrincipals().getPrimaryPrincipal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testDoGetAuthenticationInfoWithNewPasswords()
|
||||
{
|
||||
public void testDoGetAuthenticationInfoWithNewPasswords() {
|
||||
User user = UserTestData.createTrillian();
|
||||
|
||||
user.setPassword(NEW_PASSWORD);
|
||||
when(userDAO.get("tricia")).thenReturn(user);
|
||||
|
||||
AuthenticationToken token = new UsernamePasswordToken("tricia",
|
||||
NEW_PASSWORD);
|
||||
NEW_PASSWORD);
|
||||
|
||||
assertNull(realm.doGetAuthenticationInfo(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testDoGetAuthenticationInfoWithNullPassword()
|
||||
{
|
||||
public void testDoGetAuthenticationInfoWithNullPassword() {
|
||||
when(userDAO.get("tricia")).thenReturn(UserTestData.createTrillian());
|
||||
|
||||
AuthenticationToken token = new UsernamePasswordToken("tricia", "secret");
|
||||
@@ -124,30 +109,8 @@ public class LegacyRealmTest
|
||||
assertNull(realm.doGetAuthenticationInfo(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDoGetAuthenticationInfoWrongToken()
|
||||
{
|
||||
public void testDoGetAuthenticationInfoWrongToken() {
|
||||
realm.doGetAuthenticationInfo(BearerToken.valueOf("test"));
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@Mock
|
||||
private LoginAttemptHandler loginAttemptHandler;
|
||||
|
||||
@Mock
|
||||
private UserDAO userDAO;
|
||||
|
||||
@Mock
|
||||
private GroupDAO groupDAO;
|
||||
|
||||
@InjectMocks
|
||||
private DAORealmHelperFactory helperFactory;
|
||||
|
||||
private LegacyRealm realm;
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
@@ -41,6 +43,7 @@ import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.metrics.AuthenticationMetrics;
|
||||
import sonia.scm.security.AccessToken;
|
||||
import sonia.scm.security.AccessTokenBuilder;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
@@ -89,17 +92,24 @@ public class AuthenticationResource {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationResource.class);
|
||||
|
||||
static final String PATH = "v2/auth";
|
||||
private static final String AUTH_METRIC_TYPE = "UI/REST";
|
||||
|
||||
private final AccessTokenBuilderFactory tokenBuilderFactory;
|
||||
private final AccessTokenCookieIssuer cookieIssuer;
|
||||
private final Counter loginAttemptsCounter;
|
||||
private final Counter loginFailedCounter;
|
||||
private final Counter logoutCounter;
|
||||
|
||||
@Inject(optional = true)
|
||||
private LogoutRedirection logoutRedirection;
|
||||
|
||||
@Inject
|
||||
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer) {
|
||||
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer, MeterRegistry meterRegistry) {
|
||||
this.tokenBuilderFactory = tokenBuilderFactory;
|
||||
this.cookieIssuer = cookieIssuer;
|
||||
this.loginAttemptsCounter = AuthenticationMetrics.loginAttempts(meterRegistry, AUTH_METRIC_TYPE);
|
||||
this.loginFailedCounter = AuthenticationMetrics.loginFailed(meterRegistry, AUTH_METRIC_TYPE);
|
||||
this.logoutCounter = AuthenticationMetrics.logout(meterRegistry, AUTH_METRIC_TYPE);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -177,7 +187,10 @@ public class AuthenticationResource {
|
||||
HttpServletResponse response,
|
||||
AuthenticationRequestDto authentication
|
||||
) {
|
||||
loginAttemptsCounter.increment();
|
||||
|
||||
if (!authentication.isValid()) {
|
||||
loginFailedCounter.increment();
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
@@ -201,6 +214,7 @@ public class AuthenticationResource {
|
||||
res = Response.ok(token.compact()).build();
|
||||
}
|
||||
} catch (AuthenticationException ex) {
|
||||
loginFailedCounter.increment();
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("authentication failed for user ".concat(authentication.getUsername()), ex);
|
||||
} else {
|
||||
@@ -222,10 +236,9 @@ public class AuthenticationResource {
|
||||
@ApiResponse(responseCode = "204", description = "success")
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response) {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
subject.logout();
|
||||
logoutCounter.increment();
|
||||
|
||||
SecurityUtils.getSubject().logout();
|
||||
// remove authentication cookie
|
||||
cookieIssuer.invalidate(request, response);
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
|
||||
public class AuthenticationMetrics {
|
||||
|
||||
private AuthenticationMetrics() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates counter to track amount of login attempts to SCM-Manager.
|
||||
*
|
||||
* @param registry meter registry
|
||||
* @param type type of login e.g.: api_key, bearer_token, etc.
|
||||
* @return new {@link Counter}
|
||||
*/
|
||||
public static Counter loginAttempts(MeterRegistry registry, String type) {
|
||||
return Counter
|
||||
.builder("scm.auth.login.attempts")
|
||||
.description("The amount of login attempts to SCM-Manager")
|
||||
.tags("type", type)
|
||||
.register(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates counter to track amount of failed logins to SCM-Manager.
|
||||
*
|
||||
* @param registry meter registry
|
||||
* @param type type of failed login, e.g.: UI/REST
|
||||
* @return new {@link Counter}
|
||||
*/
|
||||
public static Counter loginFailed(MeterRegistry registry, String type) {
|
||||
return Counter
|
||||
.builder("scm.auth.login.failed")
|
||||
.tags("type", type)
|
||||
.description("The amount of failed logins to SCM-Manager")
|
||||
.register(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates counter to track amount of logouts to SCM-Manager.
|
||||
*
|
||||
* @param registry meter registry
|
||||
* @param type type of logout, e.g.: UI/REST
|
||||
* @return new {@link Counter}
|
||||
*/
|
||||
public static Counter logout(MeterRegistry registry, String type) {
|
||||
return Counter
|
||||
.builder("scm.auth.logout")
|
||||
.description("The amount of logouts from SCM-Manager")
|
||||
.tags("type", type)
|
||||
.register(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates counter to track amount of token refreshes by SCM-Manager.
|
||||
*
|
||||
* @param registry meter registry
|
||||
* @param type type of refreshed token, e.g.: JWT
|
||||
* @return new {@link Counter}
|
||||
*/
|
||||
public static Counter tokenRefresh(MeterRegistry registry, String type) {
|
||||
return Counter
|
||||
.builder("scm.auth.token.refresh")
|
||||
.description("The amount of authentication token refreshes")
|
||||
.tags("type", type)
|
||||
.register(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates counter to track amount of successful accesses to SCM-Manager realms with token.
|
||||
*
|
||||
* @param registry meter registry
|
||||
* @param realm type of realm e.g.: {@link sonia.scm.security.BearerRealm}
|
||||
* @param token type of token e.g.: {@link org.apache.shiro.authc.UsernamePasswordToken},
|
||||
* @return new {@link Counter}
|
||||
*/
|
||||
public static Counter accessRealmSuccessful(MeterRegistry registry, String realm, String token) {
|
||||
return Counter
|
||||
.builder("scm.auth.realm.successful")
|
||||
.description("The amount of successful login to the realm")
|
||||
.tags("realm", realm, "token", token)
|
||||
.register(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates counter to track amount of successful accesses to SCM-Manager.
|
||||
*
|
||||
* @param registry meter registry
|
||||
* @return new {@link Counter}
|
||||
*/
|
||||
public static Counter accessSuccessful(MeterRegistry registry, String tokenType) {
|
||||
return Counter
|
||||
.builder("scm.auth.access.successful")
|
||||
.description("The amount of successful accesses to SCM-Manager")
|
||||
.tags("token", tokenType)
|
||||
.register(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates counter to track amount of failed accesses to SCM-Manager.
|
||||
*
|
||||
* @param registry meter registry
|
||||
* @return new {@link Counter}
|
||||
*/
|
||||
public static Counter accessFailed(MeterRegistry registry, String tokenType) {
|
||||
return Counter
|
||||
.builder("scm.auth.access.failed")
|
||||
.description("The amount of failed accesses to SCM-Manager")
|
||||
.tags("token", tokenType)
|
||||
.register(registry);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,15 +43,9 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
@Extension
|
||||
public class AnonymousRealm extends AuthenticatingRealm {
|
||||
|
||||
/**
|
||||
* realm name
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final String REALM = "AnonymousRealm";
|
||||
|
||||
/**
|
||||
* dao realm helper
|
||||
*/
|
||||
private final DAORealmHelper helper;
|
||||
private final UserDAO userDAO;
|
||||
|
||||
|
||||
@@ -49,19 +49,14 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
*/
|
||||
@Singleton
|
||||
@Extension
|
||||
public class BearerRealm extends AuthenticatingRealm
|
||||
{
|
||||
public class BearerRealm extends AuthenticatingRealm {
|
||||
|
||||
/** realm name */
|
||||
@VisibleForTesting
|
||||
static final String REALM = "BearerRealm";
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BearerRealm.class);
|
||||
|
||||
/** dao realm helper */
|
||||
private final DAORealmHelper helper;
|
||||
|
||||
/** access token resolver **/
|
||||
private final AccessTokenResolver tokenResolver;
|
||||
|
||||
/**
|
||||
@@ -95,9 +90,7 @@ public class BearerRealm extends AuthenticatingRealm
|
||||
* Validates the given bearer token and retrieves authentication data from
|
||||
* {@link UserDAO} and {@link GroupDAO}.
|
||||
*
|
||||
*
|
||||
* @param token bearer token
|
||||
*
|
||||
* @return authentication data from user and group dao
|
||||
*/
|
||||
@Override
|
||||
@@ -114,5 +107,4 @@ public class BearerRealm extends AuthenticatingRealm
|
||||
.withSessionId(bt.getPrincipal())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,8 +45,6 @@ import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Set;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Default authorizing realm.
|
||||
*
|
||||
@@ -55,34 +53,21 @@ import java.util.Set;
|
||||
*/
|
||||
@Extension
|
||||
@Singleton
|
||||
public class DefaultRealm extends AuthorizingRealm
|
||||
{
|
||||
public class DefaultRealm extends AuthorizingRealm {
|
||||
|
||||
private static final String SEPARATOR = System.getProperty("line.separator", "\n");
|
||||
|
||||
/**
|
||||
* the logger for DefaultRealm
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultRealm.class);
|
||||
|
||||
/** Field description */
|
||||
@VisibleForTesting
|
||||
static final String REALM = "DefaultRealm";
|
||||
private final ScmPermissionResolver permissionResolver;
|
||||
private final Set<AuthorizationCollector> authorizationCollectors;
|
||||
private final DAORealmHelper helper;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param service
|
||||
* @param authorizationCollectors
|
||||
* @param helperFactory
|
||||
*/
|
||||
@Inject
|
||||
public DefaultRealm(PasswordService service, Set<AuthorizationCollector> authorizationCollectors, DAORealmHelperFactory helperFactory)
|
||||
{
|
||||
public DefaultRealm(PasswordService service,
|
||||
Set<AuthorizationCollector> authorizationCollectors,
|
||||
DAORealmHelperFactory helperFactory) {
|
||||
this.authorizationCollectors = authorizationCollectors;
|
||||
this.helper = helperFactory.create(REALM);
|
||||
|
||||
@@ -103,40 +88,16 @@ public class DefaultRealm extends AuthorizingRealm
|
||||
return permissionResolver;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param token
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(
|
||||
AuthenticationToken token)
|
||||
throws AuthenticationException
|
||||
{
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
|
||||
return helper.getAuthenticationInfo(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param principals
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
|
||||
{
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
||||
AuthorizationInfo info = collectors(principals);
|
||||
Scope scope = principals.oneByType(Scope.class);
|
||||
if (scope != null && ! scope.isEmpty()) {
|
||||
if (scope != null && !scope.isEmpty()) {
|
||||
LOG.trace("filter permissions by scope {}", scope);
|
||||
AuthorizationInfo filtered = Scopes.filter(getPermissionResolver(), info, scope);
|
||||
if (LOG.isTraceEnabled()) {
|
||||
@@ -174,7 +135,7 @@ public class DefaultRealm extends AuthorizingRealm
|
||||
}
|
||||
}
|
||||
|
||||
private void log( PrincipalCollection collection, AuthorizationInfo original, AuthorizationInfo filtered ) {
|
||||
private void log(PrincipalCollection collection, AuthorizationInfo original, AuthorizationInfo filtered) {
|
||||
StringBuilder buffer = new StringBuilder("authorization summary: ");
|
||||
|
||||
buffer.append(SEPARATOR).append("username : ").append(collection.getPrimaryPrincipal());
|
||||
@@ -183,7 +144,7 @@ public class DefaultRealm extends AuthorizingRealm
|
||||
buffer.append(SEPARATOR).append("scope : ");
|
||||
append(buffer, collection.oneByType(Scope.class));
|
||||
|
||||
if ( filtered != null ) {
|
||||
if (filtered != null) {
|
||||
buffer.append(SEPARATOR).append("permissions (filtered by scope): ");
|
||||
append(buffer, filtered);
|
||||
buffer.append(SEPARATOR).append("permissions (unfiltered): ");
|
||||
@@ -200,20 +161,11 @@ public class DefaultRealm extends AuthorizingRealm
|
||||
append(buffer, authz.getObjectPermissions());
|
||||
}
|
||||
|
||||
private void append(StringBuilder buffer, Iterable<?> iterable){
|
||||
if (iterable != null){
|
||||
for ( Object item : iterable )
|
||||
{
|
||||
private void append(StringBuilder buffer, Iterable<?> iterable) {
|
||||
if (iterable != null) {
|
||||
for (Object item : iterable) {
|
||||
buffer.append(SEPARATOR).append(" - ").append(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** set of authorization collector */
|
||||
private final Set<AuthorizationCollector> authorizationCollectors;
|
||||
|
||||
/** realm helper */
|
||||
private final DAORealmHelper helper;
|
||||
}
|
||||
|
||||
@@ -24,13 +24,16 @@
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.pam.AbstractAuthenticationStrategy;
|
||||
import org.apache.shiro.realm.Realm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import sonia.scm.metrics.AuthenticationMetrics;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -40,6 +43,13 @@ public class ScmAtLeastOneSuccessfulStrategy extends AbstractAuthenticationStrat
|
||||
|
||||
final ThreadLocal<List<Throwable>> threadLocal = new ThreadLocal<>();
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
@Inject
|
||||
public ScmAtLeastOneSuccessfulStrategy(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
|
||||
this.threadLocal.set(new ArrayList<>());
|
||||
@@ -51,6 +61,11 @@ public class ScmAtLeastOneSuccessfulStrategy extends AbstractAuthenticationStrat
|
||||
if (t != null) {
|
||||
this.threadLocal.get().add(t);
|
||||
}
|
||||
|
||||
if (isAuthenticationSuccessful(singleRealmInfo)) {
|
||||
AuthenticationMetrics.accessRealmSuccessful(meterRegistry, realm.getClass().getName(), token.getClass().getName()).increment();
|
||||
}
|
||||
|
||||
return super.afterAttempt(realm, token, singleRealmInfo, aggregateInfo, t);
|
||||
}
|
||||
|
||||
@@ -58,9 +73,15 @@ public class ScmAtLeastOneSuccessfulStrategy extends AbstractAuthenticationStrat
|
||||
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) {
|
||||
final List<Throwable> throwables = threadLocal.get();
|
||||
threadLocal.remove();
|
||||
|
||||
String tokenType = token.getClass().getName();
|
||||
|
||||
if (isAuthenticationSuccessful(aggregate)) {
|
||||
AuthenticationMetrics.accessSuccessful(meterRegistry, tokenType).increment();
|
||||
return aggregate;
|
||||
}
|
||||
AuthenticationMetrics.accessFailed(meterRegistry, tokenType).increment();
|
||||
|
||||
Optional<? extends AuthenticationException> specializedException = findSpecializedException(throwables);
|
||||
|
||||
if (specializedException.isPresent()) {
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
package sonia.scm.web.security;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.slf4j.Logger;
|
||||
@@ -31,6 +33,7 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.filter.Filters;
|
||||
import sonia.scm.filter.WebElement;
|
||||
import sonia.scm.metrics.AuthenticationMetrics;
|
||||
import sonia.scm.security.AccessToken;
|
||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||
import sonia.scm.security.AccessTokenResolver;
|
||||
@@ -54,7 +57,7 @@ import static java.util.Optional.of;
|
||||
|
||||
@Priority(Filters.PRIORITY_POST_AUTHENTICATION)
|
||||
@WebElement(value = Filters.PATTERN_RESTAPI,
|
||||
morePatterns = { Filters.PATTERN_DEBUG })
|
||||
morePatterns = {Filters.PATTERN_DEBUG})
|
||||
public class TokenRefreshFilter extends HttpFilter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TokenRefreshFilter.class);
|
||||
@@ -63,13 +66,15 @@ public class TokenRefreshFilter extends HttpFilter {
|
||||
private final JwtAccessTokenRefresher refresher;
|
||||
private final AccessTokenResolver resolver;
|
||||
private final AccessTokenCookieIssuer issuer;
|
||||
private final Counter tokenRefreshCounter;
|
||||
|
||||
@Inject
|
||||
public TokenRefreshFilter(Set<WebTokenGenerator> tokenGenerators, JwtAccessTokenRefresher refresher, AccessTokenResolver resolver, AccessTokenCookieIssuer issuer) {
|
||||
public TokenRefreshFilter(Set<WebTokenGenerator> tokenGenerators, JwtAccessTokenRefresher refresher, AccessTokenResolver resolver, AccessTokenCookieIssuer issuer, MeterRegistry meterRegistry) {
|
||||
this.tokenGenerators = tokenGenerators;
|
||||
this.refresher = refresher;
|
||||
this.resolver = resolver;
|
||||
this.issuer = issuer;
|
||||
this.tokenRefreshCounter = AuthenticationMetrics.tokenRefresh(meterRegistry, "JWT");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,12 +107,13 @@ public class TokenRefreshFilter extends HttpFilter {
|
||||
}
|
||||
if (accessToken instanceof JwtAccessToken) {
|
||||
refresher.refresh((JwtAccessToken) accessToken)
|
||||
.ifPresent(jwtAccessToken -> refreshToken(request, response, jwtAccessToken));
|
||||
.ifPresent(jwtAccessToken -> refreshJwtToken(request, response, jwtAccessToken));
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshToken(HttpServletRequest request, HttpServletResponse response, JwtAccessToken jwtAccessToken) {
|
||||
LOG.debug("refreshing authentication token");
|
||||
private void refreshJwtToken(HttpServletRequest request, HttpServletResponse response, JwtAccessToken jwtAccessToken) {
|
||||
tokenRefreshCounter.increment();
|
||||
LOG.debug("refreshing JWT authentication token");
|
||||
issuer.authenticate(request, response, jwtAccessToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -47,14 +50,15 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.net.URI.create;
|
||||
import static java.util.Optional.of;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -75,6 +79,8 @@ public class AuthenticationResourceTest {
|
||||
@Mock
|
||||
private AccessTokenBuilder accessTokenBuilder;
|
||||
|
||||
private MeterRegistry meterRegistry;
|
||||
|
||||
private final AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
|
||||
|
||||
private final MockHttpResponse response = new MockHttpResponse();
|
||||
@@ -135,7 +141,8 @@ public class AuthenticationResourceTest {
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer);
|
||||
meterRegistry = new SimpleMeterRegistry();
|
||||
authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer, meterRegistry);
|
||||
dispatcher.addSingletonResource(authenticationResource);
|
||||
|
||||
AccessToken accessToken = mock(AccessToken.class);
|
||||
@@ -157,6 +164,11 @@ public class AuthenticationResourceTest {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||
List<Meter> meters = meterRegistry.getMeters();
|
||||
assertThat(meters).hasSize(3);
|
||||
Optional<Meter> loginAttemptMeter = meters.stream().filter(m -> m.getId().getName().equals("scm.auth.login.attempts")).findFirst();
|
||||
assertThat(loginAttemptMeter).isPresent();
|
||||
assertThat(loginAttemptMeter.get().measure().iterator().next().getValue()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -167,6 +179,12 @@ public class AuthenticationResourceTest {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||
|
||||
List<Meter> meters = meterRegistry.getMeters();
|
||||
assertThat(meters).hasSize(3);
|
||||
Optional<Meter> loginAttemptMeter = meters.stream().filter(m -> m.getId().getName().equals("scm.auth.login.attempts")).findFirst();
|
||||
assertThat(loginAttemptMeter).isPresent();
|
||||
assertThat(loginAttemptMeter.get().measure().iterator().next().getValue()).isEqualTo(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -178,6 +196,10 @@ public class AuthenticationResourceTest {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus());
|
||||
|
||||
List<Meter> meters = meterRegistry.getMeters();
|
||||
assertThat(meters).hasSize(3);
|
||||
assertThat(meters.stream().map(m -> m.getId().getName())).contains("scm.auth.login.failed", "scm.auth.login.attempts", "scm.auth.logout");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -187,6 +209,10 @@ public class AuthenticationResourceTest {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus());
|
||||
|
||||
List<Meter> meters = meterRegistry.getMeters();
|
||||
assertThat(meters).hasSize(3);
|
||||
assertThat(meters.stream().map(m -> m.getId().getName())).contains("scm.auth.login.failed", "scm.auth.login.attempts", "scm.auth.logout");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -216,6 +242,12 @@ public class AuthenticationResourceTest {
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||
|
||||
List<Meter> meters = meterRegistry.getMeters();
|
||||
assertThat( meters).hasSize(3);
|
||||
Optional<Meter> logoutMeter = meters.stream().filter(m -> m.getId().getName().equals("scm.auth.logout")).findFirst();
|
||||
assertThat(logoutMeter).isPresent();
|
||||
assertThat(logoutMeter.get().measure().iterator().next().getValue()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -227,7 +259,7 @@ public class AuthenticationResourceTest {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
||||
assertThat(response.getContentAsString(), containsString("http://example.com/cas/logout"));
|
||||
assertThat(response.getContentAsString()).contains("http://example.com/cas/logout");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContext;
|
||||
@@ -56,7 +55,6 @@ class AnonymousRealmTest {
|
||||
@Mock
|
||||
private UserDAO userDAO;
|
||||
|
||||
@InjectMocks
|
||||
private AnonymousRealm realm;
|
||||
|
||||
@Mock
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@@ -62,7 +61,6 @@ class BearerRealmTest {
|
||||
@Mock
|
||||
private AccessTokenResolver accessTokenResolver;
|
||||
|
||||
@InjectMocks
|
||||
private BearerRealm realm;
|
||||
|
||||
@Mock
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
@@ -41,7 +39,6 @@ import org.apache.shiro.authz.permission.WildcardPermissionResolver;
|
||||
import org.apache.shiro.crypto.hash.DefaultHashService;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -56,38 +53,38 @@ import sonia.scm.user.UserTestData;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DefaultRealmTest
|
||||
{
|
||||
public class DefaultRealmTest {
|
||||
|
||||
@Mock
|
||||
private DefaultAuthorizationCollector collector;
|
||||
|
||||
private Set<AuthorizationCollector> authorizationCollectors;
|
||||
|
||||
@Mock
|
||||
private LoginAttemptHandler loginAttemptHandler;
|
||||
|
||||
@Mock
|
||||
private GroupDAO groupDAO;
|
||||
|
||||
@Mock
|
||||
private UserDAO userDAO;
|
||||
|
||||
@InjectMocks
|
||||
private DAORealmHelperFactory helperFactory;
|
||||
|
||||
private DefaultRealm realm;
|
||||
|
||||
private DefaultPasswordService service;
|
||||
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test(expected = DisabledAccountException.class)
|
||||
public void testDisabledAccount()
|
||||
{
|
||||
public void testDisabledAccount() {
|
||||
User user = UserTestData.createMarvin();
|
||||
|
||||
user.setActive(false);
|
||||
@@ -97,37 +94,29 @@ public class DefaultRealmTest
|
||||
realm.getAuthenticationInfo(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testGetAuthorizationInfo()
|
||||
{
|
||||
public void testGetAuthorizationInfo() {
|
||||
SimplePrincipalCollection col = new SimplePrincipalCollection();
|
||||
|
||||
realm.doGetAuthorizationInfo(col);
|
||||
verify(collector, times(1)).collect(col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DefaultRealm#doGetAuthorizationInfo(PrincipalCollection)} without scope.
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void testGetAuthorizationInfoWithoutScope(){
|
||||
public void testGetAuthorizationInfoWithoutScope() {
|
||||
SimplePrincipalCollection col = new SimplePrincipalCollection();
|
||||
|
||||
|
||||
SimpleAuthorizationInfo collectorsAuthz = new SimpleAuthorizationInfo();
|
||||
collectorsAuthz.addStringPermission("repository:*");
|
||||
when(collector.collect(col)).thenReturn(collectorsAuthz);
|
||||
|
||||
|
||||
AuthorizationInfo realmsAutz = realm.doGetAuthorizationInfo(col);
|
||||
assertThat(realmsAutz.getObjectPermissions(), is(nullValue()));
|
||||
assertThat(realmsAutz.getStringPermissions(), Matchers.contains("repository:*"));
|
||||
assertThat(realmsAutz.getObjectPermissions()).isNull();
|
||||
assertThat(realmsAutz.getStringPermissions()).contains("repository:*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAuthorizationInfoWithMultipleAuthorizationCollectors(){
|
||||
public void testGetAuthorizationInfoWithMultipleAuthorizationCollectors() {
|
||||
SimplePrincipalCollection col = new SimplePrincipalCollection();
|
||||
col.add(Scope.empty(), DefaultRealm.REALM);
|
||||
|
||||
@@ -151,124 +140,81 @@ public class DefaultRealmTest
|
||||
authorizationCollectors.add(thirdCollector);
|
||||
|
||||
AuthorizationInfo realmsAuthz = realm.doGetAuthorizationInfo(col);
|
||||
assertThat(realmsAuthz.getObjectPermissions(), contains(permission));
|
||||
assertThat(realmsAuthz.getStringPermissions(), containsInAnyOrder("repository:*", "user:*"));
|
||||
assertThat(realmsAuthz.getRoles(), Matchers.contains("awesome"));
|
||||
assertThat(realmsAuthz.getObjectPermissions()).contains(permission);
|
||||
assertThat(realmsAuthz.getStringPermissions()).containsExactlyInAnyOrder("repository:*", "user:*");
|
||||
assertThat(realmsAuthz.getRoles()).contains("awesome");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DefaultRealm#doGetAuthorizationInfo(PrincipalCollection)} with empty scope.
|
||||
*/
|
||||
@Test
|
||||
public void testGetAuthorizationInfoWithEmptyScope(){
|
||||
public void testGetAuthorizationInfoWithEmptyScope() {
|
||||
SimplePrincipalCollection col = new SimplePrincipalCollection();
|
||||
col.add(Scope.empty(), DefaultRealm.REALM);
|
||||
|
||||
|
||||
SimpleAuthorizationInfo collectorsAuthz = new SimpleAuthorizationInfo();
|
||||
collectorsAuthz.addStringPermission("repository:*");
|
||||
when(collector.collect(col)).thenReturn(collectorsAuthz);
|
||||
|
||||
|
||||
AuthorizationInfo realmsAutz = realm.doGetAuthorizationInfo(col);
|
||||
assertThat(realmsAutz.getObjectPermissions(), is(nullValue()));
|
||||
assertThat(realmsAutz.getStringPermissions(), Matchers.contains("repository:*"));
|
||||
assertThat(realmsAutz.getObjectPermissions()).isNull();
|
||||
;
|
||||
assertThat(realmsAutz.getStringPermissions()).contains("repository:*");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DefaultRealm#doGetAuthorizationInfo(PrincipalCollection)} with scope.
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void testGetAuthorizationInfoWithScope(){
|
||||
public void testGetAuthorizationInfoWithScope() {
|
||||
SimplePrincipalCollection col = new SimplePrincipalCollection();
|
||||
col.add(Scope.valueOf("user:*:me"), DefaultRealm.REALM);
|
||||
|
||||
|
||||
SimpleAuthorizationInfo collectorsAuthz = new SimpleAuthorizationInfo();
|
||||
collectorsAuthz.addStringPermission("repository:*");
|
||||
collectorsAuthz.addStringPermission("user:*:me");
|
||||
when(collector.collect(col)).thenReturn(collectorsAuthz);
|
||||
|
||||
|
||||
AuthorizationInfo realmsAutz = realm.doGetAuthorizationInfo(col);
|
||||
assertThat(
|
||||
Collections2.transform(realmsAutz.getObjectPermissions(), Permission::toString),
|
||||
allOf(
|
||||
Matchers.contains("user:*:me"),
|
||||
not(Matchers.contains("repository:*"))
|
||||
)
|
||||
);
|
||||
assertThat(Collections2.transform(realmsAutz.getObjectPermissions(), Permission::toString)).contains("user:*:me").doesNotContain("repository:*");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testSimpleAuthentication()
|
||||
{
|
||||
public void testSimpleAuthentication() {
|
||||
User user = UserTestData.createTrillian();
|
||||
UsernamePasswordToken token = daoUser(user, "secret");
|
||||
AuthenticationInfo info = realm.getAuthenticationInfo(token);
|
||||
|
||||
assertNotNull(info);
|
||||
assertThat(info).isNotNull();
|
||||
|
||||
PrincipalCollection collection = info.getPrincipals();
|
||||
|
||||
assertEquals(token.getUsername(), collection.getPrimaryPrincipal());
|
||||
assertThat(collection.getRealmNames(), hasSize(1));
|
||||
assertThat(collection.getRealmNames(), hasItem(DefaultRealm.REALM));
|
||||
assertEquals(user, collection.oneByType(User.class));
|
||||
assertThat(token.getUsername()).isEqualTo(collection.getPrimaryPrincipal());
|
||||
assertThat(collection.getRealmNames()).hasSize(1);
|
||||
assertThat(collection.getRealmNames()).contains(DefaultRealm.REALM);
|
||||
assertThat(user).isEqualTo(collection.oneByType(User.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test(expected = UnknownAccountException.class)
|
||||
public void testUnknownAccount()
|
||||
{
|
||||
public void testUnknownAccount() {
|
||||
realm.getAuthenticationInfo(new UsernamePasswordToken("tricia", "secret"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testWithoutUsername()
|
||||
{
|
||||
public void testWithoutUsername() {
|
||||
realm.getAuthenticationInfo(new UsernamePasswordToken(null, "secret"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test(expected = IncorrectCredentialsException.class)
|
||||
public void testWrongCredentials()
|
||||
{
|
||||
public void testWrongCredentials() {
|
||||
UsernamePasswordToken token = daoUser(UserTestData.createDent(), "secret");
|
||||
|
||||
token.setPassword("secret123".toCharArray());
|
||||
realm.getAuthenticationInfo(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testWrongToken()
|
||||
{
|
||||
public void testWrongToken() {
|
||||
realm.getAuthenticationInfo(new OtherAuthenticationToken());
|
||||
}
|
||||
|
||||
//~--- set methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
public void setUp() {
|
||||
service = new DefaultPasswordService();
|
||||
|
||||
DefaultHashService hashService = new DefaultHashService();
|
||||
@@ -281,96 +227,30 @@ public class DefaultRealmTest
|
||||
authorizationCollectors.add(collector);
|
||||
|
||||
realm = new DefaultRealm(service, authorizationCollectors, helperFactory);
|
||||
|
||||
|
||||
// set permission resolver
|
||||
realm.setPermissionResolver(new WildcardPermissionResolver());
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
* @param password
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private UsernamePasswordToken daoUser(User user, String password)
|
||||
{
|
||||
private UsernamePasswordToken daoUser(User user, String password) {
|
||||
user.setPassword(service.encryptPassword(password));
|
||||
when(userDAO.get(user.getName())).thenReturn(user);
|
||||
|
||||
return new UsernamePasswordToken(user.getName(), password);
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
private static class OtherAuthenticationToken implements AuthenticationToken {
|
||||
|
||||
/**
|
||||
* Class description
|
||||
*
|
||||
*
|
||||
* @version Enter version here..., 14/12/13
|
||||
* @author Enter your name here...
|
||||
*/
|
||||
private static class OtherAuthenticationToken implements AuthenticationToken
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
private static final long serialVersionUID = 8891352342377018022L;
|
||||
|
||||
//~--- get methods --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Object getCredentials()
|
||||
{
|
||||
public Object getCredentials() {
|
||||
throw new UnsupportedOperationException("Not supported yet."); // To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Object getPrincipal()
|
||||
{
|
||||
public Object getPrincipal() {
|
||||
throw new UnsupportedOperationException("Not supported yet."); // To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
@Mock
|
||||
private DefaultAuthorizationCollector collector;
|
||||
|
||||
private Set<AuthorizationCollector> authorizationCollectors;
|
||||
|
||||
@Mock
|
||||
private LoginAttemptHandler loginAttemptHandler;
|
||||
|
||||
@Mock
|
||||
private GroupDAO groupDAO;
|
||||
|
||||
@Mock
|
||||
private UserDAO userDAO;
|
||||
|
||||
@InjectMocks
|
||||
private DAORealmHelperFactory helperFactory;
|
||||
|
||||
/** Field description */
|
||||
private DefaultRealm realm;
|
||||
|
||||
/** Field description */
|
||||
private DefaultPasswordService service;
|
||||
}
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.MergableAuthenticationInfo;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.realm.Realm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.junit.Test;
|
||||
@@ -37,9 +41,11 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
@@ -49,7 +55,7 @@ public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
private Realm realm;
|
||||
|
||||
@Mock
|
||||
private AuthenticationToken token;
|
||||
private UsernamePasswordToken token;
|
||||
|
||||
@Mock
|
||||
MergableAuthenticationInfo singleRealmInfo;
|
||||
@@ -71,7 +77,7 @@ public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
|
||||
@Test
|
||||
public void shouldAddNonNullThrowableToList() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(new SimpleMeterRegistry());
|
||||
strategy.threadLocal.set(new ArrayList<>());
|
||||
|
||||
strategy.afterAttempt(realm, token, singleRealmInfo, aggregateInfo, tokenExpiredException);
|
||||
@@ -82,7 +88,7 @@ public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
|
||||
@Test(expected = TokenExpiredException.class)
|
||||
public void shouldRethrowTokenExpiredException() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(new SimpleMeterRegistry());
|
||||
strategy.threadLocal.set(singletonList(tokenExpiredException));
|
||||
|
||||
strategy.afterAllAttempts(token, aggregateInfo);
|
||||
@@ -90,7 +96,7 @@ public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
|
||||
@Test(expected = TokenValidationFailedException.class)
|
||||
public void shouldRethrowTokenValidationFailedException() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(new SimpleMeterRegistry());
|
||||
strategy.threadLocal.set(singletonList(tokenValidationFailedException));
|
||||
|
||||
strategy.afterAllAttempts(token, aggregateInfo);
|
||||
@@ -98,7 +104,7 @@ public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
|
||||
@Test(expected = TokenExpiredException.class)
|
||||
public void shouldPrioritizeRethrowingTokenExpiredExceptionOverTokenValidationFailedException() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(new SimpleMeterRegistry());
|
||||
strategy.threadLocal.set(Arrays.asList(tokenValidationFailedException, tokenExpiredException));
|
||||
|
||||
strategy.afterAllAttempts(token, aggregateInfo);
|
||||
@@ -106,7 +112,7 @@ public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
|
||||
@Test(expected = AuthenticationException.class)
|
||||
public void shouldThrowGenericErrorIfNonTokenExpiredExceptionWasCaught() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(new SimpleMeterRegistry());
|
||||
strategy.threadLocal.set(singletonList(authenticationException));
|
||||
|
||||
strategy.afterAllAttempts(token, aggregateInfo);
|
||||
@@ -114,7 +120,7 @@ public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
|
||||
@Test()
|
||||
public void shouldNotRethrowExceptionIfAuthenticationSuccessful() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(new SimpleMeterRegistry());
|
||||
strategy.threadLocal.set(singletonList(tokenExpiredException));
|
||||
when(aggregateInfo.getPrincipals()).thenReturn(principalCollection);
|
||||
when(principalCollection.isEmpty()).thenReturn(false);
|
||||
@@ -124,4 +130,50 @@ public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
assertThat(authenticationInfo).isNotNull();
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void shouldTrackSuccessfulRealmAuthenticationMetrics() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(meterRegistry);
|
||||
strategy.threadLocal.set(singletonList(tokenExpiredException));
|
||||
when(aggregateInfo.getPrincipals()).thenReturn(principalCollection);
|
||||
when(principalCollection.isEmpty()).thenReturn(false);
|
||||
|
||||
DefaultRealm realm = mock(DefaultRealm.class);
|
||||
|
||||
strategy.afterAttempt(realm, token, aggregateInfo, null, null);
|
||||
|
||||
assertThat(meterRegistry.getMeters()).hasSize(1);
|
||||
Optional<Meter> realmAccessMeter = meterRegistry.getMeters()
|
||||
.stream()
|
||||
.filter(m -> m.getId().getName().equals("scm.auth.realm.successful"))
|
||||
.findFirst();
|
||||
assertThat(realmAccessMeter).isPresent();
|
||||
assertThat(realmAccessMeter.get().measure().iterator().next().getValue()).isEqualTo(1);
|
||||
assertThat(realmAccessMeter.get().getId().getTags()).contains(
|
||||
Tag.of("realm", "sonia.scm.security.DefaultRealm"),
|
||||
Tag.of("token", "org.apache.shiro.authc.UsernamePasswordToken")
|
||||
);
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void shouldTrackGeneralSuccessfulAuthenticationMetrics() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy(meterRegistry);
|
||||
strategy.threadLocal.set(singletonList(tokenExpiredException));
|
||||
when(aggregateInfo.getPrincipals()).thenReturn(principalCollection);
|
||||
when(principalCollection.isEmpty()).thenReturn(false);
|
||||
|
||||
strategy.afterAllAttempts(token, aggregateInfo);
|
||||
|
||||
assertThat(meterRegistry.getMeters()).hasSize(1);
|
||||
Optional<Meter> accessMeter = meterRegistry.getMeters()
|
||||
.stream()
|
||||
.filter(m -> m.getId().getName().equals("scm.auth.access.successful"))
|
||||
.findFirst();
|
||||
assertThat(accessMeter).isPresent();
|
||||
assertThat(accessMeter.get().measure().iterator().next().getValue()).isEqualTo(1);
|
||||
assertThat(accessMeter.get().getId().getTags()).contains(
|
||||
Tag.of("token", "org.apache.shiro.authc.UsernamePasswordToken")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@
|
||||
|
||||
package sonia.scm.web.security;
|
||||
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||
@@ -47,6 +49,7 @@ import java.util.Set;
|
||||
|
||||
import static java.util.Collections.singleton;
|
||||
import static java.util.Optional.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -68,7 +71,6 @@ class TokenRefreshFilterTest {
|
||||
@Mock
|
||||
private AccessTokenCookieIssuer issuer;
|
||||
|
||||
@InjectMocks
|
||||
private TokenRefreshFilter filter;
|
||||
|
||||
@Mock
|
||||
@@ -78,9 +80,13 @@ class TokenRefreshFilterTest {
|
||||
@Mock
|
||||
private FilterChain filterChain;
|
||||
|
||||
private MeterRegistry meterRegistry;
|
||||
|
||||
@BeforeEach
|
||||
void initGenerators() {
|
||||
void init() {
|
||||
when(tokenGenerators.iterator()).thenReturn(singleton(tokenGenerator).iterator());
|
||||
meterRegistry = new SimpleMeterRegistry();
|
||||
filter = new TokenRefreshFilter(tokenGenerators, refresher, resolver, issuer, meterRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -130,6 +136,23 @@ class TokenRefreshFilterTest {
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTrackMetricIfTokenWasRefreshed() throws IOException, ServletException {
|
||||
BearerToken token = createValidToken();
|
||||
JwtAccessToken jwtToken = mock(JwtAccessToken.class);
|
||||
JwtAccessToken newJwtToken = mock(JwtAccessToken.class);
|
||||
when(tokenGenerator.createToken(request)).thenReturn(token);
|
||||
when(resolver.resolve(token)).thenReturn(jwtToken);
|
||||
when(refresher.refresh(jwtToken)).thenReturn(of(newJwtToken));
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertThat(meterRegistry.getMeters()).hasSize(1);
|
||||
Meter.Id meterId = meterRegistry.getMeters().get(0).getId();
|
||||
assertThat(meterId.getName()).isEqualTo("scm.auth.token.refresh");
|
||||
assertThat(meterId.getTag("type")).isEqualTo("JWT");
|
||||
}
|
||||
|
||||
BearerToken createValidToken() {
|
||||
return valueOf("some.jwt.token");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user