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 api token action to handle creation of api token index #4912

Open
wants to merge 3 commits into
base: feature/api-tokens
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public class BackendRegistry {
private Cache<AuthCredentials, User> userCache; // rest standard
private Cache<String, User> restImpersonationCache; // used for rest impersonation
private Cache<User, Set<String>> restRoleCache; //
private Cache<AuthCredentials, User> apiTokensCache;

private void createCaches() {
userCache = CacheBuilder.newBuilder()
Expand Down Expand Up @@ -135,6 +136,12 @@ public void onRemoval(RemovalNotification<User, Set<String>> notification) {
})
.build();

apiTokensCache = CacheBuilder.newBuilder()
.expireAfterWrite(ttlInMin, TimeUnit.MINUTES)
.removalListener((RemovalListener<AuthCredentials, User>) notification -> log.debug("Clear api token cache for {} due to {}", notification.getKey(), notification.getCause()))
.build();


}

public BackendRegistry(
Expand Down Expand Up @@ -170,6 +177,7 @@ public void invalidateCache() {
userCache.invalidateAll();
restImpersonationCache.invalidateAll();
restRoleCache.invalidateAll();
apiTokensCache.invalidateAll();
}

@Subscribe
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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 com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.opensearch.action.admin.indices.create.CreateIndexRequest;
import org.opensearch.action.index.IndexResponse;
import org.opensearch.client.Client;
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.ConfigConstants;
import org.opensearch.security.support.SecurityJsonNode;
import org.opensearch.threadpool.ThreadPool;

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

import static org.opensearch.rest.RestRequest.Method.*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove wildcard import

import static org.opensearch.security.dlic.rest.api.RateLimitersApiAction.NAME_JSON_PROPERTY;
import static org.opensearch.security.dlic.rest.api.Responses.*;
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix;
import static org.opensearch.security.securityconf.impl.v7.ConfigV7.*;

public class ApiTokenApiAction extends AbstractApiAction {

public static final String NAME_JSON_PROPERTY = "ip";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is copied from elsewhere? Can this be removed?



private static final List<Route> ROUTES = addRoutesPrefix(
ImmutableList.of(
new Route(GET, "/apitokens"),
new Route(PUT, "/apitokens/{name}")
// new Route(DELETE, "/apitokens/{name}"),
)
);

protected ApiTokenApiAction(ClusterService clusterService, ThreadPool threadPool, SecurityApiDependencies securityApiDependencies) {
super(Endpoint.APITOKENS, clusterService, threadPool, securityApiDependencies);
this.requestHandlersBuilder.configureRequestHandlers(this::authFailureConfigApiRequestHandlers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the naming also looks copied here. Can this be renamed?

}

@Override
public String getName() {
return "API Token actions to retrieve / update configs.";
}

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

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

private void authFailureConfigApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) {

requestHandlersBuilder.override(
GET,
(channel, request, client) -> loadConfiguration(getConfigType(), false, false).valid(configuration -> {
if (!apiTokenIndexExists()) {
ok(channel, "empty list");
} else {
ok(channel, "non-empty list");
}
}).error((status, toXContent) -> response(channel, status, toXContent)))
.override(PUT, (channel, request, client) -> loadConfiguration(getConfigType(), false, false).valid(configuration -> {
String token = createApiToken(request.param(NAME_JSON_PROPERTY), client);
ok(channel, token + " created successfully");
}).error((status, toXContent) -> response(channel, status, toXContent)));

}

public String createApiToken(String name, Client client) {
createApiTokenIndexIfAbsent(client);

return "test-token";
}

public Boolean apiTokenIndexExists() {
return clusterService.state().metadata().hasConcreteIndex(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX);
}

public void createApiTokenIndexIfAbsent(Client client) {
if (!apiTokenIndexExists()) {
final Map<String, Object> indexSettings = ImmutableMap.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all");
final CreateIndexRequest createIndexRequest = new CreateIndexRequest(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX).settings(indexSettings);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI By subclassing AbstractApiAction this would be authorized like other security APIs. Like other API handlers, I think we will want to go to the transport layer here and execute a transport action and create the index from within there.

Any calls to a system index should be wrapped into threadContext.stashContext to assure that the plugin can perform the action regardless of the authenticated user's permissions.

logger.info(client.admin().indices().create(createIndexRequest).actionGet().isAcknowledged());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum Endpoint {
AUTHTOKEN,
TENANTS,
RATELIMITERS,
APITOKENS,
MIGRATE,
VALIDATE,
WHITELIST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public static Collection<RestHandler> getHandler(
new AuditApiAction(clusterService, threadPool, securityApiDependencies),
new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies),
new RateLimitersApiAction(clusterService, threadPool, securityApiDependencies),
new ApiTokenApiAction(clusterService, threadPool, securityApiDependencies),
new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies),
new SecuritySSLCertsApiAction(
clusterService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ public abstract class DynamicConfigModel {

public abstract Settings getDynamicOnBehalfOfSettings();

public abstract Settings getDynamicApiTokenSettings();



protected final Map<String, String> authImplMap = new HashMap<>();

public DynamicConfigModel() {
Expand Down Expand Up @@ -142,5 +146,4 @@ public DynamicConfigModel() {
authImplMap.put("ip_authFailureListener", AddressBasedRateLimiter.class.getName());
authImplMap.put("username_authFailureListener", UserNameBasedRateLimiter.class.getName());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ public Settings getDynamicOnBehalfOfSettings() {
.build();
}

@Override
public Settings getDynamicApiTokenSettings() {
return Settings.builder()
.put(Settings.builder().loadFromSource(config.dynamic.api_tokens.configAsJson(), XContentType.JSON).build())
.build();
}

private void buildAAA() {

final SortedSet<AuthDomain> restAuthDomains0 = new TreeSet<>();
Expand Down Expand Up @@ -387,6 +394,17 @@ private void buildAAA() {
restAuthDomains0.add(_ad);
}

Settings apiTokenSettings = getDynamicApiTokenSettings();
if (!isKeyNull(apiTokenSettings, "signing_key") && !isKeyNull(apiTokenSettings, "encryption_key")) {
final AuthDomain _ad = new AuthDomain(
new NoOpAuthenticationBackend(Settings.EMPTY, null),
new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings(), this.cih.getClusterName()),
false,
-1
);
restAuthDomains0.add(_ad);
}

List<Destroyable> originalDestroyableComponents = destroyableComponents;

restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public static class Dynamic {
public String transport_userrname_attribute;
public boolean do_not_fail_on_forbidden_empty;
public OnBehalfOfSettings on_behalf_of = new OnBehalfOfSettings();
public ApiTokenSettings api_tokens = new ApiTokenSettings();

@Override
public String toString() {
Expand Down Expand Up @@ -495,4 +496,51 @@ public String toString() {
}
}

public static class ApiTokenSettings {
@JsonProperty("enabled")
private Boolean apiTokenEnabled = Boolean.FALSE;
@JsonProperty("signing_key")
private String signingKey;
@JsonProperty("encryption_key")
private String encryptionKey;

@JsonIgnore
public String configAsJson() {
try {
return DefaultObjectMapper.writeValueAsString(this, false);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

public Boolean getApiTokenEnabled() {
return apiTokenEnabled;
}

public void setOboEnabled(Boolean apiTokenEnabled) {
this.apiTokenEnabled = apiTokenEnabled;
}

public String getSigningKey() {
return signingKey;
}

public void setSigningKey(String signingKey) {
this.signingKey = signingKey;
}

public String getEncryptionKey() {
return encryptionKey;
}

public void setEncryptionKey(String encryptionKey) {
this.encryptionKey = encryptionKey;
}

@Override
public String toString() {
return "ApiTokenSettings [ enabled=" + apiTokenEnabled + ", signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]";
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ public enum RolesMappingResolution {
// Variable for initial admin password support
public static final String OPENSEARCH_INITIAL_ADMIN_PASSWORD = "OPENSEARCH_INITIAL_ADMIN_PASSWORD";

public static final String OPENSEARCH_API_TOKENS_INDEX = ".opensearch_security_api_tokens";

public static Set<String> getSettingAsSet(
final Settings settings,
final String key,
Expand Down
Loading