Skip to content

Commit

Permalink
[Backport 1.3] Add early rejection from RestHandler for unauthorized …
Browse files Browse the repository at this point in the history
…requests (opensearch-project#3418) (opensearch-project#3495)

Backport of 6b0b682 from opensearch-project#3418

Previously unauthorized requests were fully processed and rejected once
they reached the RestHandler. This allocations more memory and resources
for these requests that might not be useful if they are already detected
as unauthorized. Using the headerVerifer and decompressor customization
from [1], perform an early authorization check when only the headers are
available, save an 'early response' for transmission and do not perform
the decompression on the request to speed up closing out the connection.

- Resolves opensearch-project/OpenSearch#10260

Signed-off-by: Peter Nied <petern@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <craig5008@gmail.com>
Co-authored-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Peter Nied <petern@amazon.com>
  • Loading branch information
peternied and cwperks committed Nov 9, 2023
1 parent 114d7a8 commit f12a66b
Show file tree
Hide file tree
Showing 24 changed files with 752 additions and 243 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ private Optional<SecurityResponse> handleLowLevel(RestRequest restRequest) throw
return Optional.of(new SecurityResponse(HttpStatus.SC_OK, SecurityResponse.CONTENT_TYPE_APP_JSON, responseBodyString));
} catch (JsonProcessingException e) {
log.warn("Error while parsing JSON for /_opendistro/_security/api/authtoken", e);
return Optional.of(new SecurityResponse(HttpStatus.SC_BAD_REQUEST, null, "JSON could not be parsed"));
return Optional.of(new SecurityResponse(HttpStatus.SC_BAD_REQUEST, new Exception("JSON could not be parsed")));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class HTTPSamlAuthenticator implements HTTPAuthenticator, Destroyable {
public static final String IDP_METADATA_FILE = "idp.metadata_file";
public static final String IDP_METADATA_CONTENT = "idp.metadata_content";

private static final String API_AUTHTOKEN_SUFFIX = "api/authtoken";
public static final String API_AUTHTOKEN_SUFFIX = "api/authtoken";
private static final String AUTHINFO_SUFFIX = "authinfo";
private static final String REGEX_PATH_PREFIX = "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/" +"(.*)";
private static final Pattern PATTERN_PATH_PREFIX = Pattern.compile(REGEX_PATH_PREFIX);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
public static final String PLUGINS_PREFIX = "_plugins/_security";

private boolean sslCertReloadEnabled;
private volatile SecurityRestFilter securityRestHandler;
private volatile SecurityInterceptor si;
private volatile PrivilegesEvaluator evaluator;
private volatile ThreadPool threadPool;
Expand Down Expand Up @@ -722,13 +721,13 @@ public Map<String, Supplier<HttpServerTransport>> getHttpTransports(Settings set
settings, configPath, evaluateSslExceptionHandler());
//TODO close odshst
final SecurityHttpServerTransport odshst = new SecurityHttpServerTransport(settings, networkService, bigArrays,
threadPool, sks, evaluateSslExceptionHandler(), xContentRegistry, validatingDispatcher, clusterSettings, sharedGroupFactory);
threadPool, sks, evaluateSslExceptionHandler(), xContentRegistry, validatingDispatcher, clusterSettings, sharedGroupFactory, securityRestHandler);

return Collections.singletonMap("org.opensearch.security.http.SecurityHttpServerTransport",
() -> odshst);
} else if (!client) {
return Collections.singletonMap("org.opensearch.security.http.SecurityHttpServerTransport",
() -> new SecurityNonSslHttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings, sharedGroupFactory));
() -> new SecurityNonSslHttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings, sharedGroupFactory, securityRestHandler));
}
}
return Collections.emptyMap();
Expand Down
18 changes: 11 additions & 7 deletions src/main/java/org/opensearch/security/auth/BackendRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import org.opensearch.common.settings.Settings;
import org.opensearch.common.transport.TransportAddress;
import org.opensearch.rest.RestStatus;
import org.opensearch.common.util.concurrent.ThreadContext;
import org.opensearch.security.auditlog.AuditLog;
import org.opensearch.security.auth.blocking.ClientBlockRegistry;
import org.opensearch.security.auth.internal.NoOpAuthenticationBackend;
Expand Down Expand Up @@ -187,6 +188,8 @@ public BackendRegistry(final Settings settings, final AdminDNs adminDns,
this.auditLog = auditLog;
this.threadPool = threadPool;
this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver);
this.restAuthDomains = Collections.emptySortedSet();
this.ipAuthFailureListeners = Collections.emptyList();


this.ttlInMin = settings.getAsInt(ConfigConstants.SECURITY_CACHE_TTL_MINUTES, 60);
Expand Down Expand Up @@ -353,7 +356,6 @@ public User authenticate(final TransportRequest request, final String sslPrincip
/**
*
* @param request
* @param channel
* @return The authenticated user, null means another roundtrip
* @throws OpenSearchSecurityException
*/
Expand All @@ -368,15 +370,17 @@ public boolean authenticate(final SecurityRequestChannel request) {
log.debug("Rejecting REST request because of blocked address: {}", request.getRemoteAddress().orElse(null));
}

request.queueForSending(new SecurityResponse(SC_UNAUTHORIZED, null, "Authentication finally failed"));
request.queueForSending(new SecurityResponse(SC_UNAUTHORIZED, new Exception("Authentication finally failed")));
return false;
}

final String sslPrincipal = (String) threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL);
ThreadContext threadContext = this.threadPool.getThreadContext();

if(adminDns.isAdminDN(sslPrincipal)) {
//PKI authenticated REST call
threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User(sslPrincipal));
final String sslPrincipal = (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL);

if (adminDns.isAdminDN(sslPrincipal)) {
// PKI authenticated REST call
threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User(sslPrincipal));
auditLog.logSucceededLogin(sslPrincipal, true, null, request);
return true;
}
Expand All @@ -388,7 +392,7 @@ public boolean authenticate(final SecurityRequestChannel request) {

if (!isInitialized()) {
log.error("Not yet initialized (you may need to run securityadmin)");
request.queueForSending(new SecurityResponse(SC_SERVICE_UNAVAILABLE, null, "OpenSearch Security not initialized."));
request.queueForSending(new SecurityResponse(SC_SERVICE_UNAVAILABLE, new Exception("OpenSearch Security not initialized.")));
return false;
}

Expand Down
49 changes: 49 additions & 0 deletions src/main/java/org/opensearch/security/filter/NettyAttribute.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.opensearch.security.filter;

import java.util.Optional;

import org.opensearch.http.netty4.Netty4HttpChannel;
import org.opensearch.rest.RestRequest;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AttributeKey;

public class NettyAttribute {

/**
* Gets an attribute value from the request context and clears it from that context
*/
public static <T> Optional<T> popFrom(final RestRequest request, final AttributeKey<T> attribute) {
if (request.getHttpChannel() instanceof Netty4HttpChannel) {
Channel nettyChannel = ((Netty4HttpChannel) request.getHttpChannel()).getNettyChannel();
return Optional.ofNullable(nettyChannel.attr(attribute).getAndSet(null));
}
return Optional.empty();
}

/**
* Gets an attribute value from the channel handler context and clears it from that context
*/
public static <T> Optional<T> popFrom(final ChannelHandlerContext ctx, final AttributeKey<T> attribute) {
return Optional.ofNullable(ctx.channel().attr(attribute).getAndSet(null));
}

/**
* Gets an attribute value from the channel handler context
*/
public static <T> Optional<T> peekFrom(final ChannelHandlerContext ctx, final AttributeKey<T> attribute) {
return Optional.ofNullable(ctx.channel().attr(attribute).get());
}

/**
* Clears an attribute value from the channel handler context
*/
public static <T> void clearAttribute(final RestRequest request, final AttributeKey<T> attribute) {
if (request.getHttpChannel() instanceof Netty4HttpChannel) {
Channel nettyChannel = ((Netty4HttpChannel) request.getHttpChannel()).getNettyChannel();
nettyChannel.attr(attribute).set(null);
}
}

}
100 changes: 100 additions & 0 deletions src/main/java/org/opensearch/security/filter/NettyRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.filter;

import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

import javax.net.ssl.SSLEngine;

import io.netty.handler.ssl.SslHandler;
import org.opensearch.http.netty4.Netty4HttpChannel;
import org.opensearch.rest.RestRequest.Method;

import io.netty.handler.codec.http.HttpRequest;
import org.opensearch.rest.RestUtils;

/**
* Wraps the functionality of HttpRequest for use in the security plugin
*/
public class NettyRequest implements SecurityRequest {

protected final HttpRequest underlyingRequest;
protected final Netty4HttpChannel underlyingChannel;

NettyRequest(final HttpRequest request, final Netty4HttpChannel channel) {
this.underlyingRequest = request;
this.underlyingChannel = channel;
}

@Override
public Map<String, List<String>> getHeaders() {
final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
underlyingRequest.headers().forEach(h -> headers.put(h.getKey(), List.of(h.getValue())));
return headers;
}

@Override
public SSLEngine getSSLEngine() {
// We look for Ssl_handler called `ssl_http` in the outbound pipeline of Netty channel first, and if its not
// present we look for it in inbound channel. If its present in neither we return null, else we return the sslHandler.
SslHandler sslhandler = (SslHandler) underlyingChannel.getNettyChannel().pipeline().get("ssl_http");
return sslhandler != null ? sslhandler.engine() : null;
}

@Override
public String path() {
String rawPath = SecurityRestUtils.path(underlyingRequest.uri());
return RestUtils.decodeComponent(rawPath);
}

@Override
public Method method() {
return Method.valueOf(underlyingRequest.method().name());
}

@Override
public Optional<InetSocketAddress> getRemoteAddress() {
return Optional.ofNullable(this.underlyingChannel.getRemoteAddress());
}

@Override
public String uri() {
return underlyingRequest.uri();
}

@Override
public Map<String, String> params() {
return params(underlyingRequest.uri());
}

private static Map<String, String> params(String uri) {
// Sourced from
// https://github.com/opensearch-project/OpenSearch/blob/main/server/src/main/java/org/opensearch/http/AbstractHttpServerTransport.java#L419-L422
final Map<String, String> params = new HashMap<>();
final int index = uri.indexOf(63);
if (index >= 0) {
try {
RestUtils.decodeQueryString(uri, index + 1, params);
} catch (IllegalArgumentException var4) {
return Collections.emptyMap();
}
}

return params;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.filter;

import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import io.netty.handler.codec.http.HttpRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.opensearch.http.netty4.Netty4HttpChannel;

public class NettyRequestChannel extends NettyRequest implements SecurityRequestChannel {
private final Logger log = LogManager.getLogger(NettyRequestChannel.class);

private AtomicBoolean hasCompleted = new AtomicBoolean(false);
private final AtomicReference<SecurityResponse> responseRef = new AtomicReference<SecurityResponse>(null);

NettyRequestChannel(final HttpRequest request, Netty4HttpChannel channel) {
super(request, channel);
}

@Override
public void queueForSending(SecurityResponse response) {
if (underlyingChannel == null) {
throw new UnsupportedOperationException("Channel was not defined");
}

if (hasCompleted.get()) {
throw new UnsupportedOperationException("This channel has already completed");
}

if (getQueuedResponse().isPresent()) {
throw new UnsupportedOperationException("Another response was already queued");
}

responseRef.set(response);
}

@Override
public Optional<SecurityResponse> getQueuedResponse() {
return Optional.ofNullable(responseRef.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,14 @@
package org.opensearch.security.filter;

import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.opensearch.rest.RestStatus;
import org.opensearch.rest.BytesRestResponse;
import org.opensearch.rest.RestChannel;
import org.opensearch.rest.RestRequest;

public class OpenSearchRequestChannel extends OpenSearchRequest implements SecurityRequestChannel {

private final Logger log = LogManager.getLogger(OpenSearchRequest.class);

private final AtomicReference<SecurityResponse> responseRef = new AtomicReference<SecurityResponse>(null);
private final AtomicBoolean hasCompleted = new AtomicBoolean(false);
private final RestChannel underlyingChannel;

OpenSearchRequestChannel(final RestRequest request, final RestChannel channel) {
Expand All @@ -46,10 +38,6 @@ public void queueForSending(final SecurityResponse response) {
throw new UnsupportedOperationException("Channel was not defined");
}

if (hasCompleted.get()) {
throw new UnsupportedOperationException("This channel has already completed");
}

if (getQueuedResponse().isPresent()) {
throw new UnsupportedOperationException("Another response was already queued");
}
Expand All @@ -61,37 +49,4 @@ public void queueForSending(final SecurityResponse response) {
public Optional<SecurityResponse> getQueuedResponse() {
return Optional.ofNullable(responseRef.get());
}

@Override
public boolean sendResponse() {
if (underlyingChannel == null) {
throw new UnsupportedOperationException("Channel was not defined");
}

if (hasCompleted.get()) {
throw new UnsupportedOperationException("This channel has already completed");
}

if (!getQueuedResponse().isPresent()) {
throw new UnsupportedOperationException("No response has been associated with this channel");
}

final SecurityResponse response = responseRef.get();

try {
final BytesRestResponse restResponse = new BytesRestResponse(RestStatus.fromCode(response.getStatus()), response.getBody());
if (response.getHeaders() != null) {
response.getHeaders().forEach(restResponse::addHeader);
}
underlyingChannel.sendResponse(restResponse);

return true;
} catch (final Exception e) {
log.error("Error when attempting to send response", e);
throw new RuntimeException(e);
} finally {
hasCompleted.set(true);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@
public interface SecurityRequestChannel extends SecurityRequest {

/** Associate a response with this channel */
public void queueForSending(final SecurityResponse response);
void queueForSending(final SecurityResponse response);

/** Acess the queued response */
public Optional<SecurityResponse> getQueuedResponse();

/** Send the response through the channel */
public boolean sendResponse();
Optional<SecurityResponse> getQueuedResponse();
}
Loading

0 comments on commit f12a66b

Please sign in to comment.