You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by en...@apache.org on 2022/09/24 23:59:08 UTC
[sling-org-apache-sling-auth-core] branch master updated: SLING-11583 Allow custom decoration of the login event properties (#12)
This is an automated email from the ASF dual-hosted git repository.
enorman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-auth-core.git
The following commit(s) were added to refs/heads/master by this push:
new 3c5c7b7 SLING-11583 Allow custom decoration of the login event properties (#12)
3c5c7b7 is described below
commit 3c5c7b728657d510284ad54986eded587fa54e9b
Author: Eric Norman <en...@apache.org>
AuthorDate: Sat Sep 24 16:59:03 2022 -0700
SLING-11583 Allow custom decoration of the login event properties (#12)
---
pom.xml | 6 +
.../sling/auth/core/LoginEventDecorator.java | 58 +++++++
.../sling/auth/core/impl/SlingAuthenticator.java | 35 +++-
.../org/apache/sling/auth/core/package-info.java | 4 +-
.../auth/core/impl/SlingAuthenticatorOsgiTest.java | 184 +++++++++++++++++++--
5 files changed, 263 insertions(+), 24 deletions(-)
diff --git a/pom.xml b/pom.xml
index ad03490..7284c34 100644
--- a/pom.xml
+++ b/pom.xml
@@ -174,5 +174,11 @@
<version>3.1.0</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <version>4.2.0</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/src/main/java/org/apache/sling/auth/core/LoginEventDecorator.java b/src/main/java/org/apache/sling/auth/core/LoginEventDecorator.java
new file mode 100644
index 0000000..6e0c8e2
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/core/LoginEventDecorator.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.auth.core;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.auth.core.spi.AuthenticationInfo;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Components should implement this interface to customize properties
+ * in the Login and/or LoginFailed event
+ */
+public interface LoginEventDecorator {
+
+ /**
+ * Called to allow the component to modify the login event properties
+ *
+ * @param request the current request
+ * @param authInfo the current authInfo
+ * @param eventProperties the event properties to decorate
+ */
+ @NotNull default void decorateLoginEvent(final @NotNull HttpServletRequest request,
+ final @NotNull AuthenticationInfo authInfo,
+ final @NotNull Map<String, Object> eventProperties) {
+ //no-op
+ }
+
+ /**
+ * Called to allow the component to modify the login failed event properties
+ *
+ * @param request the current request
+ * @param authInfo the current authInfo
+ * @param eventProperties the event properties to decorate
+ */
+ @NotNull default void decorateLoginFailedEvent(final @NotNull HttpServletRequest request,
+ final @NotNull AuthenticationInfo authInfo,
+ final @NotNull Map<String, Object> eventProperties) {
+ //no-op
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java b/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
index 4a6ed94..8cdadf0 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
@@ -22,6 +22,7 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -44,6 +45,7 @@ import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.auth.core.AuthConstants;
import org.apache.sling.auth.core.AuthUtil;
import org.apache.sling.auth.core.AuthenticationSupport;
+import org.apache.sling.auth.core.LoginEventDecorator;
import org.apache.sling.auth.core.spi.AuthenticationFeedbackHandler;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationHandler.FAILURE_REASON_CODES;
@@ -309,6 +311,12 @@ public class SlingAuthenticator implements Authenticator,
private final ResourceResolverFactory resourceResolverFactory;
+ /**
+ * LoginEventDecorator services
+ */
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, service = LoginEventDecorator.class, fieldOption = FieldOption.REPLACE)
+ private volatile List<LoginEventDecorator> loginEventDecorators = new ArrayList<>(); // NOSONAR
+
// ---------- SCR integration
@Activate
@@ -448,7 +456,7 @@ public class SlingAuthenticator implements Authenticator,
try {
postProcess(authInfo, request, response);
} catch (LoginException e) {
- postLoginFailedEvent(authInfo, e);
+ postLoginFailedEvent(request, authInfo, e);
handleLoginFailure(request, response, authInfo, e);
return false;
@@ -723,7 +731,7 @@ public class SlingAuthenticator implements Authenticator,
final boolean impersChanged = setSudoCookie(request, response, authInfo);
if (sendLoginEvent != null) {
- postLoginEvent(authInfo);
+ postLoginEvent(request, authInfo);
}
// provide the resource resolver to the feedback handler
@@ -756,7 +764,7 @@ public class SlingAuthenticator implements Authenticator,
return processRequest;
} catch (LoginException re) {
- postLoginFailedEvent(authInfo, re);
+ postLoginFailedEvent(request, authInfo, re);
// handle failure feedback before proceeding to handling the
// failed login internally
@@ -1387,11 +1395,17 @@ public class SlingAuthenticator implements Authenticator,
}
}
- private void postLoginEvent(final AuthenticationInfo authInfo) {
+ private void postLoginEvent(final HttpServletRequest request, final AuthenticationInfo authInfo) {
final Map<String, Object> properties = new HashMap<>();
properties.put(SlingConstants.PROPERTY_USERID, authInfo.getUser());
properties.put(AuthenticationInfo.AUTH_TYPE, authInfo.getAuthType());
+ // allow extensions to supply additional properties
+ final List<LoginEventDecorator> localList = this.loginEventDecorators;
+ for (final LoginEventDecorator decorator : localList) {
+ decorator.decorateLoginEvent(request, authInfo, properties);
+ }
+
EventAdmin localEA = this.eventAdmin;
if (localEA != null) {
localEA.postEvent(new Event(AuthConstants.TOPIC_LOGIN, properties));
@@ -1402,19 +1416,26 @@ public class SlingAuthenticator implements Authenticator,
* Post an event to let subscribers know that a login failure has occurred. For examples, subscribers
* to the {@link AuthConstants#TOPIC_LOGIN_FAILED} event topic may be used to implement a failed login throttling solution.
*/
- private void postLoginFailedEvent(final AuthenticationInfo authInfo, Exception reason) {
+ private void postLoginFailedEvent(final HttpServletRequest request,
+ final AuthenticationInfo authInfo, Exception reason) {
// The reason for the failure may be useful to downstream subscribers.
FAILURE_REASON_CODES reasonCode = FailureCodesMapper.getFailureReason(authInfo, reason);
//if reason code is unknowm, it is problem some non-login related failure, so don't send the event
if (reasonCode != FAILURE_REASON_CODES.UNKNOWN) {
- final Map<String, Object> properties = new HashMap<>();
+ final Map<String, Object> properties = new HashMap<>();
if (authInfo.getUser() != null) {
properties.put(SlingConstants.PROPERTY_USERID, authInfo.getUser());
}
if (authInfo.getAuthType() != null) {
properties.put(AuthenticationInfo.AUTH_TYPE, authInfo.getAuthType());
}
- properties.put("reason_code", reasonCode.name());
+ properties.put("reason_code", reasonCode.name());
+
+ // allow extensions to supply additional properties
+ final List<LoginEventDecorator> localList = this.loginEventDecorators;
+ for (final LoginEventDecorator decorator : localList) {
+ decorator.decorateLoginFailedEvent(request, authInfo, properties);
+ }
EventAdmin localEA = this.eventAdmin;
if (localEA != null) {
diff --git a/src/main/java/org/apache/sling/auth/core/package-info.java b/src/main/java/org/apache/sling/auth/core/package-info.java
index 66dee4f..f8d608e 100755
--- a/src/main/java/org/apache/sling/auth/core/package-info.java
+++ b/src/main/java/org/apache/sling/auth/core/package-info.java
@@ -22,9 +22,9 @@
* of utility functions in the {@link org.apache.sling.auth.core.AuthUtil}
* class.
*
- * @version 1.4.0
+ * @version 1.5.0
*/
-@org.osgi.annotation.versioning.Version("1.4.0")
+@org.osgi.annotation.versioning.Version("1.5.0")
package org.apache.sling.auth.core;
diff --git a/src/test/java/org/apache/sling/auth/core/impl/SlingAuthenticatorOsgiTest.java b/src/test/java/org/apache/sling/auth/core/impl/SlingAuthenticatorOsgiTest.java
index 5d9bf6e..d798d25 100644
--- a/src/test/java/org/apache/sling/auth/core/impl/SlingAuthenticatorOsgiTest.java
+++ b/src/test/java/org/apache/sling/auth/core/impl/SlingAuthenticatorOsgiTest.java
@@ -16,29 +16,49 @@
*/
package org.apache.sling.auth.core.impl;
+import static org.apache.sling.auth.core.impl.SlingAuthenticationMetrics.AUTHENTICATE_FAILED_METER_NAME;
+import static org.apache.sling.auth.core.impl.SlingAuthenticationMetrics.AUTHENTICATE_SUCCESS_METER_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.auth.core.AuthConstants;
+import org.apache.sling.auth.core.LoginEventDecorator;
+import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.commons.metrics.Meter;
import org.apache.sling.commons.metrics.MetricsService;
import org.apache.sling.commons.metrics.Timer;
import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.awaitility.Awaitility;
+import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import static org.apache.sling.auth.core.impl.SlingAuthenticationMetrics.AUTHENTICATE_FAILED_METER_NAME;
-import static org.apache.sling.auth.core.impl.SlingAuthenticationMetrics.AUTHENTICATE_SUCCESS_METER_NAME;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
public class SlingAuthenticatorOsgiTest {
@@ -50,13 +70,15 @@ public class SlingAuthenticatorOsgiTest {
private Timer.Context ctx = mock(Timer.Context.class);
private Timer timer = mock(Timer.class);
private final MetricsService metricsService = mock(MetricsService.class);
+ private final AuthenticationHandler testAuthHandler = mock(AuthenticationHandler.class);
+ private final TestEventHandler testEventHandler = new TestEventHandler();
+ private final ResourceResolverFactory resourceResolverFactory = mock(ResourceResolverFactory.class);
private SlingAuthenticator authenticator;
@Before
public void before() throws Exception {
ResourceResolver rr = mock(ResourceResolver.class);
- ResourceResolverFactory resourceResolverFactory = mock(ResourceResolverFactory.class);
when(resourceResolverFactory.getResourceResolver(any(AuthenticationInfo.class))).thenReturn(rr);
@@ -68,8 +90,13 @@ public class SlingAuthenticatorOsgiTest {
context.registerService(ResourceResolverFactory.class, resourceResolverFactory);
context.registerService(MetricsService.class, metricsService);
context.registerInjectActivateService(AuthenticationRequirementsManager.class);
+
+ //register a test auth handler
+ context.registerService(AuthenticationHandler.class, testAuthHandler, Collections.singletonMap(AuthenticationHandler.PATH_PROPERTY, new String[] {"/"}));
+ context.registerService(EventHandler.class, testEventHandler);
+ context.registerService(LoginEventDecorator.class, new TestLoginEventDecorator());
+
context.registerInjectActivateService(AuthenticationHandlersManager.class);
-
authenticator = context.registerInjectActivateService(SlingAuthenticator.class);
}
@@ -92,4 +119,131 @@ public class SlingAuthenticatorOsgiTest {
verifyNoInteractions((failedMeter));
}
+ /**
+ * Verify decoration of a login event
+ */
+ @Test
+ public void testLoginEventDecoration() {
+ assertLoginEvent(
+ (req, resp) -> {
+ // provide test authInfo
+ AuthenticationInfo authInfo = new AuthenticationInfo("testing", "admin", "admin".toCharArray());
+ authInfo.put(AuthConstants.AUTH_INFO_LOGIN, Boolean.TRUE);
+ when(testAuthHandler.extractCredentials(req, resp)).thenReturn(authInfo);
+ },
+ () -> testEventHandler.collectedEvents(AuthConstants.TOPIC_LOGIN),
+ event -> assertEquals("test1Value", event.getProperty("test1"))
+ );
+ }
+
+ /**
+ * Verify decoration of a login failed event
+ */
+ @Test
+ public void testLoginFailedEventDecoration() {
+ assertLoginEvent(
+ (req, resp) -> {
+ // provide invalid test authInfo
+ AuthenticationInfo authInfo = new AuthenticationInfo("testing", "invalid", "invalid".toCharArray());
+ when(testAuthHandler.extractCredentials(req, resp)).thenReturn(authInfo);
+ // throw exception to trigger FailedLogin event
+ try {
+ when(resourceResolverFactory.getResourceResolver(authInfo)).thenThrow(new LoginException("Test LoginFailed"));
+ } catch (LoginException e) {
+ // should never get here as the LoginException should be caught by the SlingAuthenticator
+ fail("Unexpected exception caught: " + e.getMessage());
+ }
+ },
+ () -> testEventHandler.collectedEvents(AuthConstants.TOPIC_LOGIN_FAILED),
+ event -> assertEquals("test2Value", event.getProperty("test2"))
+ );
+ }
+
+ /**
+ * The common parts for verifying the LoginEvent properties to avoid
+ * code duplication in the similar tests
+ *
+ * @param prepareAuthInfo to do the work of mocking the authInfo
+ * @param collectEvents to do the work of collecting the delivered events
+ * @param verifyEvent to do the work to assert that the event has the expected state
+ */
+ protected void assertLoginEvent(BiConsumer<HttpServletRequest, HttpServletResponse> prepareAuthInfo,
+ Supplier<List<Event>> collectEvents,
+ Consumer<Event> verifyEvent) {
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ when(req.getServletPath()).thenReturn("/");
+ when(req.getServerName()).thenReturn("localhost");
+ when(req.getServerPort()).thenReturn(80);
+ when(req.getScheme()).thenReturn("http");
+ when(req.getRequestURI()).thenReturn("http://localhost:80/");
+
+ HttpServletResponse resp = mock(HttpServletResponse.class);
+
+ // prepare the auth mocks
+ prepareAuthInfo.accept(req, resp);
+
+ testEventHandler.clear();
+ authenticator.handleSecurity(req, resp);
+
+ // wait for the login event to arrive
+ Awaitility.await("eventDelivery")
+ .atMost(Duration.ofSeconds(5))
+ .pollInterval(Duration.ofMillis(100))
+ .until(() -> {
+ List<Event> events = collectEvents.get();
+ return !events.isEmpty();
+ });
+ List<Event> events = collectEvents.get();
+ assertEquals(1, events.size());
+ // make sure the event has the state that we expect
+ verifyEvent.accept(events.get(0));
+ }
+
+ /**
+ * EventHandler that collects the events that were delivered for inspection
+ */
+ static class TestEventHandler implements EventHandler {
+ private Map<String, List<Event>> collectedEvents = new HashMap<>();
+
+ public void clear() {
+ collectedEvents.clear();
+ }
+
+ public @NotNull List<Event> collectedEvents(String topic) {
+ List<Event> list = collectedEvents.get(topic);
+ return list == null ? Collections.emptyList() : new ArrayList<>(list);
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ String topic = event.getTopic();
+ // collect the event if it is one of the topics we are interested in
+ if (AuthConstants.TOPIC_LOGIN_FAILED.equals(topic) ||
+ AuthConstants.TOPIC_LOGIN.equals(topic)) {
+ List<Event> list = collectedEvents.computeIfAbsent(topic, t -> new ArrayList<>());
+ list.add(event);
+ }
+ }
+
+ }
+
+ /**
+ * Test login event decorator that adds a test value to the event properties
+ */
+ static class TestLoginEventDecorator implements LoginEventDecorator {
+
+ @Override
+ public @NotNull void decorateLoginEvent(@NotNull HttpServletRequest request,
+ @NotNull AuthenticationInfo authInfo, @NotNull Map<String, Object> eventProperties) {
+ eventProperties.put("test1", "test1Value");
+ }
+
+ @Override
+ public @NotNull void decorateLoginFailedEvent(@NotNull HttpServletRequest request,
+ @NotNull AuthenticationInfo authInfo, @NotNull Map<String, Object> eventProperties) {
+ eventProperties.put("test2", "test2Value");
+ }
+
+ }
+
}
\ No newline at end of file