Skip to content

Commit

Permalink
Add initial changes to expose an endpoint for auth failure listener g…
Browse files Browse the repository at this point in the history
…et call (opensearch-project#4641)

Signed-off-by: Derek Ho <[email protected]>
Signed-off-by: Craig Perkins <[email protected]>
Co-authored-by: Craig Perkins <[email protected]>
  • Loading branch information
derek-ho and cwperks authored Sep 5, 2024
1 parent 4f2e689 commit d2daa98
Show file tree
Hide file tree
Showing 9 changed files with 608 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/*
* 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.Set;

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;
import static org.opensearch.security.securityconf.impl.v7.ConfigV7.ALLOWED_TRIES_DEFAULT;
import static org.opensearch.security.securityconf.impl.v7.ConfigV7.BLOCK_EXPIRY_SECONDS_DEFAULT;
import static org.opensearch.security.securityconf.impl.v7.ConfigV7.MAX_BLOCKED_CLIENTS_DEFAULT;
import static org.opensearch.security.securityconf.impl.v7.ConfigV7.MAX_TRACKED_CLIENTS_DEFAULT;
import static org.opensearch.security.securityconf.impl.v7.ConfigV7.TIME_WINDOW_SECONDS_DEFAULT;

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"),
new Route(DELETE, "/authfailurelisteners/{name}"),
new Route(PUT, "/authfailurelisteners/{name}")
)
);

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)
.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_JSON_PROPERTY);

// Try to remove the listener by name
if (config.dynamic.auth_failure_listeners.getListeners().remove(listenerName) == null) {
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;
}

// Try to put the listener by name
config.dynamic.auth_failure_listeners.getListeners()
.put(listenerName, createAuthFailureListenerWithDefaults(authFailureListener));
saveOrUpdateConfiguration(client, configuration, new OnSucessActionListener<>(channel) {
@Override
public void onResponse(IndexResponse indexResponse) {

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

}

private ConfigV7.AuthFailureListener createAuthFailureListenerWithDefaults(SecurityJsonNode authFailureListener) {
List<String> ignoreHosts = authFailureListener.get(IGNORE_HOSTS_JSON_PROPERTY).isNull()
? Collections.emptyList()
: authFailureListener.get(IGNORE_HOSTS_JSON_PROPERTY).asList();

return new ConfigV7.AuthFailureListener(
authFailureListener.get(TYPE_JSON_PROPERTY).asString(),
authFailureListener.get(AUTHENTICATION_BACKEND_JSON_PROPERTY).asString(),
ignoreHosts,
authFailureListener.get(ALLOWED_TRIES_JSON_PROPERTY).asInt(ALLOWED_TRIES_DEFAULT),
authFailureListener.get(TIME_WINDOW_SECONDS_JSON_PROPERTY).asInt(TIME_WINDOW_SECONDS_DEFAULT),
authFailureListener.get(BLOCK_EXPIRY_JSON_PROPERTY).asInt(BLOCK_EXPIRY_SECONDS_DEFAULT),
authFailureListener.get(MAX_BLOCKED_CLIENTS_JSON_PROPERTY).asInt(MAX_BLOCKED_CLIENTS_DEFAULT),
authFailureListener.get(MAX_TRACKED_CLIENTS_JSON_PROPERTY).asInt(MAX_TRACKED_CLIENTS_DEFAULT)
);

}

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 (!(Set.of(IP_TYPE, USERNAME_TYPE).contains(authFailureListener.get(TYPE_JSON_PROPERTY).asString()))) {
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("username 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("ip auth failure listeners should not have an authentication backend")
);
}

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
Expand Up @@ -52,6 +52,12 @@

public class ConfigV7 {

public static int ALLOWED_TRIES_DEFAULT = 10;
public static int TIME_WINDOW_SECONDS_DEFAULT = 60 * 60;
public static int BLOCK_EXPIRY_SECONDS_DEFAULT = 60 * 10;
public static int MAX_BLOCKED_CLIENTS_DEFAULT = 100_000;
public static int MAX_TRACKED_CLIENTS_DEFAULT = 100_000;

public Dynamic dynamic;

public ConfigV7() {
Expand Down Expand Up @@ -227,11 +233,11 @@ public static class AuthFailureListener {
public String type;
public String authentication_backend;
public List<String> ignore_hosts;
public int allowed_tries = 10;
public int time_window_seconds = 60 * 60;
public int block_expiry_seconds = 60 * 10;
public int max_blocked_clients = 100_000;
public int max_tracked_clients = 100_000;
public int allowed_tries = ALLOWED_TRIES_DEFAULT;
public int time_window_seconds = TIME_WINDOW_SECONDS_DEFAULT;
public int block_expiry_seconds = BLOCK_EXPIRY_SECONDS_DEFAULT;
public int max_blocked_clients = MAX_BLOCKED_CLIENTS_DEFAULT;
public int max_tracked_clients = MAX_TRACKED_CLIENTS_DEFAULT;

public AuthFailureListener() {
super();
Expand All @@ -248,6 +254,26 @@ public AuthFailureListener(ConfigV6.AuthFailureListener v6) {
this.max_tracked_clients = v6.max_tracked_clients;
}

public AuthFailureListener(
String type,
String authentication_backend,
List<String> ignore_hosts,
int allowed_tries,
int time_window_seconds,
int block_expiry_seconds,
int max_blocked_clients,
int max_tracked_clients
) {
this.type = type;
this.authentication_backend = authentication_backend;
this.ignore_hosts = ignore_hosts;
this.allowed_tries = allowed_tries;
this.time_window_seconds = time_window_seconds;
this.block_expiry_seconds = block_expiry_seconds;
this.max_blocked_clients = max_blocked_clients;
this.max_tracked_clients = max_tracked_clients;
}

@JsonIgnore
public String asJson() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ public String asString() {
}
}

public Integer asInt(Integer defaultValue) {
if (isNull(node)) {
return defaultValue;
} else {
return node.asInt(0);
}
}

private static boolean isNull(JsonNode node) {
return node == null || node.isNull();
}
Expand Down
Loading

0 comments on commit d2daa98

Please sign in to comment.