Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial changes to expose an endpoint for auth failure listener get call #4641

Merged
merged 13 commits into from
Sep 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
import org.opensearch.http.HttpServerTransport;
import org.opensearch.http.HttpServerTransport.Dispatcher;
import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport;
import org.opensearch.identity.PluginSubject;
import org.opensearch.identity.Subject;
import org.opensearch.identity.noop.NoopSubject;
import org.opensearch.index.IndexModule;
Expand All @@ -119,6 +120,7 @@
import org.opensearch.plugins.ExtensionAwarePlugin;
import org.opensearch.plugins.IdentityPlugin;
import org.opensearch.plugins.MapperPlugin;
import org.opensearch.plugins.Plugin;
import org.opensearch.plugins.SecureHttpTransportSettingsProvider;
import org.opensearch.plugins.SecureSettingsFactory;
import org.opensearch.plugins.SecureTransportSettingsProvider;
Expand Down Expand Up @@ -164,6 +166,7 @@
import org.opensearch.security.hasher.PasswordHasherFactory;
import org.opensearch.security.http.NonSslHttpServerTransport;
import org.opensearch.security.http.XFFResolver;
import org.opensearch.security.identity.NoopPluginSubject;
import org.opensearch.security.identity.SecurityTokenManager;
import org.opensearch.security.privileges.PrivilegesEvaluator;
import org.opensearch.security.privileges.PrivilegesInterceptor;
Expand Down Expand Up @@ -2102,7 +2105,7 @@ private static String handleKeyword(final String field) {
}

@Override
public Subject getSubject() {
public Subject getCurrentSubject() {
// Not supported
return new NoopSubject();
}
Expand All @@ -2112,6 +2115,11 @@ public SecurityTokenManager getTokenManager() {
return tokenManager;
}

@Override
public PluginSubject getPluginSubject(Plugin plugin) {
return new NoopPluginSubject(threadPool);
}

@Override
public Optional<SecureSettingsFactory> getSecureSettingFactory(Settings settings) {
return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, sslExceptionHandler, securityRestHandler));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/*
* 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.dlic.rest.api;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.fasterxml.jackson.databind.node.ObjectNode;

import org.opensearch.action.index.IndexResponse;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.settings.Settings;
import org.opensearch.core.rest.RestStatus;
import org.opensearch.core.xcontent.ToXContent;
import org.opensearch.security.DefaultObjectMapper;
import org.opensearch.security.dlic.rest.validation.EndpointValidator;
import org.opensearch.security.dlic.rest.validation.RequestContentValidator;
import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType;
import org.opensearch.security.dlic.rest.validation.ValidationResult;
import org.opensearch.security.securityconf.impl.CType;
import org.opensearch.security.securityconf.impl.v7.ConfigV7;
import org.opensearch.security.support.SecurityJsonNode;
import org.opensearch.threadpool.ThreadPool;

import static org.opensearch.rest.RestRequest.Method.DELETE;
import static org.opensearch.rest.RestRequest.Method.GET;
import static org.opensearch.rest.RestRequest.Method.PUT;
import static org.opensearch.security.dlic.rest.api.Responses.badRequest;
import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage;
import static org.opensearch.security.dlic.rest.api.Responses.notFound;
import static org.opensearch.security.dlic.rest.api.Responses.ok;
import static org.opensearch.security.dlic.rest.api.Responses.response;
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix;

public class AuthFailureListenersApiAction extends AbstractApiAction {

public static final String IP_TYPE = "ip";

public static final String USERNAME_TYPE = "username";

public static final String NAME_JSON_PROPERTY = "name";

public static final String TYPE_JSON_PROPERTY = "type";
public static final String IGNORE_HOSTS_JSON_PROPERTY = "ignore_hosts";
public static final String AUTHENTICATION_BACKEND_JSON_PROPERTY = "authentication_backend";
public static final String ALLOWED_TRIES_JSON_PROPERTY = "allowed_tries";
public static final String TIME_WINDOW_SECONDS_JSON_PROPERTY = "time_window_seconds";
public static final String BLOCK_EXPIRY_JSON_PROPERTY = "block_expiry_seconds";
public static final String MAX_BLOCKED_CLIENTS_JSON_PROPERTY = "max_blocked_clients";
public static final String MAX_TRACKED_CLIENTS_JSON_PROPERTY = "max_tracked_clients";

private static final List<Route> ROUTES = addRoutesPrefix(
ImmutableList.of(
new Route(GET, "/authfailurelisteners"),
cwperks marked this conversation as resolved.
Show resolved Hide resolved
new Route(DELETE, "/authfailurelisteners/{name}"),
new Route(PUT, "/authfailurelisteners/{name}")
cwperks marked this conversation as resolved.
Show resolved Hide resolved
)
);

protected AuthFailureListenersApiAction(
ClusterService clusterService,
ThreadPool threadPool,
SecurityApiDependencies securityApiDependencies
) {
super(Endpoint.AUTHFAILURELISTENERS, clusterService, threadPool, securityApiDependencies);
this.requestHandlersBuilder.configureRequestHandlers(this::authFailureConfigApiRequestHandlers);
}

@Override
public String getName() {
return "Auth failure listener actions to Retrieve / Update configs.";
}

@Override
public List<Route> routes() {
return ROUTES;
}

@Override
protected CType getConfigType() {
return CType.CONFIG;
}

@Override
protected EndpointValidator createEndpointValidator() {
return new EndpointValidator() {

@Override
public Endpoint endpoint() {
return endpoint;
}

@Override
public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() {
return securityApiDependencies.restApiAdminPrivilegesEvaluator();
}

@Override
public RequestContentValidator createRequestContentValidator(Object... params) {
return RequestContentValidator.of(new RequestContentValidator.ValidationContext() {
@Override
public Object[] params() {
return params;
}

@Override
public Settings settings() {
return securityApiDependencies.settings();
}

@Override
public Map<String, DataType> allowedKeys() {
final ImmutableMap.Builder<String, DataType> allowedKeys = ImmutableMap.builder();

return allowedKeys.put(TYPE_JSON_PROPERTY, DataType.STRING)
cwperks marked this conversation as resolved.
Show resolved Hide resolved
.put(IGNORE_HOSTS_JSON_PROPERTY, DataType.ARRAY)
.put(AUTHENTICATION_BACKEND_JSON_PROPERTY, DataType.STRING)
.put(ALLOWED_TRIES_JSON_PROPERTY, DataType.INTEGER)
.put(TIME_WINDOW_SECONDS_JSON_PROPERTY, DataType.INTEGER)
.put(BLOCK_EXPIRY_JSON_PROPERTY, DataType.INTEGER)
.put(MAX_BLOCKED_CLIENTS_JSON_PROPERTY, DataType.INTEGER)
.put(MAX_TRACKED_CLIENTS_JSON_PROPERTY, DataType.INTEGER)
.build();
}
});
}
};
}

private ToXContent authFailureContent(final ConfigV7 config) {
return (builder, params) -> {
builder.startObject();
for (String name : config.dynamic.auth_failure_listeners.getListeners().keySet()) {
ConfigV7.AuthFailureListener listener = config.dynamic.auth_failure_listeners.getListeners().get(name);
builder.startObject(name);
builder.field(NAME_JSON_PROPERTY, name)
.field(TYPE_JSON_PROPERTY, listener.type)
.field(IGNORE_HOSTS_JSON_PROPERTY, listener.ignore_hosts)
.field(AUTHENTICATION_BACKEND_JSON_PROPERTY, listener.authentication_backend)
.field(ALLOWED_TRIES_JSON_PROPERTY, listener.allowed_tries)
.field(TIME_WINDOW_SECONDS_JSON_PROPERTY, listener.time_window_seconds)
.field(BLOCK_EXPIRY_JSON_PROPERTY, listener.block_expiry_seconds)
.field(MAX_BLOCKED_CLIENTS_JSON_PROPERTY, listener.max_blocked_clients)
.field(MAX_TRACKED_CLIENTS_JSON_PROPERTY, listener.max_tracked_clients);
builder.endObject();
}
builder.endObject();
return builder;
};
}

private void authFailureConfigApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) {

requestHandlersBuilder.override(
GET,
(channel, request, client) -> loadConfiguration(getConfigType(), false, false).valid(configuration -> {
final var config = (ConfigV7) configuration.getCEntry(CType.CONFIG.toLCString());
ok(channel, authFailureContent(config));
}).error((status, toXContent) -> response(channel, status, toXContent))
).override(DELETE, (channel, request, client) -> loadConfiguration(getConfigType(), false, false).valid(configuration -> {
ConfigV7 config = (ConfigV7) configuration.getCEntry(CType.CONFIG.toLCString());

String listenerName = request.param("name");
cwperks marked this conversation as resolved.
Show resolved Hide resolved

// Try to remove the listener by name
if (config.dynamic.auth_failure_listeners.getListeners().remove(listenerName) == null) {
cwperks marked this conversation as resolved.
Show resolved Hide resolved
notFound(channel, "listener not found");
}
saveOrUpdateConfiguration(client, configuration, new OnSucessActionListener<>(channel) {
@Override
public void onResponse(IndexResponse indexResponse) {
ok(channel, authFailureContent(config));
}
});
}).error((status, toXContent) -> response(channel, status, toXContent)))
.override(PUT, (channel, request, client) -> loadConfiguration(getConfigType(), false, false).valid(configuration -> {
ConfigV7 config = (ConfigV7) configuration.getCEntry(CType.CONFIG.toLCString());

String listenerName = request.param(NAME_JSON_PROPERTY);

ObjectNode body = (ObjectNode) DefaultObjectMapper.readTree(request.content().utf8ToString());
SecurityJsonNode authFailureListener = new SecurityJsonNode(body);
ValidationResult<SecurityJsonNode> validationResult = validateAuthFailureListener(authFailureListener, listenerName);

if (!validationResult.isValid()) {
badRequest(channel, validationResult.toString());
return;
}
String authenticationBackend = authFailureListener.get(TYPE_JSON_PROPERTY).asString().equals(IP_TYPE)
cwperks marked this conversation as resolved.
Show resolved Hide resolved
|| authFailureListener.get(AUTHENTICATION_BACKEND_JSON_PROPERTY).isNull()
? null
: authFailureListener.get(AUTHENTICATION_BACKEND_JSON_PROPERTY).asString();
List<String> ignoreHosts = authFailureListener.get(IGNORE_HOSTS_JSON_PROPERTY).isNull()
? Collections.emptyList()
: authFailureListener.get(IGNORE_HOSTS_JSON_PROPERTY).asList();

// Try to put the listener by name
config.dynamic.auth_failure_listeners.getListeners()
.put(
listenerName,
new ConfigV7.AuthFailureListener(
authFailureListener.get(TYPE_JSON_PROPERTY).asString(),
Optional.ofNullable(authenticationBackend),
ignoreHosts,
authFailureListener.get(ALLOWED_TRIES_JSON_PROPERTY).asInt(),
authFailureListener.get(TIME_WINDOW_SECONDS_JSON_PROPERTY).asInt(),
authFailureListener.get(BLOCK_EXPIRY_JSON_PROPERTY).asInt(),
authFailureListener.get(MAX_BLOCKED_CLIENTS_JSON_PROPERTY).asInt(),
authFailureListener.get(MAX_TRACKED_CLIENTS_JSON_PROPERTY).asInt()
)
);
saveOrUpdateConfiguration(client, configuration, new OnSucessActionListener<>(channel) {
@Override
public void onResponse(IndexResponse indexResponse) {

ok(channel, authFailureContent(config));
}
});
}).error((status, toXContent) -> response(channel, status, toXContent)));

}

private ValidationResult<SecurityJsonNode> validateAuthFailureListener(SecurityJsonNode authFailureListener, String name) {
if (name == null) {
return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("name is required"));
}
if (authFailureListener.get(TYPE_JSON_PROPERTY).isNull()) {
return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("type is required"));
}
if (!(authFailureListener.get(TYPE_JSON_PROPERTY).asString().equals(IP_TYPE)
cwperks marked this conversation as resolved.
Show resolved Hide resolved
|| authFailureListener.get(TYPE_JSON_PROPERTY).asString().equals(USERNAME_TYPE))) {
return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("type must be username or ip"));
}
if (authFailureListener.get(TYPE_JSON_PROPERTY).asString().equals(USERNAME_TYPE)
&& (authFailureListener.get(AUTHENTICATION_BACKEND_JSON_PROPERTY).isNull()
|| !authFailureListener.get(AUTHENTICATION_BACKEND_JSON_PROPERTY).asString().equals("internal"))) {
return ValidationResult.error(
RestStatus.BAD_REQUEST,
badRequestMessage("ip auth failure listeners must have 'internal' authentication backend")
);
}
if (authFailureListener.get(TYPE_JSON_PROPERTY).asString().equals(IP_TYPE)
&& !authFailureListener.get(AUTHENTICATION_BACKEND_JSON_PROPERTY).isNull()) {
return ValidationResult.error(
RestStatus.BAD_REQUEST,
badRequestMessage("username auth failure listeners should not have an authentication backend")
cwperks marked this conversation as resolved.
Show resolved Hide resolved
);
}

return ValidationResult.success(authFailureListener);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public enum Endpoint {
PERMISSIONSINFO,
AUTHTOKEN,
TENANTS,
AUTHFAILURELISTENERS,
MIGRATE,
VALIDATE,
WHITELIST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public static Collection<RestHandler> getHandler(
new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies),
new AuditApiAction(clusterService, threadPool, securityApiDependencies),
new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies),
new AuthFailureListenersApiAction(clusterService, threadPool, securityApiDependencies),
new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies),
new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies),
new CertificatesApiAction(clusterService, threadPool, securityApiDependencies)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public static enum DataType {
STRING,
ARRAY,
OBJECT,
INTEGER,
BOOLEAN;
}

Expand Down Expand Up @@ -179,6 +180,7 @@ protected ValidationResult<JsonNode> validateJsonKeys(final JsonNode jsonContent
final Set<String> allowed = new HashSet<>(validationContext.allowedKeys().keySet());
requestedKeys.removeAll(allowed);
invalidKeys.addAll(requestedKeys);

if (!missingMandatoryKeys.isEmpty() || !invalidKeys.isEmpty() || !missingMandatoryOrKeys.isEmpty()) {
this.validationError = ValidationError.INVALID_CONFIGURATION;
return ValidationResult.error(RestStatus.BAD_REQUEST, this);
Expand All @@ -196,6 +198,11 @@ private ValidationResult<JsonNode> validateDataType(final JsonNode jsonContent)
if (dataType != null) {
JsonToken valueToken = parser.nextToken();
switch (dataType) {
case INTEGER:
if (valueToken != JsonToken.VALUE_NUMBER_INT) {
wrongDataTypes.put(currentName, "Integer expected");
}
break;
case STRING:
if (valueToken != JsonToken.VALUE_STRING) {
wrongDataTypes.put(currentName, "String expected");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.identity;

import java.security.Principal;
import java.util.concurrent.Callable;

import org.opensearch.common.util.concurrent.ThreadContext;
import org.opensearch.identity.NamedPrincipal;
import org.opensearch.identity.PluginSubject;
import org.opensearch.threadpool.ThreadPool;

public class NoopPluginSubject implements PluginSubject {
private final ThreadPool threadPool;

public NoopPluginSubject(ThreadPool threadPool) {
super();
this.threadPool = threadPool;
}

@Override
public Principal getPrincipal() {
return NamedPrincipal.UNAUTHENTICATED;
}

@Override
public <T> T runAs(Callable<T> callable) throws Exception {
try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) {
return callable.call();
}
}
}
Loading
Loading