--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ and is available at http://www.eclipse.org/legal/epl-v10.html
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-parent</artifactId>
+ <version>0.20.4-SNAPSHOT</version>
+ <relativePath>../parent</relativePath>
+ </parent>
+
+ <artifactId>aaa-jetty-auth-log-filter</artifactId>
+ <name>ODL :: aaa :: ${project.artifactId}</name>
+ <packaging>bundle</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-security</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-filterchain</artifactId>
+ <!-- for a String constant only -->
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>repackaged-shiro</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.filter;
+
+import static org.apache.shiro.subject.support.DefaultSubjectContext.PRINCIPALS_SESSION_KEY;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Base64;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequestWrapper;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.eclipse.jetty.security.AbstractLoginService;
+import org.eclipse.jetty.security.DefaultUserIdentity;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Request;
+import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterConstants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This filter sets the Authentication field with the user from Basic authorization to ensure the correct name
+ * is printed in the Jetty NCSA logs.
+ *
+ * <p>To enable this filter is required to activate it in Karaf console:
+ * {@code scr:enable org.opendaylight.aaa.filter.JettyAuthenticationLogFilter}
+ *
+ * <p>Alternativate way to activate it activate it through {@code etc/org.opendaylight.aaa.filterchain.cfg} via setting
+ * {@code customFilterList=org.opendaylight.aaa.filter.JettyAuthenticationLogFilter}.
+ */
+@Component(enabled = false, property = CustomFilterAdapterConstants.FILTERCHAIN_FILTER + "=true")
+public final class JettyAuthenticationLogFilter implements Filter {
+ private static final Logger LOG = LoggerFactory.getLogger(JettyAuthenticationLogFilter.class);
+ private static final String BASIC = "Basic";
+ private static final String BASIC_SEP = BASIC + " ";
+
+ @Activate
+ public JettyAuthenticationLogFilter() {
+ LOG.info("Activation of JettyAuthenticationLogFilter");
+ }
+
+ @Override
+ public void init(final FilterConfig newFilterConfig) throws ServletException {
+ LOG.debug("Initializing JettyAuthenticationLogFilter");
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
+ throws IOException, ServletException {
+ chain.doFilter(request, response);
+ // Apply the filter only if processing a Jetty request.
+ if (request instanceof HttpServletRequestWrapper wrapper && wrapper.getRequest() instanceof Request unwrapped) {
+ doFilter(unwrapped);
+ }
+ }
+
+ /**
+ * Set Authentication data in the Jetty request to ensure accurate output in Jetty NCSA logs.
+ *
+ * @param request Jetty request
+ */
+ private void doFilter(final Request request) {
+ if (!requestIsNullOrUnauthenticated(request)) {
+ LOG.trace("Request {} is already authenticated", request);
+ return;
+ }
+
+ // Get the session from the request, or return null if no session was used.
+ final var requestSession = request.getSession(false);
+ if (requestSession != null) {
+ // Set Authentication from Principal provided in the Session.
+ final var attribute = requestSession.getAttribute(PRINCIPALS_SESSION_KEY);
+ if (attribute instanceof PrincipalCollection collection
+ && collection.getPrimaryPrincipal() instanceof Principal principal) {
+ final var defaultUserIdentity = new DefaultUserIdentity(null, principal, new String[0]);
+ final var userAuthentication = new UserAuthentication(BASIC, defaultUserIdentity);
+ request.setAuthentication(userAuthentication);
+ LOG.debug("Session user {} has been set in the request authentication", userAuthentication);
+ return;
+ }
+ }
+
+ // Get the user name from the request.
+ final var authorization = request.getHeader("Authorization");
+ if (authorization == null) {
+ LOG.trace("No Authorization header present in {}", request);
+ return;
+ }
+ if (!authorization.startsWith(BASIC_SEP)) {
+ LOG.trace("Request {} does not use basic authorization", request);
+ return;
+ }
+ final var userAndPassword = new String(
+ Base64.getDecoder().decode(authorization.substring(BASIC_SEP.length())), StandardCharsets.UTF_8).split(":");
+
+ // Create an Authentication class for the Jetty request based on Basic Authentication.
+ final var userPrincipal = new AbstractLoginService.UserPrincipal(userAndPassword[0], null);
+ final var defaultUserIdentity = new DefaultUserIdentity(null, userPrincipal, new String[0]);
+ final var userAuthentication = new UserAuthentication(BASIC, defaultUserIdentity);
+ request.setAuthentication(userAuthentication);
+ LOG.debug("Basic authentication user {} has been set in the request authentication", userAuthentication);
+ }
+
+ @Deactivate
+ @Override
+ public void destroy() {
+ LOG.debug("Destroying JettyAuthenticationLogFilter");
+ }
+
+ private static boolean requestIsNullOrUnauthenticated(final Request baseRequest) {
+ final var auth = baseRequest.getAuthentication();
+ return auth == null || auth == Authentication.UNAUTHENTICATED;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.filter;
+
+import static org.apache.shiro.subject.support.DefaultSubjectContext.PRINCIPALS_SESSION_KEY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import java.security.Principal;
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpSession;
+import org.apache.shiro.subject.SimplePrincipalCollection;
+import org.eclipse.jetty.security.UserAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class JettyAuthenticationLogFilterTest {
+ private static final String SESSION_USER = "sessionUser";
+ private static final String BASIC_USER = "admin";
+
+ @Mock
+ private HttpServletRequestWrapper mockServletRequest;
+ @Mock
+ private Response mockResponse;
+ @Mock
+ private FilterChain mockFilterChain;
+ @Mock
+ private HttpSession mockHttpSession;
+ @Mock
+ private Request mockRequest;
+ @Mock
+ private Principal mockPrincipal;
+ @Mock
+ private SimplePrincipalCollection mockSimplePrincipal;
+ @Captor
+ private ArgumentCaptor<UserAuthentication> userCaptor;
+
+ private final JettyAuthenticationLogFilter logFilter = new JettyAuthenticationLogFilter();
+
+ @BeforeEach
+ void beforeEach() {
+ // Prepare environment.
+ doReturn(mockRequest).when(mockServletRequest).getRequest();
+ }
+
+ @AfterEach
+ void teardown() {
+ logFilter.destroy();
+ }
+
+ @Test
+ void testFilterWithBasicAuth() throws Exception {
+ // Setup environment only for Basic Auth without session.
+ doReturn("Basic YWRtaW46YWRtaW4=").when(mockRequest).getHeader(eq("Authorization"));
+
+ // Execute filter.
+ logFilter.doFilter(mockServletRequest, mockResponse, mockFilterChain);
+
+ // Verify correct Authentication user.
+ verify(mockRequest).setAuthentication(userCaptor.capture());
+ final var noSessionAuthentication = userCaptor.getValue();
+ assertNotNull(noSessionAuthentication);
+ assertEquals(BASIC_USER, noSessionAuthentication.getUserIdentity().getUserPrincipal().getName());
+ }
+
+ @Test
+ void testFilterWithUnauthenticatedRequestWithBasicAuth() throws Exception {
+ // Setup environment only for Basic Auth without session and Request with Unauthenticated value.
+ doReturn("Basic YWRtaW46YWRtaW4=").when(mockRequest).getHeader(eq("Authorization"));
+ doReturn(Authentication.UNAUTHENTICATED).when(mockRequest).getAuthentication();
+
+ // Execute filter.
+ logFilter.doFilter(mockServletRequest, mockResponse, mockFilterChain);
+
+ // Verify correct Authentication user.
+ verify(mockRequest).setAuthentication(userCaptor.capture());
+ final var sessionAuthentication = userCaptor.getValue();
+ assertNotNull(sessionAuthentication);
+ assertEquals(BASIC_USER, sessionAuthentication.getUserIdentity().getUserPrincipal().getName());
+ }
+
+ @Test
+ void testFilterWithSession() throws Exception {
+ // Setup environment only for Session without Basic Authentication.
+ doReturn(mockHttpSession).when(mockRequest).getSession(eq(false));
+ doReturn(mockSimplePrincipal).when(mockHttpSession).getAttribute(eq(PRINCIPALS_SESSION_KEY));
+ doReturn(mockPrincipal).when(mockSimplePrincipal).getPrimaryPrincipal();
+ doReturn(SESSION_USER).when(mockPrincipal).getName();
+
+ // Execute filter.
+ logFilter.doFilter(mockServletRequest, mockResponse, mockFilterChain);
+
+ // Verify correct Authentication user.
+ verify(mockRequest).setAuthentication(userCaptor.capture());
+ final var sessionAuthentication = userCaptor.getValue();
+ assertNotNull(sessionAuthentication);
+ assertEquals(SESSION_USER, sessionAuthentication.getUserIdentity().getUserPrincipal().getName());
+ }
+}
*/
package org.opendaylight.aaa.shiro.principal;
+import com.google.common.base.MoreObjects;
import java.util.Set;
import org.opendaylight.aaa.api.Authentication;
import org.opendaylight.aaa.api.shiro.principal.ODLPrincipal;
* making the auth request.
*/
public final class ODLPrincipalImpl implements ODLPrincipal {
-
private final String username;
private final String domain;
private final String userId;
* @param auth Contains identifying information for the particular request.
* @return A Principal for the given session; essentially a DTO.
*/
- public static ODLPrincipal createODLPrincipal(Authentication auth) {
+ public static ODLPrincipal createODLPrincipal(final Authentication auth) {
return createODLPrincipal(auth.user(), auth.domain(), auth.userId(), auth.roles());
}
* @param roles The roles associated with <code>username</code>@<code>domain</code>
* @return A Principal for the given session; essentially a DTO.
*/
- public static ODLPrincipal createODLPrincipal(String username, String domain,
- String userId, Set<String> roles) {
+ public static ODLPrincipal createODLPrincipal(final String username, final String domain,
+ final String userId, final Set<String> roles) {
return new ODLPrincipalImpl(username, domain, userId, roles);
}
* @param userId The unique key for <code>username</code>
* @return A Principal for the given session; essentially a DTO.
*/
- public static ODLPrincipal createODLPrincipal(String username, String domain,
- String userId) {
+ public static ODLPrincipal createODLPrincipal(final String username, final String domain,
+ final String userId) {
return ODLPrincipalImpl.createODLPrincipal(username, domain, userId, null);
}
@Override
public String getUsername() {
- return this.username;
+ return username;
}
@Override
public String getDomain() {
- return this.domain;
+ return domain;
}
@Override
public String getUserId() {
- return this.userId;
+ return userId;
}
@Override
public Set<String> getRoles() {
- return this.roles;
+ return roles;
}
@Override
public String getName() {
return getUserId();
}
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).omitNullValues()
+ .add("userId", userId)
+ .add("username", username)
+ .add("domain", domain)
+ .add("roles", roles)
+ .toString();
+ }
}
<type>cfg</type>
<classifier>config</classifier>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-jetty-auth-log-filter</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<groupId>${project.groupId}</groupId>
<artifactId>aaa-idm-store-h2</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-jetty-auth-log-filter</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>aaa-password-service-api</artifactId>
<groupId>${project.groupId}</groupId>
<artifactId>aaa-filterchain</artifactId>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>aaa-jetty-auth-log-filter</artifactId>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<dependencies>
<dependency>
- <groupId>org.opendaylight.aaa</groupId>
- <artifactId>odl-aaa-password-service</artifactId>
- <classifier>features</classifier>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>odl-aaa-api</artifactId>
<type>xml</type>
+ <classifier>features</classifier>
</dependency>
-
<dependency>
- <groupId>org.opendaylight.controller</groupId>
- <artifactId>odl-jolokia</artifactId>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>odl-aaa-cert</artifactId>
+ <type>xml</type>
<classifier>features</classifier>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>odl-aaa-encryption-service</artifactId>
<type>xml</type>
+ <classifier>features</classifier>
</dependency>
-
- <!-- Existing AAA infrastructure -->
<dependency>
<groupId>org.opendaylight.aaa</groupId>
- <artifactId>odl-aaa-web</artifactId>
+ <artifactId>odl-aaa-password-service</artifactId>
<classifier>features</classifier>
<type>xml</type>
</dependency>
<dependency>
<groupId>org.opendaylight.aaa</groupId>
- <artifactId>aaa-shiro</artifactId>
+ <artifactId>odl-aaa-web</artifactId>
+ <classifier>features</classifier>
+ <type>xml</type>
</dependency>
<dependency>
- <groupId>org.opendaylight.aaa</groupId>
- <artifactId>aaa-shiro-api</artifactId>
+ <groupId>org.opendaylight.odlparent</groupId>
+ <artifactId>odl-karaf-feat-jetty</artifactId>
+ <type>xml</type>
+ <classifier>features</classifier>
</dependency>
+
<dependency>
- <groupId>org.opendaylight.aaa</groupId>
- <artifactId>odl-aaa-encryption-service</artifactId>
- <type>xml</type>
+ <groupId>org.opendaylight.controller</groupId>
+ <artifactId>odl-jolokia</artifactId>
<classifier>features</classifier>
+ <type>xml</type>
</dependency>
+
+ <!-- Existing AAA infrastructure -->
<dependency>
<groupId>org.opendaylight.aaa</groupId>
<artifactId>aaa-filterchain</artifactId>
</dependency>
<dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>odl-aaa-api</artifactId>
- <type>xml</type>
- <classifier>features</classifier>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-jetty-auth-log-filter</artifactId>
</dependency>
<dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>odl-aaa-cert</artifactId>
- <type>xml</type>
- <classifier>features</classifier>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-shiro</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-shiro-api</artifactId>
</dependency>
<dependency>
<groupId>org.opendaylight.aaa</groupId>
<type>xml</type>
<classifier>aaa-datastore-config</classifier>
</dependency>
- <dependency>
- <groupId>org.opendaylight.odlparent</groupId>
- <artifactId>odl-karaf-feat-jetty</artifactId>
- <type>xml</type>
- <classifier>features</classifier>
- </dependency>
<!--H2 Store -->
<dependency>
<module>aaa-cli-jar</module>
<module>aaa-filterchain</module>
<module>aaa-idm-store-h2</module>
+ <module>aaa-jetty-auth-log-filter</module>
<module>aaa-password-service</module>
<module>artifacts</module>
<module>features</module>