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