diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 0b00bcf943..52a3e44276 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -103,6 +103,7 @@ public class BackendRegistry { private Cache userCache; // rest standard private Cache restImpersonationCache; // used for rest impersonation private Cache> restRoleCache; // + private Cache apiTokensCache; private void createCaches() { userCache = CacheBuilder.newBuilder() @@ -135,6 +136,17 @@ public void onRemoval(RemovalNotification> notification) { }) .build(); + apiTokensCache = CacheBuilder.newBuilder() + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener( + (RemovalListener) notification -> log.debug( + "Clear api token cache for {} due to {}", + notification.getKey(), + notification.getCause() + ) + ) + .build(); + } public BackendRegistry( @@ -170,6 +182,7 @@ public void invalidateCache() { userCache.invalidateAll(); restImpersonationCache.invalidateAll(); restRoleCache.invalidateAll(); + apiTokensCache.invalidateAll(); } @Subscribe diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ApiTokenApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ApiTokenApiAction.java new file mode 100644 index 0000000000..df1e96c6e0 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ApiTokenApiAction.java @@ -0,0 +1,109 @@ +/* + * 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.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; + +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.ok; +import static org.opensearch.security.dlic.rest.api.Responses.response; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class ApiTokenApiAction extends AbstractApiAction { + + public static final String NAME_JSON_PROPERTY = "name"; + + private static final List 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::apiTokenApiRequestHandlers); + } + + @Override + public String getName() { + return "API Token actions to retrieve / update configs."; + } + + @Override + public List routes() { + return ROUTES; + } + + @Override + protected CType getConfigType() { + return CType.CONFIG; + } + + private void apiTokenApiRequestHandlers(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()) { + try (final ThreadContext.StoredContext ctx = client.threadPool().getThreadContext().stashContext()) { + final Map 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 + ); + logger.info(client.admin().indices().create(createIndexRequest).actionGet().isAcknowledged()); + } + } + } +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java b/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java index ecc9dcbc59..afed070a78 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java @@ -25,6 +25,7 @@ public enum Endpoint { AUTHTOKEN, TENANTS, RATELIMITERS, + APITOKENS, MIGRATE, VALIDATE, WHITELIST, diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index c28a1bdc1d..f1dfed03f3 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -96,6 +96,7 @@ public static Collection 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, diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java index 064f555a75..a6f1789150 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java @@ -110,6 +110,8 @@ public abstract class DynamicConfigModel { public abstract Settings getDynamicOnBehalfOfSettings(); + public abstract Settings getDynamicApiTokenSettings(); + protected final Map authImplMap = new HashMap<>(); public DynamicConfigModel() { @@ -142,5 +144,4 @@ public DynamicConfigModel() { authImplMap.put("ip_authFailureListener", AddressBasedRateLimiter.class.getName()); authImplMap.put("username_authFailureListener", UserNameBasedRateLimiter.class.getName()); } - } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 4bc9e82882..3d1d48ca94 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -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 restAuthDomains0 = new TreeSet<>(); @@ -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 originalDestroyableComponents = destroyableComponents; restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index 77fb973a52..c9d431bb06 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -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() { @@ -495,4 +496,57 @@ 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 + + "]"; + } + } + } diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index f35afc6489..2a3b898bdf 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -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 getSettingAsSet( final Settings settings, final String key,