Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions agents-audit/dest-auditserver/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
</dependency>

<!-- Test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.ranger.audit.model.AuthzAuditEvent;
import org.apache.ranger.audit.provider.MiscUtil;
import org.apache.ranger.plugin.authn.DefaultJwtProvider;
import org.apache.ranger.plugin.util.PluginHeaderAuthConfig;
import org.apache.ranger.plugin.util.RangerRESTClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -98,6 +99,16 @@ public void init(Properties props, String propPrefix) {
this.restClient.setMaxRetryAttempts(maxRetryAttempts);
this.restClient.setRetryIntervalMs(retryIntervalMs);

// Trusted header auth is orthogonal to authn.type (JWT/Basic/Kerberos): when enabled,
// trusted headers are added in addition to whatever authType configured above.
Map<String, String> trustedHeaders = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, propPrefix);

if (!trustedHeaders.isEmpty()) {
this.restClient.setTrustedAuthHeaders(trustedHeaders);

LOG.debug("Trusted authentication headers added for audit-server destination");
}

LOG.info("<== RangerAuditServerDestination:init()");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.ranger.audit.destination;

import org.apache.ranger.plugin.util.PluginHeaderAuthConfig;
import org.junit.jupiter.api.Test;

import java.util.Map;
import java.util.Properties;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class RangerAuditServerDestinationTest {
private static final String AUDIT_DEST_PREFIX = "xasecure.audit.destination.auditserver";

@Test
public void buildTrustedAuthHeadersUsesAuditDestinationPrefix() {
Properties props = new Properties();

props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "true");
props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive");

Map<String, String> headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX);

assertEquals(1, headers.size());
assertEquals("spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive", headers.get("X-Spiffe-Id"));
}

@Test
public void buildTrustedAuthHeadersEmptyWhenAuditDestinationDisabled() {
Properties props = new Properties();

props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "false");
Comment thread
ramackri marked this conversation as resolved.
props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive");

Map<String, String> headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX);

assertTrue(headers.isEmpty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
Expand Down Expand Up @@ -143,6 +145,7 @@ public String getMethod() {
private volatile Client cookieAuthClient;
private JwtProvider jwtProvider;
private volatile String authHeader;
private volatile Map<String, String> trustedAuthHeaders = Collections.emptyMap();

public RangerRESTClient(String url, String sslConfigFileName, Configuration config) {
this(url, sslConfigFileName, config, getPropertyPrefix(config));
Expand Down Expand Up @@ -215,6 +218,19 @@ public void setRetryIntervalMs(int retryIntervalMs) {
this.retryIntervalMs = retryIntervalMs;
}

/**
* Trusted HTTP headers for SPIFFE or other header-based auth.
* Applied to every REST request from this client.
*/
public void setTrustedAuthHeaders(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
trustedAuthHeaders = Collections.emptyMap();
} else {
trustedAuthHeaders = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
}
resetClient();
}

public void setBasicAuthInfo(String username, String password) {
setBasicAuthFilter(username, password);
}
Expand Down Expand Up @@ -494,9 +510,17 @@ private Invocation.Builder createInvocationBuilder(int currentIndex, String rela
builder = builder.cookie(sessionId);
}

applyTrustedAuthHeaders(builder);
Comment thread
ramackri marked this conversation as resolved.

return builder;
}

private void applyTrustedAuthHeaders(Invocation.Builder builder) {
for (Map.Entry<String, String> entry : trustedAuthHeaders.entrySet()) {
builder.header(entry.getKey(), entry.getValue());
}
}

private Response performRequest(HttpMethod method, String relativeUrl, Map<String, String> params, Object requestBody, Cookie sessionId) throws Exception {
Response finalResponse = null;
int startIndex = this.lastKnownActiveUrlIndex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,23 @@

package org.apache.ranger.plugin.util;

import com.sun.net.httpserver.HttpServer;
import org.apache.hadoop.conf.Configuration;
import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
import org.apache.ranger.plugin.policyengine.RangerPolicyEngineOptions;
import org.apache.ranger.plugin.service.RangerBasePlugin;
import org.junit.jupiter.api.Test;

import javax.ws.rs.core.Response;

import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand All @@ -33,6 +45,8 @@ public class TestRangerRESTClient {
private static final String SERVICE_NAME = "test-service";
private static final String APP_ID = "test-app";
private static final String ERR_MESSAGE = "Ranger URL is null or empty.";
private static final String VALID_SPIFFE =
"spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/om";

@Test
public void testPluginInit_WithNoUrl_ThrowsException() {
Expand All @@ -50,4 +64,41 @@ public void testPluginInit_WithValidUrl_Succeeds() {
plugin.init();
assertNotNull(plugin, "RangerBasePlugin should be initialized successfully");
}

@Test
public void setTrustedAuthHeadersAddsHeaderToOutboundRequest() throws Exception {
AtomicReference<String> capturedSpiffeHeader = new AtomicReference<>();
HttpServer httpServer = HttpServer.create(new InetSocketAddress(0), 0);

httpServer.createContext("/", exchange -> {
List<String> values = exchange.getRequestHeaders().get("X-Spiffe-Id");

if (values != null && !values.isEmpty()) {
capturedSpiffeHeader.set(values.get(0));
}

exchange.sendResponseHeaders(200, -1);
exchange.close();
});

httpServer.start();

try {
String serverUrl = "http://localhost:" + httpServer.getAddress().getPort();
Configuration conf = new Configuration();
RangerRESTClient client = new RangerRESTClient(serverUrl, null, conf);
Map<String, String> headers = new LinkedHashMap<>();

headers.put("X-Spiffe-Id", VALID_SPIFFE);
client.setTrustedAuthHeaders(headers);

try (Response response = client.get("/test", Collections.emptyMap())) {
assertEquals(200, response.getStatus());
}

assertEquals(VALID_SPIFFE, capturedSpiffeHeader.get());
} finally {
httpServer.stop(0);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* 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.ranger.plugin.util;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

/**
* Outbound trusted-header auth for audit-server and other REST clients.
*
* <p>When {@code authn.header.enabled=true}, trusted HTTP headers are added to every
* outbound REST request. Each header is configured as a property whose name is the
* HTTP header name under {@code authn.header} (audit destination example):
* <pre>
* xasecure.audit.destination.auditserver.authn.header.enabled=true
* xasecure.audit.destination.auditserver.authn.header.X-Spiffe-Id=file:/path/to/spiffe-id.file
* # valid value specs:
* # file:/path/to/spiffe-id.file
* # env:SPIFFE_ID
* # spiffe://trust-domain/ns/.../sa/... (literal string)
* </pre>
*/
public final class PluginHeaderAuthConfig {
private static final Logger LOG = LoggerFactory.getLogger(PluginHeaderAuthConfig.class);

public static final String PROP_HEADER_PREFIX = "authn.header.";
public static final String PROP_HEADER_AUTH_ENABLED = PROP_HEADER_PREFIX + "enabled";

private static final String VALUE_PREFIX_FILE = "file:";
private static final String VALUE_PREFIX_ENV = "env:";

private PluginHeaderAuthConfig() {
// to block instantiation
}

/**
* Returns whether trusted header auth is enabled for the given config prefix.
*
* @param props plugin or site configuration properties
* @param configPrefix property prefix for header-auth settings
* @return {@code true} when header auth is enabled
*/
public static boolean isHeaderAuthEnabled(final Properties props, final String configPrefix) {
return props != null && StringUtils.isNotBlank(configPrefix) && Boolean.parseBoolean(props.getProperty(configPrefix + "." + PROP_HEADER_AUTH_ENABLED, "false"));
}

/**
* Builds trusted HTTP headers for outbound REST calls when header auth is enabled.
*
* @param props plugin or site configuration properties
* @param configPrefix prefix such as {@code xasecure.audit.destination.auditserver}
* @return immutable header map; empty when auth is disabled or misconfigured
*/
public static Map<String, String> buildTrustedAuthHeaders(final Properties props, final String configPrefix) {
final Map<String, String> ret;

if (isHeaderAuthEnabled(props, configPrefix)) {
String propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX;
Map<String, String> headers = new LinkedHashMap<>();

for (String propertyName : props.stringPropertyNames()) {
if (!propertyName.startsWith(propertyPrefix)) {
continue;
}

String headerName = propertyName.substring(propertyPrefix.length());

if (StringUtils.isBlank(headerName) || "enabled".equals(headerName)) {
continue;
}

String headerValue = resolveConfiguredValue(props.getProperty(propertyName));

addConfiguredHeader(headers, configPrefix, headerName, headerValue);
}

if (headers.isEmpty()) {
LOG.warn("Plugin header auth enabled for {} but no trusted headers could be resolved", configPrefix);
}

ret = Collections.unmodifiableMap(headers);
} else {
ret = Collections.emptyMap();
}

return ret;
}

private static void addConfiguredHeader(final Map<String, String> headers, final String configPrefix, final String headerName, final String headerValue) {
if (StringUtils.isBlank(headerValue)) {
LOG.warn("Plugin header auth enabled for {} but trusted header {} has no resolvable value", configPrefix, headerName);
} else {
headers.put(headerName, headerValue);
}
}

private static String resolveConfiguredValue(final String valueSpec) {
final String ret;

if (StringUtils.isBlank(valueSpec)) {
ret = null;
} else if (valueSpec.startsWith(VALUE_PREFIX_FILE)) {
ret = readFirstNonBlankLine(valueSpec.substring(VALUE_PREFIX_FILE.length()));
} else if (valueSpec.startsWith(VALUE_PREFIX_ENV)) {
ret = StringUtils.trimToNull(System.getenv(valueSpec.substring(VALUE_PREFIX_ENV.length())));
} else {
ret = StringUtils.trimToNull(valueSpec);
}

return ret;
}

private static String readFirstNonBlankLine(final String filePath) {
String ret = null;

if (StringUtils.isNotBlank(filePath)) {
try {
Path path = Paths.get(filePath.trim());

if (Files.isRegularFile(path)) {
for (String line : Files.readAllLines(path, StandardCharsets.UTF_8)) {
String trimmed = StringUtils.trimToNull(line);

if (trimmed != null) {
ret = trimmed;

break;
}
}
}
} catch (IOException ex) {
LOG.debug("Unable to read trusted header value from file {}", filePath, ex);
}
}

return ret;
}
}
Loading