From 9388aa7b1bb517654224f616a21ced783db69daa Mon Sep 17 00:00:00 2001 From: "chaitali.borole" Date: Mon, 1 Jun 2026 14:12:56 +0530 Subject: [PATCH] ATLAS-5284: Support JWT authentication & Header based Authentication for Atlas --- authn/pom.xml | 60 ++++ .../apache/atlas/authn/handler/AtlasAuth.java | 65 ++++ .../atlas/authn/handler/AtlasAuthHandler.java | 29 ++ .../jwt/AtlasDefaultJwtAuthHandler.java | 87 +++++ .../handler/jwt/AtlasJwtAuthHandler.java | 339 ++++++++++++++++++ client/common/pom.xml | 7 + .../org/apache/atlas/AtlasBaseClient.java | 42 ++- .../retriever/JwTokenRetrieverDefault.java | 152 ++++++++ .../atlas/token/retriever/TokenRetriever.java | 24 ++ distro/src/conf/atlas-application.properties | 8 +- pom.xml | 1 + webapp/pom.xml | 6 + .../filters/AtlasAuthenticationFilter.java | 2 +- .../web/filters/AtlasAuthenticationToken.java | 55 +++ .../web/filters/AtlasHeaderPreAuthFilter.java | 169 +++++++++ .../atlas/web/filters/AtlasJwtAuthFilter.java | 119 ++++++ .../web/filters/AtlasJwtAuthWrapper.java | 111 ++++++ .../AtlasKnoxSSOAuthenticationFilter.java | 1 + .../apache/atlas/web/filters/AuditFilter.java | 15 +- .../web/security/AtlasSecurityConfig.java | 26 +- webapp/src/main/resources/spring-security.xml | 4 + .../filters/AtlasHeaderPreAuthFilterTest.java | 146 ++++++++ .../web/security/AtlasSecurityConfigTest.java | 32 +- 23 files changed, 1481 insertions(+), 19 deletions(-) create mode 100644 authn/pom.xml create mode 100644 authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuth.java create mode 100644 authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuthHandler.java create mode 100644 authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasDefaultJwtAuthHandler.java create mode 100644 authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasJwtAuthHandler.java create mode 100644 client/common/src/main/java/org/apache/atlas/token/retriever/JwTokenRetrieverDefault.java create mode 100644 client/common/src/main/java/org/apache/atlas/token/retriever/TokenRetriever.java create mode 100644 webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationToken.java create mode 100644 webapp/src/main/java/org/apache/atlas/web/filters/AtlasHeaderPreAuthFilter.java create mode 100644 webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthFilter.java create mode 100644 webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapper.java create mode 100644 webapp/src/test/java/org/apache/atlas/web/filters/AtlasHeaderPreAuthFilterTest.java diff --git a/authn/pom.xml b/authn/pom.xml new file mode 100644 index 00000000000..548900a6423 --- /dev/null +++ b/authn/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + org.apache.atlas + apache-atlas + 3.0.0-SNAPSHOT + + + atlas-authn + + + + org.apache.commons + commons-lang3 + + + + com.nimbusds + nimbus-jose-jwt + 9.37.3 + + + + javax.servlet + javax.servlet-api + + + + org.apache.commons + commons-configuration2 + ${commons-conf2.version} + + + + org.slf4j + slf4j-api + + + + diff --git a/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuth.java b/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuth.java new file mode 100644 index 00000000000..035905e0cee --- /dev/null +++ b/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuth.java @@ -0,0 +1,65 @@ +/* + * 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.atlas.authn.handler; + +public class AtlasAuth { + public static enum AUTH_TYPE { + JWT_JWKS("JWT-JWKS"); + + private final String authType; + + private AUTH_TYPE(String authType) { + this.authType = authType; + } + } + + private String userName; + private AUTH_TYPE type; + private boolean isAuthenticated; + + public AtlasAuth(final String username, AUTH_TYPE type) { + this.userName = username; + this.isAuthenticated = true; + this.type = type; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public AUTH_TYPE getType() { + return type; + } + + public void setType(AUTH_TYPE type) { + this.type = type; + } + + public boolean isAuthenticated() { + return isAuthenticated; + } + + public void setAuthenticated(boolean authenticated) { + isAuthenticated = authenticated; + } +} diff --git a/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuthHandler.java b/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuthHandler.java new file mode 100644 index 00000000000..35fea06de47 --- /dev/null +++ b/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuthHandler.java @@ -0,0 +1,29 @@ +/* + * 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.atlas.authn.handler; + +import org.apache.commons.configuration2.Configuration; + +import javax.servlet.http.HttpServletRequest; + +public interface AtlasAuthHandler { + void initialize(Configuration config) throws Exception; + + AtlasAuth authenticate(HttpServletRequest request); +} diff --git a/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasDefaultJwtAuthHandler.java b/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasDefaultJwtAuthHandler.java new file mode 100644 index 00000000000..4558563e55e --- /dev/null +++ b/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasDefaultJwtAuthHandler.java @@ -0,0 +1,87 @@ +/* + * 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.atlas.authn.handler.jwt; + +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.JWTClaimsSetVerifier; +import org.apache.atlas.authn.handler.AtlasAuth; +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.ServletRequest; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +public class AtlasDefaultJwtAuthHandler extends AtlasJwtAuthHandler { + protected static final String AUTHORIZATION_HEADER = "Authorization"; + + @Override + public ConfigurableJWTProcessor getJwtProcessor(JWSKeySelector keySelector) { + ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + JWTClaimsSetVerifier claimsVerifier = new DefaultJWTClaimsVerifier<>(); + + jwtProcessor.setJWSKeySelector(keySelector); + jwtProcessor.setJWTClaimsSetVerifier(claimsVerifier); + + return jwtProcessor; + } + + @Override + public AtlasAuth authenticate(HttpServletRequest request) { + AtlasAuth atlasAuth = null; + String jwtAuthHeaderStr = getJwtAuthHeader(request); + String jwtCookieStr = StringUtils.isBlank(jwtAuthHeaderStr) ? getJwtCookie(request) : null; + + String username = authenticate(jwtAuthHeaderStr, jwtCookieStr); + if (username != null) { + atlasAuth = new AtlasAuth(username, AtlasAuth.AUTH_TYPE.JWT_JWKS); + } + return atlasAuth; + } + + public static boolean canAuthenticateRequest(final ServletRequest request) { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + String jwtAuthHeaderStr = getJwtAuthHeader(httpServletRequest); + String jwtCookieStr = StringUtils.isBlank(jwtAuthHeaderStr) ? getJwtCookie(httpServletRequest) : null; + boolean proceed = shouldProceedAuth(jwtAuthHeaderStr, jwtCookieStr); + return proceed; + } + + public static String getJwtAuthHeader(final HttpServletRequest httpServletRequest) { + return httpServletRequest.getHeader(AUTHORIZATION_HEADER); + } + + public static String getJwtCookie(final HttpServletRequest httpServletRequest) { + String jwtCookieStr = null; + Cookie[] cookies = httpServletRequest.getCookies(); + + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookieName.equals(cookie.getName())) { + jwtCookieStr = cookie.getName() + "=" + cookie.getValue(); + break; + } + } + } + return jwtCookieStr; + } +} diff --git a/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasJwtAuthHandler.java b/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasJwtAuthHandler.java new file mode 100644 index 00000000000..fefad4893bf --- /dev/null +++ b/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasJwtAuthHandler.java @@ -0,0 +1,339 @@ +/* + * 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.atlas.authn.handler.jwt; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import org.apache.atlas.authn.handler.AtlasAuthHandler; +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Date; +import java.util.List; + +public abstract class AtlasJwtAuthHandler implements AtlasAuthHandler { + private static final Logger LOG = LoggerFactory.getLogger(AtlasJwtAuthHandler.class); + + private JWSVerifier verifier; + private String jwksProviderUrl; + public static final String KEY_PROVIDER_URL = "atlas.jwt.provider.url"; // JWKS provider URL + public static final String KEY_JWT_PUBLIC_KEY = "atlas.jwt.public-key"; // JWT token provider public key + public static final String KEY_JWT_COOKIE_NAME = "atlas.jwt.cookie-name"; // JWT cookie name + public static final String KEY_JWT_AUDIENCES = "atlas.jwt.audiences"; + public static final String JWT_AUTHZ_PREFIX = "Bearer "; + + protected List audiences; + protected JWKSource keySource; + + protected static String cookieName = "hadoop-jwt"; + + @Override + public void initialize(final Configuration configuration) throws Exception { + LOG.debug("===>>> AtlasJwtAuthHandler.initialize()"); + + // mandatory configurations + jwksProviderUrl = configuration.getString(KEY_PROVIDER_URL); + if (!StringUtils.isBlank(jwksProviderUrl)) { + keySource = new RemoteJWKSet<>(new URL(jwksProviderUrl)); + } + + // optional configurations + String pemPublicKey = configuration.getString(KEY_JWT_PUBLIC_KEY); + + // setup JWT provider public key if configured + if (StringUtils.isNotBlank(pemPublicKey)) { + verifier = new RSASSAVerifier(parseJwtPublicKey(pemPublicKey)); + } else if (StringUtils.isBlank(jwksProviderUrl)) { + throw new Exception("AtlasJwtAuthHandler: Mandatory configs ('jwks.provider-url' & 'jwt.public-key') are missing, must provide atleast one."); + } + + // setup custom cookie name if configured + String customCookieName = configuration.getString(KEY_JWT_COOKIE_NAME); + if (customCookieName != null) { + cookieName = customCookieName; + } + + // setup audiences if configured + String audiencesStr = configuration.getString(KEY_JWT_AUDIENCES); + if (StringUtils.isNotBlank(audiencesStr)) { + audiences = Arrays.asList(audiencesStr.split(",")); + } + + LOG.debug("<<<=== AtlasJwtAuthHandler.initialize()"); + + } + + protected String authenticate(final String jwtAuthHeader, final String jwtCookie) { + LOG.debug("===>>> AtlasJwtAuthHandler.authenticate()"); + + if (shouldProceedAuth(jwtAuthHeader, jwtCookie)) { + String serializedJWT = getJWT(jwtAuthHeader, jwtCookie); + + if (StringUtils.isNotBlank(serializedJWT)) { + try { + final SignedJWT jwtToken = SignedJWT.parse(serializedJWT); + boolean valid = validateToken(jwtToken); + if (valid) { + final String userName = jwtToken.getJWTClaimsSet().getSubject(); + LOG.info("JWT claims validated; issuing principal user={}", userName); + return userName; + } else { + String sub = null; + try { + sub = jwtToken.getJWTClaimsSet().getSubject(); + } catch (ParseException ignored) { + // ignore + } + LOG.warn("JWT validation failed (signature, audience, or expiry). subject={}", sub); + } + } catch (ParseException pe) { + LOG.warn("Unable to parse the JWT token", pe); + } + } else { + LOG.warn("JWT token not found."); + } + } + + LOG.debug("<<<=== AtlasJwtAuthHandler.authenticate()"); + + return null; + } + + protected String getJWT(final String jwtAuthHeader, final String jwtCookie) { + String serializedJWT = null; + + // try to fetch from AUTH header + if (StringUtils.isNotBlank(jwtAuthHeader) && jwtAuthHeader.startsWith(JWT_AUTHZ_PREFIX)) { + serializedJWT = jwtAuthHeader.substring(JWT_AUTHZ_PREFIX.length()); + } + + // if not found in AUTH header, try to fetch from cookie + if (StringUtils.isBlank(serializedJWT) && StringUtils.isNotBlank(jwtCookie)) { + String[] cookie = jwtCookie.split("="); + if (cookieName.equals(cookie[0])) { + serializedJWT = cookie[1]; + } + } + + return serializedJWT; + } + + /** + * This method provides a single method for validating the JWT for use in + * request processing. It provides for the override of specific aspects of this + * implementation through submethods used within but also allows for the + * override of the entire token validation algorithm. + * + * @param jwtToken the token to validate + * @return true if valid + */ + protected boolean validateToken(final SignedJWT jwtToken) { + boolean expValid = validateExpiration(jwtToken); + boolean sigValid = false; + boolean audValid = false; + + if (expValid) { + sigValid = validateSignature(jwtToken); + + if (sigValid) { + audValid = validateAudiences(jwtToken); + } + } + + LOG.debug("expValid={}, sigValid={}, audValid={}", expValid, sigValid, audValid); + + return sigValid && audValid && expValid; + } + + /** + * Verify the signature of the JWT token in this method. This method depends on + * the public key that was established during init based upon the provisioned + * public key. Override this method in subclasses in order to customize the + * signature verification behavior. + * + * @param jwtToken the token that contains the signature to be validated + * @return valid true if signature verifies successfully; false otherwise + */ + protected boolean validateSignature(final SignedJWT jwtToken) { + boolean valid = false; + + if (JWSObject.State.SIGNED == jwtToken.getState()) { + LOG.debug("JWT token is in a SIGNED state"); + + if (jwtToken.getSignature() != null) { + try { + if (StringUtils.isNotBlank(jwksProviderUrl)) { + JWSKeySelector keySelector = new JWSVerificationKeySelector(jwtToken.getHeader().getAlgorithm(), keySource); + + // Create a JWT processor for the access tokens + ConfigurableJWTProcessor jwtProcessor = getJwtProcessor(keySelector); + + // Process the token + jwtProcessor.process(jwtToken, null); + valid = true; + LOG.debug("JWT token has been successfully verified."); + } else if (verifier != null) { + if (jwtToken.verify(verifier)) { + valid = true; + LOG.debug("JWT token has been successfully verified."); + } else { + LOG.warn("JWT signature verification failed."); + } + } else { + LOG.warn("Cannot authenticate JWT token as neither JWKS provider URL nor public key provided."); + } + } catch (JOSEException | BadJOSEException e) { + LOG.error("Error while validating signature.", e); + } + } + } + + if (!valid) { + LOG.warn("Signature could not be verified."); + } + + return valid; + } + + private static RSAPublicKey parseJwtPublicKey(String pem) throws Exception { + String trimmed = StringUtils.trimToEmpty(pem); + + if (trimmed.contains("BEGIN CERTIFICATE")) { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + try (ByteArrayInputStream input = new ByteArrayInputStream(trimmed.getBytes(StandardCharsets.UTF_8))) { + X509Certificate cert = (X509Certificate) factory.generateCertificate(input); + PublicKey key = cert.getPublicKey(); + + if (key instanceof RSAPublicKey) { + return (RSAPublicKey) key; + } + } + + throw new IllegalArgumentException("Certificate does not contain an RSA public key"); + } + + String base64 = trimmed + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s", ""); + + byte[] decoded = Base64.getDecoder().decode(base64); + X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded); + KeyFactory kf = KeyFactory.getInstance("RSA"); + PublicKey key = kf.generatePublic(spec); + + if (key instanceof RSAPublicKey) { + return (RSAPublicKey) key; + } + + throw new IllegalArgumentException("Provided key is not an RSA public key"); + + } + + public abstract ConfigurableJWTProcessor getJwtProcessor(JWSKeySelector keySelector); + + /** + * Validate whether any of the accepted audience claims is present in the issued + * token claims list for audience. Override this method in subclasses in order + * to customize the audience validation behavior. + * + * @param jwtToken the JWT token where the allowed audiences will be found + * @return true if an expected audience is present, otherwise false + */ + protected boolean validateAudiences(final SignedJWT jwtToken) { + boolean valid = false; + try { + List tokenAudienceList = jwtToken.getJWTClaimsSet().getAudience(); + // if there were no expected audiences configured then just + // consider any audience acceptable + if (audiences == null) { + valid = true; + } else { + // if any of the configured audiences is found then consider it + // acceptable + for (String aud : tokenAudienceList) { + if (audiences.contains(aud)) { + LOG.debug("JWT token audience has been successfully validated."); + valid = true; + break; + } + } + if (!valid) { + LOG.warn("JWT audience validation failed."); + } + } + } catch (ParseException pe) { + LOG.warn("Unable to parse the JWT token.", pe); + } + return valid; + } + + /** + * Validate that the expiration time of the JWT token has not been violated. If + * it has then throw an AuthenticationException. Override this method in + * subclasses in order to customize the expiration validation behavior. + * + * @param jwtToken the token that contains the expiration date to validate + * @return valid true if the token has not expired; false otherwise + */ + protected boolean validateExpiration(final SignedJWT jwtToken) { + boolean valid = false; + try { + Date expires = jwtToken.getJWTClaimsSet().getExpirationTime(); + if (expires == null || new Date().before(expires)) { + valid = true; + LOG.debug("JWT token expiration date has been successfully validated."); + } else { + LOG.warn("JWT token provided is expired."); + } + } catch (ParseException pe) { + LOG.warn("Failed to validate JWT expiry.", pe); + } + + return valid; + } + + public static boolean shouldProceedAuth(final String authHeader, final String jwtCookie) { + return (StringUtils.isNotBlank(authHeader) && authHeader.startsWith(JWT_AUTHZ_PREFIX)) || (StringUtils.isNotBlank(jwtCookie) && jwtCookie.startsWith(cookieName)); + } +} diff --git a/client/common/pom.xml b/client/common/pom.xml index 517c3030a29..735bb279b7b 100644 --- a/client/common/pom.xml +++ b/client/common/pom.xml @@ -48,6 +48,13 @@ org.apache.atlas atlas-intg + + + org.apache.commons + commons-configuration2 + ${commons-conf2.version} + + org.apache.httpcomponents diff --git a/client/common/src/main/java/org/apache/atlas/AtlasBaseClient.java b/client/common/src/main/java/org/apache/atlas/AtlasBaseClient.java index 2faa7c5e11b..2bb02045704 100644 --- a/client/common/src/main/java/org/apache/atlas/AtlasBaseClient.java +++ b/client/common/src/main/java/org/apache/atlas/AtlasBaseClient.java @@ -43,6 +43,8 @@ import org.apache.atlas.model.impexp.AtlasServer; import org.apache.atlas.model.metrics.AtlasMetrics; import org.apache.atlas.security.SecureClientUtils; +import org.apache.atlas.token.retriever.JwTokenRetrieverDefault; +import org.apache.atlas.token.retriever.TokenRetriever; import org.apache.atlas.type.AtlasType; import org.apache.atlas.utils.AtlasJson; import org.apache.atlas.utils.AuthenticationUtil; @@ -69,8 +71,10 @@ import java.net.URI; import java.util.List; import java.util.Map; +import java.util.Optional; import static org.apache.atlas.security.SecurityProperties.TLS_ENABLED; +import static org.apache.atlas.token.retriever.JwTokenRetrieverDefault.JWT_SOURCE; public abstract class AtlasBaseClient { private static final Logger LOG = LoggerFactory.getLogger(AtlasBaseClient.class); @@ -109,6 +113,8 @@ public abstract class AtlasBaseClient { private static final API EXPORT = new API(BASE_URI + ADMIN_EXPORT, HttpMethod.POST, Response.Status.OK, MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM); private static final String IMPORT_REQUEST_PARAMTER = "request"; private static final String IMPORT_DATA_PARAMETER = "data"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String JWT_AUTHZ_PREFIX = "Bearer "; protected WebResource service; protected Configuration configuration; @@ -117,6 +123,8 @@ public abstract class AtlasBaseClient { private AtlasClientContext atlasClientContext; private boolean retryEnabled; private Cookie cookie; + private boolean useJwtAuth; + private TokenRetriever tokenRetriever; private SecureClientUtils clientUtils; @@ -361,7 +369,7 @@ protected Client getClient(Configuration configuration, UserGroupInformation ugi final URLConnectionClientHandler handler; - if (isKerberosEnabled) { + if (isKerberosEnabled && !useJwtAuth) { handler = clientUtils.getClientConnectionHandler(config, configuration, doAsUser, ugi); } else { if (configuration.getBoolean(TLS_ENABLED, false)) { @@ -457,6 +465,8 @@ protected T callAPIWithResource(API api, WebResource resource, Object reques requestBuilder.cookie(cookie); } + handleJwt(requestBuilder); + clientResponse = requestBuilder.method(api.getMethod(), ClientResponse.class, requestObject); LOG.debug("HTTP Status : {}", clientResponse.getStatus()); @@ -538,10 +548,12 @@ void initializeState(String[] baseUrls, UserGroupInformation ugi, String doAsUse void initializeState(Configuration configuration, String[] baseUrls, UserGroupInformation ugi, String doAsUser) { this.configuration = configuration; + useJwtAuth = isJwtSourceConfigured(configuration); + tokenRetriever = useJwtAuth ? getJwtTokenRetriever(configuration) : null; Client client = getClient(configuration, ugi, doAsUser); - if ((!AuthenticationUtil.isKerberosAuthenticationEnabled()) && basicAuthUser != null && basicAuthPassword != null) { + if (!useJwtAuth && (!AuthenticationUtil.isKerberosAuthenticationEnabled()) && basicAuthUser != null && basicAuthPassword != null) { final HTTPBasicAuthFilter authFilter = new HTTPBasicAuthFilter(basicAuthUser, basicAuthPassword); client.addFilter(authFilter); @@ -553,6 +565,32 @@ void initializeState(Configuration configuration, String[] baseUrls, UserGroupIn service = client.resource(UriBuilder.fromUri(activeServiceUrl).build()); } + private TokenRetriever getJwtTokenRetriever(Configuration configuration) { + return new JwTokenRetrieverDefault(configuration); + } + + private boolean isJwtSourceConfigured(Configuration configuration) { + String jwtSource = configuration.getString(JWT_SOURCE, ""); + return StringUtils.isNotBlank(jwtSource); + } + + private void handleJwt(WebResource.Builder requestBuilder) { + if (!useJwtAuth) { + return; + } + if (tokenRetriever == null) { + LOG.warn("AtlasBaseClient.handleJwt(): tokenRetriever is null. Skipping JWT header injection."); + return; + } + + Optional jwtOptional = tokenRetriever.retrieve(); + if (jwtOptional.isPresent()) { + requestBuilder.header(AUTHORIZATION_HEADER, JWT_AUTHZ_PREFIX + jwtOptional.get()); + } else { + LOG.warn("AtlasBaseClient.handleJwt(): JWT token not available from configured retriever. Authorization header not set."); + } + } + void sleepBetweenRetries() { try { Thread.sleep(getSleepBetweenRetriesMs()); diff --git a/client/common/src/main/java/org/apache/atlas/token/retriever/JwTokenRetrieverDefault.java b/client/common/src/main/java/org/apache/atlas/token/retriever/JwTokenRetrieverDefault.java new file mode 100644 index 00000000000..a453ada2d23 --- /dev/null +++ b/client/common/src/main/java/org/apache/atlas/token/retriever/JwTokenRetrieverDefault.java @@ -0,0 +1,152 @@ +/** + * 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.atlas.token.retriever; + +import org.apache.atlas.security.SecurityUtil; +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Optional; + +public class JwTokenRetrieverDefault implements TokenRetriever { + private static final Logger LOG = LoggerFactory.getLogger(JwTokenRetrieverDefault.class); + + public static final String JWT_SOURCE = "atlas.jwt.source"; + public static final String JWT_ENV = "atlas.jwt.env"; + public static final String JWT_FILE = "atlas.jwt.file"; + public static final String JWT_CRED_FILE = "atlas.jwt.cred.file"; + public static final String JWT_CRED_ALIAS = "atlas.jwt.cred.alias"; + + private static final String SOURCE_ENV = "env"; + private static final String SOURCE_FILE = "file"; + private static final String SOURCE_CRED = "cred"; + private static final long CRED_CHECK_INTERVAL_MS = 60 * 1000; + + private final String jwtSource; + private final String jwtEnvVar; + private final String jwtFilePath; + private final String jwtCredFilePath; + private final String jwtCredPathPropertyName; + private final String jwtCredAlias; + private final Configuration configuration; + private long jwtFileLastModified; + private long jwtCredFileLastCheckedAt; + + private volatile Optional cachedJwt = Optional.empty(); + + public JwTokenRetrieverDefault(Configuration config) { + configuration = config; + + jwtSource = StringUtils.trimToEmpty(config.getString(JWT_SOURCE, "")); + jwtEnvVar = StringUtils.trimToEmpty(config.getString(JWT_ENV, "")); + jwtFilePath = StringUtils.trimToEmpty(config.getString(JWT_FILE, "")); + jwtCredPathPropertyName = JWT_CRED_FILE; + jwtCredFilePath = StringUtils.trimToEmpty(config.getString(jwtCredPathPropertyName, "")); + jwtCredAlias = StringUtils.trimToEmpty(config.getString(JWT_CRED_ALIAS, "")); + } + + @Override + public synchronized Optional retrieve() { + String source = StringUtils.lowerCase(jwtSource); + switch (source) { + case SOURCE_ENV: + return getJwtFromEnv(); + case SOURCE_FILE: + return getJwtFromFile(); + case SOURCE_CRED: + return getJwtFromCredProvider(); + default: + if (StringUtils.isNotBlank(source)) { + LOG.warn("JwTokenRetrieverDefault.retrieve(): unsupported source='{}'", source); + } + return Optional.empty(); + } + } + + private Optional getJwtFromEnv() { + if (StringUtils.isBlank(jwtEnvVar)) { + LOG.warn("JwTokenRetrieverDefault.getJwtFromEnv(): '{}' is not configured.", JWT_ENV); + return Optional.empty(); + } + + String token = StringUtils.trimToEmpty(System.getenv(jwtEnvVar)); + return StringUtils.isBlank(token) ? Optional.empty() : Optional.of(token); + } + + private Optional getJwtFromFile() { + if (StringUtils.isBlank(jwtFilePath)) { + LOG.warn("JwTokenRetrieverDefault.getJwtFromFile(): '{}' is not configured.", JWT_FILE); + return Optional.empty(); + } + + File jwtFile = new File(jwtFilePath); + if (!jwtFile.canRead()) { + return cachedJwt; + } + + if (jwtFile.lastModified() == jwtFileLastModified && cachedJwt.isPresent()) { + return cachedJwt; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(jwtFile))) { + String line; + while ((line = reader.readLine()) != null) { + if (StringUtils.isNotBlank(line) && !line.startsWith("#")) { + cachedJwt = Optional.of(line.trim()); + jwtFileLastModified = jwtFile.lastModified(); + break; + } + } + } catch (IOException e) { + LOG.error("JwTokenRetrieverDefault.getJwtFromFile(): failed to read JWT from file={}", jwtFilePath, e); + } + + return cachedJwt; + } + + private Optional getJwtFromCredProvider() { + if (StringUtils.isBlank(jwtCredFilePath) || StringUtils.isBlank(jwtCredAlias)) { + LOG.warn("JwTokenRetrieverDefault.getJwtFromCredProvider(): '{}' or '{}' is not configured.", JWT_CRED_FILE, JWT_CRED_ALIAS); + return Optional.empty(); + } + + long now = System.currentTimeMillis(); + if ((now - jwtCredFileLastCheckedAt) <= CRED_CHECK_INTERVAL_MS && cachedJwt.isPresent()) { + return cachedJwt; + } + + try { + String token = StringUtils.trimToEmpty(SecurityUtil.getPassword(configuration, jwtCredAlias, jwtCredPathPropertyName)); + if (StringUtils.isNotBlank(token)) { + cachedJwt = Optional.of(token); + } + } catch (Exception e) { + LOG.error("JwTokenRetrieverDefault.getJwtFromCredProvider(): failed to read JWT from credential provider.", e); + } finally { + jwtCredFileLastCheckedAt = now; + } + + return cachedJwt; + } +} diff --git a/client/common/src/main/java/org/apache/atlas/token/retriever/TokenRetriever.java b/client/common/src/main/java/org/apache/atlas/token/retriever/TokenRetriever.java new file mode 100644 index 00000000000..3fd71927dce --- /dev/null +++ b/client/common/src/main/java/org/apache/atlas/token/retriever/TokenRetriever.java @@ -0,0 +1,24 @@ +/** + * 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.atlas.token.retriever; + +import java.util.Optional; + +public interface TokenRetriever { + Optional retrieve(); +} diff --git a/distro/src/conf/atlas-application.properties b/distro/src/conf/atlas-application.properties index b5734d7a8de..4d766b55734 100755 --- a/distro/src/conf/atlas-application.properties +++ b/distro/src/conf/atlas-application.properties @@ -281,4 +281,10 @@ atlas.search.gremlin.enable=false ######### Skip check for the same attribute name in Parent type and Child type ######### -#atlas.skip.check.for.parent.child.attribute.name=true \ No newline at end of file +#atlas.skip.check.for.parent.child.attribute.name=true + +######### Header Based Authentication ######### +atlas.authn.header.enabled=false +atlas.authn.header.username=x-awc-username +atlas.authn.header.roles=x-awc-roles +atlas.authn.header.requestid=x-awc-requestid \ No newline at end of file diff --git a/pom.xml b/pom.xml index c0f4c05b022..d15fd95694c 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ addons/storm-bridge-shim addons/trino-extractor atlas-examples + authn authorization build-tools client diff --git a/webapp/pom.xml b/webapp/pom.xml index 87760e5d226..376e8ef471e 100755 --- a/webapp/pom.xml +++ b/webapp/pom.xml @@ -76,6 +76,12 @@ + + org.apache.atlas + atlas-authn + ${project.version} + + com.sun.jersey jersey-client diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationFilter.java b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationFilter.java index bc477f76242..d72122aa6d4 100644 --- a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationFilter.java +++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationFilter.java @@ -427,7 +427,7 @@ public void doFilter(final ServletRequest request, final ServletResponse respons if (existingAuth == null) { String authHeader = httpRequest.getHeader("Authorization"); - if (authHeader != null && authHeader.startsWith("Basic")) { + if (authHeader != null && (authHeader.startsWith("Basic") || authHeader.startsWith("Bearer"))) { filterChain.doFilter(request, response); } else if (isKerberos) { doKerberosAuth(request, response, filterChain); diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationToken.java b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationToken.java new file mode 100644 index 00000000000..15cd70b9a7d --- /dev/null +++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationToken.java @@ -0,0 +1,55 @@ +/* + * 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.atlas.web.filters; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +public class AtlasAuthenticationToken extends AbstractAuthenticationToken { + public static final int AUTH_TYPE_TRUSTED_PROXY = 3; + + private final UserDetails principal; + private final int authType; + + public AtlasAuthenticationToken(UserDetails principal, Collection authorities, int authType) { + super(authorities); + + this.principal = principal; + this.authType = authType; + + super.setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Object getCredentials() { + return null; + } + + public int getAuthType() { + return authType; + } +} diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasHeaderPreAuthFilter.java b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasHeaderPreAuthFilter.java new file mode 100644 index 00000000000..42fb54b5971 --- /dev/null +++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasHeaderPreAuthFilter.java @@ -0,0 +1,169 @@ +/* + * 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 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.atlas.web.filters; + +import org.apache.atlas.ApplicationProperties; +import org.apache.atlas.web.model.User; +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetails; + +import javax.inject.Inject; +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.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class AtlasHeaderPreAuthFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(AtlasHeaderPreAuthFilter.class); + + public static final String PROP_HEADER_AUTH_ENABLED = "atlas.authn.header.enabled"; + public static final String PROP_USERNAME_HEADER = "atlas.authn.header.username"; + public static final String PROP_ROLES_HEADER = "atlas.authn.header.roles"; + public static final String PROP_REQUEST_ID_HEADER = "atlas.authn.header.requestid"; + public static final String REQUEST_ID_ATTRIBUTE = "atlas.request.id"; + + private Configuration configuration; + private boolean headerAuthEnabled; + private String userNameHeaderName; + private String rolesHeaderName; + + @Inject + public AtlasHeaderPreAuthFilter() { + loadConfiguration(); + } + + private void loadConfiguration() { + try { + configuration = ApplicationProperties.get(); + } catch (Exception e) { + LOG.error("Error loading application properties for header pre-auth", e); + configuration = null; + } + if (configuration == null) { + headerAuthEnabled = false; + userNameHeaderName = null; + rolesHeaderName = null; + return; + } + headerAuthEnabled = configuration.getBoolean(PROP_HEADER_AUTH_ENABLED, false); + userNameHeaderName = StringUtils.trimToNull(configuration.getString(PROP_USERNAME_HEADER, "")); + rolesHeaderName = StringUtils.trimToNull(configuration.getString(PROP_ROLES_HEADER, "")); + } + + private List getAuthoritiesFromRolesHeader(String rolesHeader) { + List ret = new ArrayList<>(); + if (StringUtils.isBlank(rolesHeader)) { + return ret; + } + + for (String role : rolesHeader.split(",")) { + String trimmed = StringUtils.trimToNull(role); + if (trimmed != null) { + ret.add(new org.springframework.security.core.authority.SimpleGrantedAuthority(trimmed)); + } + } + + return ret; + } + + @Override + public void init(FilterConfig filterConfig) { + loadConfiguration(); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + + AtlasResponseRequestWrapper responseWrapper = new AtlasResponseRequestWrapper(httpResponse); + HeadersUtil.setSecurityHeaders(responseWrapper); + + try { + if (headerAuthEnabled) { + Authentication pre = SecurityContextHolder.getContext().getAuthentication(); + + if (pre == null || !pre.isAuthenticated()) { + String username = StringUtils.trimToNull(httpRequest.getHeader(userNameHeaderName)); + + if (username != null) { + List grantedAuths = + getAuthoritiesFromRolesHeader(httpRequest.getHeader(rolesHeaderName)); + + UserDetails principal = new User(username, "", grantedAuths); + + AtlasAuthenticationToken token = + new AtlasAuthenticationToken(principal, grantedAuths, + AtlasAuthenticationToken.AUTH_TYPE_TRUSTED_PROXY); + + token.setDetails(new WebAuthenticationDetails(httpRequest)); + + SecurityContextHolder.getContext().setAuthentication(token); + } + } + } + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String requestId = getRequestId(auth, httpRequest); + httpRequest.setAttribute(REQUEST_ID_ATTRIBUTE, requestId); + + chain.doFilter(servletRequest, responseWrapper); + + } catch (IOException e) { + throw new RuntimeException(e); + } catch (ServletException e) { + throw new RuntimeException(e); + } + } + + private String getRequestId(Authentication auth, HttpServletRequest request) { + String ret = null; + + String requestIdHeaderName = configuration != null ? configuration.getString(PROP_REQUEST_ID_HEADER) : null; + + if (requestIdHeaderName != null && + auth instanceof AtlasAuthenticationToken && + ((AtlasAuthenticationToken) auth).getAuthType() == AtlasAuthenticationToken.AUTH_TYPE_TRUSTED_PROXY) { + + ret = StringUtils.trimToNull(request.getHeader(requestIdHeaderName)); + } + + return ret != null ? ret : UUID.randomUUID().toString(); + } + + @Override + public void destroy() { + } +} diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthFilter.java b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthFilter.java new file mode 100644 index 00000000000..95b74eef5e7 --- /dev/null +++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthFilter.java @@ -0,0 +1,119 @@ +/* + * 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.atlas.web.filters; + +import org.apache.atlas.ApplicationProperties; +import org.apache.atlas.authn.handler.AtlasAuth; +import org.apache.atlas.authn.handler.jwt.AtlasDefaultJwtAuthHandler; +import org.apache.atlas.authn.handler.jwt.AtlasJwtAuthHandler; +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +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.HttpServletRequest; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +@Lazy(true) +@Component +public class AtlasJwtAuthFilter extends AtlasDefaultJwtAuthHandler implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(AtlasJwtAuthFilter.class); + private static final String DEFAULT_ATLAS_ROLE = "ROLE_USER"; + private static Configuration configuration; + + @PostConstruct + public void initialize() { + LOG.debug("===>>> AtlasJwtAuthFilter.initialize()"); + + try { + configuration = ApplicationProperties.get(); + if (StringUtils.isEmpty(configuration.getString(AtlasJwtAuthHandler.KEY_PROVIDER_URL))) { + configuration.setProperty(AtlasJwtAuthHandler.KEY_PROVIDER_URL, + configuration.getString(AtlasKnoxSSOAuthenticationFilter.JWT_AUTH_PROVIDER_URL)); + } + configuration.setProperty(AtlasJwtAuthHandler.KEY_JWT_PUBLIC_KEY, + configuration.getString(AtlasKnoxSSOAuthenticationFilter.JWT_PUBLIC_KEY, "")); + configuration.setProperty(AtlasJwtAuthHandler.KEY_JWT_COOKIE_NAME, + configuration.getString(AtlasKnoxSSOAuthenticationFilter.JWT_COOKIE_NAME, + AtlasKnoxSSOAuthenticationFilter.JWT_COOKIE_NAME_DEFAULT)); + configuration.setProperty(AtlasJwtAuthHandler.KEY_JWT_AUDIENCES, + configuration.getString(AtlasKnoxSSOAuthenticationFilter.JWT_AUDIENCES, "")); + + super.initialize(configuration); + } catch (Exception e) { + LOG.error("Failed to initialize Atlas JWT Auth Filter.", e); + } + + LOG.debug("<<<=== AtlasJwtAuthFilter.initialize()"); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + LOG.debug("===>>> AtlasJwtAuthFilter.doFilter()"); + + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + AtlasAuth atlasAuth = authenticate(httpServletRequest); + + if (atlasAuth != null) { + final List grantedAuths = Arrays.asList(new SimpleGrantedAuthority(DEFAULT_ATLAS_ROLE)); + final UserDetails principal = new User(atlasAuth.getUserName(), "", grantedAuths); + final Authentication finalAuthentication = new UsernamePasswordAuthenticationToken(principal, "", grantedAuths); + final WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpServletRequest); + ((AbstractAuthenticationToken) finalAuthentication).setDetails(webDetails); + SecurityContextHolder.getContext().setAuthentication(finalAuthentication); + } + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + LOG.debug("<<<=== AtlasJwtAuthFilter.doFilter() - user=[{}], isUserAuthenticated? [{}]", + auth.getPrincipal(), auth.isAuthenticated()); + } else { + LOG.warn("<<<=== AtlasJwtAuthFilter.doFilter() - Failed to authenticate request using Atlas JWT authentication framework."); + } + } + + @Override + public void destroy() { + } +} diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapper.java b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapper.java new file mode 100644 index 00000000000..d079228c2a7 --- /dev/null +++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapper.java @@ -0,0 +1,111 @@ +/* + * 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.atlas.web.filters; + +import org.apache.atlas.utils.PropertiesUtil; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; + +import javax.annotation.PostConstruct; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Lazy +@Component +public class AtlasJwtAuthWrapper extends GenericFilterBean { + private static final Logger LOG = LoggerFactory.getLogger(AtlasJwtAuthWrapper.class); + + private String[] browserUserAgents = new String[] {""}; + + @PostConstruct + public void initialize() { + String defaultUserAgent = PropertiesUtil.getProperty(AtlasKnoxSSOAuthenticationFilter.DEFAULT_BROWSER_USERAGENT); + String userAgent = PropertiesUtil.getProperty(AtlasKnoxSSOAuthenticationFilter.BROWSER_USERAGENT); + + if (StringUtils.isBlank(userAgent) && StringUtils.isNotBlank(defaultUserAgent)) { + userAgent = defaultUserAgent; + } + + if (StringUtils.isNotBlank(userAgent)) { + browserUserAgents = userAgent.split(","); + } + } + + @Lazy + @Autowired + AtlasJwtAuthFilter atlasJwtAuthFilter; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + LOG.debug("===>>> AtlasJwtAuthWrapper.doFilter({}, {}, {})", servletRequest, servletResponse, filterChain); + + boolean useJwtAuthMechanism = servletRequest != null && !isRequestAuthenticated() + && AtlasJwtAuthFilter.canAuthenticateRequest(servletRequest); + + if (useJwtAuthMechanism) { + atlasJwtAuthFilter.doFilter(servletRequest, servletResponse, filterChain); + + if (!isRequestAuthenticated()) { + String userAgent = ((HttpServletRequest) servletRequest).getHeader("User-Agent"); + if (isBrowserAgent(userAgent)) { + LOG.debug("Redirecting to login page as request does not have valid JWT auth details."); + ((HttpServletResponse) servletResponse).sendRedirect("/login.jsp"); + } + } + } else { + LOG.debug("<<<=== AtlasJwtAuthWrapper.doFilter() - Skipping JWT auth."); + } + filterChain.doFilter(servletRequest, servletResponse); + + LOG.debug("<<<=== AtlasJwtAuthWrapper.doFilter()"); + } + + protected boolean isBrowserAgent(String userAgent) { + boolean isBrowserAgent = false; + + if (browserUserAgents.length > 0 && StringUtils.isNotBlank(userAgent)) { + for (String ua : browserUserAgents) { + if (userAgent.toLowerCase().startsWith(ua.toLowerCase())) { + isBrowserAgent = true; + break; + } + } + } + + return isBrowserAgent; + } + + private boolean isRequestAuthenticated() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return auth != null && auth.isAuthenticated(); + } +} diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java index c41f9d4686f..f2d43390b08 100644 --- a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java +++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java @@ -82,6 +82,7 @@ public class AtlasKnoxSSOAuthenticationFilter implements Filter { public static final String JWT_COOKIE_NAME = "atlas.sso.knox.cookiename"; public static final String JWT_ORIGINAL_URL_QUERY_PARAM = "atlas.sso.knox.query.param.originalurl"; public static final String JWT_COOKIE_NAME_DEFAULT = "hadoop-jwt"; + public static final String JWT_AUDIENCES = "atlas.sso.knox.audiences"; public static final String JWT_ORIGINAL_URL_QUERY_PARAM_DEFAULT = "originalUrl"; public static final String DEFAULT_BROWSER_USERAGENT = "Mozilla,Opera,Chrome"; public static final String PROXY_ATLAS_URL_PATH = "/atlas"; diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/AuditFilter.java b/webapp/src/main/java/org/apache/atlas/web/filters/AuditFilter.java index 207879c7a67..4a03a622002 100755 --- a/webapp/src/main/java/org/apache/atlas/web/filters/AuditFilter.java +++ b/webapp/src/main/java/org/apache/atlas/web/filters/AuditFilter.java @@ -69,12 +69,8 @@ public static void audit(AuditLog auditLog) { @Override public void init(FilterConfig filterConfig) throws ServletException { - LOG.info("AuditFilter initialization started"); - deleteTypeOverrideEnabled = REST_API_ENABLE_DELETE_TYPE_OVERRIDE.getBoolean(); createShellEntityForNonExistingReference = REST_API_CREATE_SHELL_ENTITY_FOR_NON_EXISTING_REF.getBoolean(); - - LOG.info("REST_API_ENABLE_DELETE_TYPE_OVERRIDE={}", deleteTypeOverrideEnabled); } @Override @@ -83,7 +79,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha final Date requestTime = new Date(); final HttpServletRequest httpRequest = (HttpServletRequest) request; final HttpServletResponse httpResponse = (HttpServletResponse) response; - final String requestId = UUID.randomUUID().toString(); + final String requestId = getRequestId(httpRequest); final Thread currentThread = Thread.currentThread(); final String oldName = currentThread.getName(); final String user = AtlasAuthorizationUtils.getCurrentUserName(); @@ -140,6 +136,15 @@ private String formatName(String oldName, String requestId) { return oldName + " - " + requestId; } + private String getRequestId(HttpServletRequest httpRequest) { + String requestId = (String) httpRequest.getAttribute(AtlasHeaderPreAuthFilter.REQUEST_ID_ATTRIBUTE); + if (StringUtils.isNotEmpty(requestId)) { + return requestId; + } + + return UUID.randomUUID().toString(); + } + private void recordAudit(HttpServletRequest httpRequest, Date when, String who, int httpStatus, long timeTaken) { final String fromAddress = httpRequest.getRemoteAddr(); final String whatRequest = httpRequest.getMethod(); diff --git a/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java b/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java index 0f7ad76b41d..a155c15b376 100644 --- a/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java +++ b/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java @@ -22,6 +22,8 @@ import org.apache.atlas.web.filters.AtlasAuthenticationFilter; import org.apache.atlas.web.filters.AtlasCSRFPreventionFilter; import org.apache.atlas.web.filters.AtlasDelegatingAuthenticationEntryPoint; +import org.apache.atlas.web.filters.AtlasHeaderPreAuthFilter; +import org.apache.atlas.web.filters.AtlasJwtAuthWrapper; import org.apache.atlas.web.filters.AtlasKnoxSSOAuthenticationFilter; import org.apache.atlas.web.filters.HeadersUtil; import org.apache.atlas.web.filters.StaleTransactionCleanupFilter; @@ -97,6 +99,8 @@ public class AtlasSecurityConfig extends WebSecurityConfigurerAdapter { private final AtlasAuthenticationFilter atlasAuthenticationFilter; private final AtlasCSRFPreventionFilter csrfPreventionFilter; private final AtlasAuthenticationEntryPoint atlasAuthenticationEntryPoint; + private final AtlasHeaderPreAuthFilter headerPreAuthFilter; + private final AtlasJwtAuthWrapper atlasJwtAuthWrapper; // Our own Atlas filters need to be registered as well private final Configuration configuration; @@ -120,7 +124,9 @@ public AtlasSecurityConfig(AtlasKnoxSSOAuthenticationFilter ssoAuthenticationFil AtlasAuthenticationEntryPoint atlasAuthenticationEntryPoint, Configuration configuration, StaleTransactionCleanupFilter staleTransactionCleanupFilter, - ActiveServerFilter activeServerFilter) { + ActiveServerFilter activeServerFilter, + AtlasHeaderPreAuthFilter headerPreAuthFilter, + AtlasJwtAuthWrapper atlasJwtAuthWrapper) { this.ssoAuthenticationFilter = ssoAuthenticationFilter; this.csrfPreventionFilter = atlasCSRFPreventionFilter; this.atlasAuthenticationFilter = atlasAuthenticationFilter; @@ -131,6 +137,8 @@ public AtlasSecurityConfig(AtlasKnoxSSOAuthenticationFilter ssoAuthenticationFil this.configuration = configuration; this.staleTransactionCleanupFilter = staleTransactionCleanupFilter; this.activeServerFilter = activeServerFilter; + this.headerPreAuthFilter = headerPreAuthFilter; + this.atlasJwtAuthWrapper = atlasJwtAuthWrapper; this.keycloakEnabled = configuration.getBoolean(AtlasAuthenticationProvider.KEYCLOAK_AUTH_METHOD, false); } @@ -238,9 +246,19 @@ protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.addFilterAfter(activeServerFilter, BasicAuthenticationFilter.class); } - httpSecurity.addFilterAfter(staleTransactionCleanupFilter, BasicAuthenticationFilter.class) - .addFilterBefore(ssoAuthenticationFilter, BasicAuthenticationFilter.class) - .addFilterAfter(atlasAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class) + httpSecurity.addFilterAfter(staleTransactionCleanupFilter, BasicAuthenticationFilter.class); + + if (headerPreAuthFilter != null) { + httpSecurity.addFilterBefore(headerPreAuthFilter, BasicAuthenticationFilter.class); + } + + httpSecurity.addFilterBefore(ssoAuthenticationFilter, BasicAuthenticationFilter.class); + + if (atlasJwtAuthWrapper != null) { + httpSecurity.addFilterAfter(atlasJwtAuthWrapper, AtlasKnoxSSOAuthenticationFilter.class); + } + + httpSecurity.addFilterAfter(atlasAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class) .addFilterAfter(csrfPreventionFilter, AtlasAuthenticationFilter.class); if (keycloakEnabled) { diff --git a/webapp/src/main/resources/spring-security.xml b/webapp/src/main/resources/spring-security.xml index 3020690dca0..ea41d46ab72 100644 --- a/webapp/src/main/resources/spring-security.xml +++ b/webapp/src/main/resources/spring-security.xml @@ -34,6 +34,7 @@ + @@ -54,6 +55,9 @@ + + + diff --git a/webapp/src/test/java/org/apache/atlas/web/filters/AtlasHeaderPreAuthFilterTest.java b/webapp/src/test/java/org/apache/atlas/web/filters/AtlasHeaderPreAuthFilterTest.java new file mode 100644 index 00000000000..56d0dc32424 --- /dev/null +++ b/webapp/src/test/java/org/apache/atlas/web/filters/AtlasHeaderPreAuthFilterTest.java @@ -0,0 +1,146 @@ +/* + * 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.atlas.web.filters; + +import org.apache.atlas.ApplicationProperties; +import org.apache.commons.configuration2.Configuration; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +public class AtlasHeaderPreAuthFilterTest { + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private FilterChain filterChain; + + @Mock + private Configuration configuration; + + @BeforeMethod + public void setUp() { + MockitoAnnotations.openMocks(this); + SecurityContextHolder.clearContext(); + } + + @AfterMethod + public void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + public void testDoFilterEnabledWithUsernameAndRoles() throws Exception { + when(configuration.getBoolean(AtlasHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED, false)).thenReturn(true); + when(configuration.getString(AtlasHeaderPreAuthFilter.PROP_USERNAME_HEADER, "")) + .thenReturn("x-user"); + when(configuration.getString(AtlasHeaderPreAuthFilter.PROP_ROLES_HEADER, "")) + .thenReturn("x-roles"); + when(request.getHeader("x-user")).thenReturn("alice"); + when(request.getHeader("x-roles")).thenReturn("ROLE_ADMIN, ROLE_USER"); + + try (MockedStatic appProps = org.mockito.Mockito.mockStatic(ApplicationProperties.class)) { + appProps.when(ApplicationProperties::get).thenReturn(configuration); + + AtlasHeaderPreAuthFilter filter = new AtlasHeaderPreAuthFilter(); + filter.doFilter(request, response, filterChain); + } + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + assertNotNull(auth); + assertTrue(auth instanceof AtlasAuthenticationToken); + + AtlasAuthenticationToken token = (AtlasAuthenticationToken) auth; + UserDetails principal = (UserDetails) token.getPrincipal(); + + assertEquals(principal.getUsername(), "alice"); + assertEquals(token.getAuthorities().size(), 2); + assertEquals(token.getAuthType(), AtlasAuthenticationToken.AUTH_TYPE_TRUSTED_PROXY); + verify(filterChain).doFilter(eq(request), any(HttpServletResponse.class)); + } + + @Test + public void testDoFilterEnabledWithoutUsernameDoesNotAuthenticate() throws IOException, ServletException { + when(configuration.getBoolean(AtlasHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED, false)).thenReturn(true); + when(configuration.getString(AtlasHeaderPreAuthFilter.PROP_USERNAME_HEADER, "")) + .thenReturn("x-user"); + when(configuration.getString(AtlasHeaderPreAuthFilter.PROP_ROLES_HEADER, "")) + .thenReturn("x-roles"); + when(request.getHeader("x-user")).thenReturn(" "); + + try (MockedStatic appProps = org.mockito.Mockito.mockStatic(ApplicationProperties.class)) { + appProps.when(ApplicationProperties::get).thenReturn(configuration); + + AtlasHeaderPreAuthFilter filter = new AtlasHeaderPreAuthFilter(); + filter.doFilter(request, response, filterChain); + } + + assertEquals(SecurityContextHolder.getContext().getAuthentication(), null); + verify(filterChain).doFilter(eq(request), any(HttpServletResponse.class)); + } + + @Test + public void testDoFilterEnabledKeepsExistingAuthentication() throws IOException, ServletException { + Authentication existing = org.mockito.Mockito.mock(Authentication.class); + when(existing.isAuthenticated()).thenReturn(true); + SecurityContextHolder.getContext().setAuthentication(existing); + + when(configuration.getBoolean(AtlasHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED, false)).thenReturn(true); + when(configuration.getString(AtlasHeaderPreAuthFilter.PROP_USERNAME_HEADER, "")) + .thenReturn("x-user"); + when(configuration.getString(AtlasHeaderPreAuthFilter.PROP_ROLES_HEADER, "")) + .thenReturn("x-roles"); + when(request.getHeader("x-user")).thenReturn("alice"); + + try (MockedStatic appProps = org.mockito.Mockito.mockStatic(ApplicationProperties.class)) { + appProps.when(ApplicationProperties::get).thenReturn(configuration); + + AtlasHeaderPreAuthFilter filter = new AtlasHeaderPreAuthFilter(); + filter.doFilter(request, response, filterChain); + } + + assertSame(SecurityContextHolder.getContext().getAuthentication(), existing); + verify(filterChain).doFilter(eq(request), any(HttpServletResponse.class)); + } +} diff --git a/webapp/src/test/java/org/apache/atlas/web/security/AtlasSecurityConfigTest.java b/webapp/src/test/java/org/apache/atlas/web/security/AtlasSecurityConfigTest.java index a0d14f293d4..3882a8d7f45 100644 --- a/webapp/src/test/java/org/apache/atlas/web/security/AtlasSecurityConfigTest.java +++ b/webapp/src/test/java/org/apache/atlas/web/security/AtlasSecurityConfigTest.java @@ -25,6 +25,8 @@ import org.apache.atlas.web.filters.AtlasCSRFPreventionFilter; import org.apache.atlas.web.filters.AtlasDelegatingAuthenticationEntryPoint; import org.apache.atlas.web.filters.AtlasKnoxSSOAuthenticationFilter; +import org.apache.atlas.web.filters.AtlasHeaderPreAuthFilter; +import org.apache.atlas.web.filters.AtlasJwtAuthWrapper; import org.apache.atlas.web.filters.StaleTransactionCleanupFilter; import org.apache.commons.configuration2.Configuration; import org.keycloak.adapters.AdapterDeploymentContext; @@ -128,6 +130,12 @@ public class AtlasSecurityConfigTest { @Mock private ActiveServerFilter mockActiveServerFilter; + @Mock + private AtlasHeaderPreAuthFilter mockHeaderPreAuthFilter; + + @Mock + private AtlasJwtAuthWrapper mockAtlasJwtAuthWrapper; + @Mock private KeycloakConfigResolver mockKeycloakConfigResolver; @@ -169,7 +177,9 @@ public void testConstructor_WithKeycloakDisabled() throws Exception { mockAtlasAuthenticationEntryPoint, mockConfiguration, mockStaleTransactionCleanupFilter, - mockActiveServerFilter); + mockActiveServerFilter, + mockHeaderPreAuthFilter, + mockAtlasJwtAuthWrapper); // Verify using reflection assertFalse((Boolean) getPrivateField(atlasSecurityConfig, "keycloakEnabled")); @@ -195,7 +205,9 @@ public void testConstructor_WithKeycloakEnabled() throws Exception { mockAtlasAuthenticationEntryPoint, mockConfiguration, mockStaleTransactionCleanupFilter, - mockActiveServerFilter); + mockActiveServerFilter, + mockHeaderPreAuthFilter, + mockAtlasJwtAuthWrapper); // Verify using reflection assertTrue((Boolean) getPrivateField(atlasSecurityConfig, "keycloakEnabled")); @@ -355,7 +367,9 @@ private void testHttpSecurityConfiguration(boolean migrationEnabled, boolean haE mockAtlasAuthenticationEntryPoint, mockConfiguration, mockStaleTransactionCleanupFilter, - mockActiveServerFilter); + mockActiveServerFilter, + mockHeaderPreAuthFilter, + mockAtlasJwtAuthWrapper); // Set up comprehensive HttpSecurity mocking first setupHttpSecurityMocks(); @@ -439,7 +453,9 @@ private void testDetailedHttpSecurityConfiguration(boolean migrationEnabled, boo mockAtlasAuthenticationEntryPoint, mockConfiguration, mockStaleTransactionCleanupFilter, - mockActiveServerFilter); + mockActiveServerFilter, + mockHeaderPreAuthFilter, + mockAtlasJwtAuthWrapper); // Create fresh HttpSecurity mock for each test to avoid state pollution HttpSecurity freshHttpSecurity = mock(HttpSecurity.class); @@ -590,7 +606,9 @@ public void testAdapterDeploymentContext_WithConfigFile() throws Exception { mockAtlasAuthenticationEntryPoint, mockConfiguration, mockStaleTransactionCleanupFilter, - mockActiveServerFilter); + mockActiveServerFilter, + mockHeaderPreAuthFilter, + mockAtlasJwtAuthWrapper); setPrivateField(atlasSecurityConfig, "keycloakConfigFileResource", mockKeycloakConfigFileResource); @@ -845,7 +863,9 @@ private void setupAtlasSecurityConfig(boolean keycloakEnabled) { mockAtlasAuthenticationEntryPoint, mockConfiguration, mockStaleTransactionCleanupFilter, - mockActiveServerFilter); + mockActiveServerFilter, + mockHeaderPreAuthFilter, + mockAtlasJwtAuthWrapper); // Set the keycloakConfigFileResource using reflection setPrivateField(atlasSecurityConfig, "keycloakConfigFileResource", mockKeycloakConfigFileResource);