Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
6ae7090
Merge branch 'main' of github.com:opensearch-project/security into HEAD
derek-ho Nov 14, 2024
3177c34
Scaffolding for POST/DELETE/GET api tokens calls (#4921)
derek-ho Dec 16, 2024
dacdae5
Adds JTI and expiration field support for API Tokens (#4967)
derek-ho Dec 20, 2024
e255e14
Merge branch 'main' of github.com:opensearch-project/security into HEAD
derek-ho Dec 20, 2024
190bfec
Merge branch 'main' of github.com:opensearch-project/security into HEAD
derek-ho Jan 17, 2025
79f0c46
Api token authc/z implementation with Cache (#4992)
derek-ho Feb 4, 2025
a8b4ac1
Subset of permissions check on creation (#5012)
derek-ho Feb 21, 2025
3776667
Merge branch 'main' of github.com:opensearch-project/security into HEAD
derek-ho Mar 11, 2025
8750e8b
Change API token index actions to use action listeners and limit to 1…
derek-ho Mar 24, 2025
7b8b069
Merge branch 'main' into feature/api-tokens-cwperx
cwperks May 22, 2025
12c0f9c
Fix naming
cwperks May 22, 2025
896e9e2
Use one PrivilegesEvaluatorContext
cwperks May 22, 2025
97db90d
Handle authz
cwperks May 22, 2025
362e67f
fix unit tests
cwperks May 22, 2025
fa98ae2
Fix tests
cwperks May 23, 2025
f3cd485
Merge branch 'main' into feature/api-tokens-cwperx
cwperks May 27, 2025
68107ff
Add integrationTests for API Token
cwperks May 27, 2025
f5b965a
Add more integration tests
cwperks May 27, 2025
ba93aa3
Add token prefix
cwperks May 28, 2025
e35d3ef
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Jun 23, 2025
c028420
Rebase with main
cwperks Jun 23, 2025
109c1ef
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Jun 25, 2025
3a71078
Fix compilation issues
cwperks Jun 25, 2025
dad7551
Add to CHANGELOG
cwperks Jun 25, 2025
30f4f6f
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Jul 21, 2025
3f11a61
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Aug 25, 2025
eb512e3
Address PR feedback
cwperks Aug 25, 2025
93a0e4f
Attempt to resolve conflicts
cwperks Nov 25, 2025
bdd7d7f
Fix unit test
cwperks Nov 26, 2025
6c45459
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Mar 17, 2026
51c086f
Address PR feedback
cwperks Mar 17, 2026
cd47c36
Address comments
cwperks Mar 17, 2026
2780872
Use XContent parsing
cwperks Mar 17, 2026
720f72f
Address comments
cwperks Mar 17, 2026
c86b305
Fix test
cwperks Mar 17, 2026
64bee5f
Make API Tokens Opaque Strings
cwperks Mar 17, 2026
a0876e4
Update delete
cwperks Mar 17, 2026
78b5cbb
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Mar 24, 2026
0d867ed
Implement soft-delete for token revocation
cwperks Mar 25, 2026
eacd135
spotlessApply
cwperks Mar 25, 2026
eb1b7cd
Modify dashboards-info endpoint to include whether API tokens are ena…
cwperks Mar 26, 2026
037a6f8
Merge branch 'dashboards-token-enabled' into feature/api-tokens-cwperx
cwperks Mar 27, 2026
25b8bee
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Mar 27, 2026
a017555
Add max_token_expiration_seconds to token settings and also my expira…
cwperks Mar 27, 2026
62bbece
Add created_by
cwperks Mar 27, 2026
052e2f3
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Apr 7, 2026
4bbd6f0
Merge branch 'main' into feature/api-tokens-cwperx
cwperks Apr 7, 2026
c84edd2
Merge branch 'feature/api-tokens-cwperx' of https://github.com/cwperk…
cwperks Apr 7, 2026
a3af347
Merge branch 'main' into feature/api-tokens-cwperx
cwperks May 4, 2026
3ed0685
Address review feedback
cwperks May 4, 2026
43930aa
Remove references to jti
cwperks May 5, 2026
78049ef
Add support for v4
cwperks May 5, 2026
8b619cc
Address feedback
cwperks May 5, 2026
f0772a5
Audit log test and token cache
cwperks May 5, 2026
39b5609
Assert full name
cwperks May 5, 2026
d85f1eb
Remove unnecessary changes
cwperks May 5, 2026
a85a39e
spotlessApply
cwperks May 5, 2026
67105b5
Merge branch 'main' into feature/api-tokens-cwperx
cwperks May 5, 2026
0061001
Merge branch 'feature/api-tokens-cwperx' of https://github.com/cwperk…
cwperks May 6, 2026
5ead3fc
Address code review comments and make new audit log category for API_…
cwperks May 6, 2026
42ecbe0
Fix unit tests
cwperks May 6, 2026
52ce2e6
Address review feedback
cwperks May 11, 2026
aaf3fc4
Merge branch 'main' into feature/api-tokens-cwperx
cwperks May 11, 2026
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 @@ -46,6 +46,7 @@ public void testDashboardsInfoValidationMessage() throws Exception {
assertThat(response, isOk());
assertThat(response.getTextFromJsonBody("/password_validation_error_message"), equalTo(DEFAULT_PASSWORD_MESSAGE));
assertThat(response.getTextFromJsonBody("/password_validation_regex"), equalTo(DEFAULT_PASSWORD_REGEX));
assertThat(response.getTextFromJsonBody("/api_tokens_enabled"), equalTo("false"));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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.privileges;

import java.util.List;
import java.util.Map;

import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.http.HttpStatus;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;

import org.opensearch.security.auditlog.impl.AuditCategory;
import org.opensearch.security.auditlog.impl.AuditMessage;
import org.opensearch.test.framework.ApiTokenConfig;
import org.opensearch.test.framework.AuditCompliance;
import org.opensearch.test.framework.AuditConfiguration;
import org.opensearch.test.framework.AuditFilters;
import org.opensearch.test.framework.TestSecurityConfig;
import org.opensearch.test.framework.audit.AuditLogsRule;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;

import static org.opensearch.security.auditlog.impl.AuditCategory.AUTHENTICATED;
import static org.opensearch.security.auditlog.impl.AuditCategory.GRANTED_PRIVILEGES;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;

public class ApiTokenAuditTest {

static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS);

private static final String API_TOKEN_PATH = "_plugins/_security/api/apitokens";
private static final String TOKEN_PAYLOAD = """
{
"name": "audit-test-token",
"cluster_permissions": ["cluster_monitor"],
"duration_seconds": 3600
}
""";

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
.users(ADMIN_USER)
.nodeSettings(
Map.of(
SECURITY_RESTAPI_ROLES_ENABLED,
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
"plugins.security.unsupported.restapi.allow_securityconfig_modification",
true
)
)
.authc(AUTHC_HTTPBASIC_INTERNAL)
.apiToken(new ApiTokenConfig().enabled(true))
.audit(
new AuditConfiguration(true).compliance(new AuditCompliance().enabled(true).internalConfig(true))
.filters(new AuditFilters().enabledRest(true).enabledTransport(true))
)
.build();

@Rule
public AuditLogsRule auditLogsRule = new AuditLogsRule();

@Test
public void testApiTokenAuthenticationIsAudited() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should also add a test about any changes to the token being audited.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added a test for revoke and corresponding entry

String token;
try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
TestRestClient.HttpResponse response = client.postJson(API_TOKEN_PATH, TOKEN_PAYLOAD);
response.assertStatusCode(HttpStatus.SC_OK);
token = response.getTextFromJsonBody("/token");
}

Header authHeader = new BasicHeader("Authorization", "ApiKey " + token);
try (TestRestClient client = cluster.getRestClient(authHeader)) {
TestRestClient.HttpResponse response = client.get("_cluster/health");
response.assertStatusCode(HttpStatus.SC_OK);
}

auditLogsRule.assertExactlyOne(
(AuditMessage msg) -> msg.getCategory() == AUTHENTICATED && "token:audit-test-token".equals(msg.getInitiatingUser())
);
auditLogsRule.assertExactlyOne(
(AuditMessage msg) -> msg.getCategory() == GRANTED_PRIVILEGES && "token:audit-test-token".equals(msg.getEffectiveUser())
);
}

@Test
public void testApiTokenCreationIsAuditedWithTokenWriteCategory() {
String createPayload = """
{
"name": "audit-write-test-token",
"cluster_permissions": ["cluster_monitor"],
"duration_seconds": 3600
}
""";
try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
TestRestClient.HttpResponse response = client.postJson(API_TOKEN_PATH, createPayload);
response.assertStatusCode(HttpStatus.SC_OK);
}

auditLogsRule.assertExactlyOne((AuditMessage msg) -> msg.getCategory() == AuditCategory.API_TOKEN_WRITE);
}

@Test
public void testApiTokenRevocationIsAudited() {
String createPayload = """
{
"name": "audit-revoke-test-token",
"cluster_permissions": ["cluster_monitor"],
"duration_seconds": 3600
}
""";
String tokenId;
try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
TestRestClient.HttpResponse response = client.postJson(API_TOKEN_PATH, createPayload);
response.assertStatusCode(HttpStatus.SC_OK);
tokenId = response.getTextFromJsonBody("/id");
}

try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
client.delete(API_TOKEN_PATH + "/" + tokenId).assertStatusCode(HttpStatus.SC_OK);
}

auditLogsRule.assertExactly(2, (AuditMessage msg) -> msg.getCategory() == AuditCategory.API_TOKEN_WRITE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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.privileges;

import java.util.List;
import java.util.Map;

import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.http.HttpStatus;
import org.junit.ClassRule;
import org.junit.Test;

import org.opensearch.test.framework.ApiTokenConfig;
import org.opensearch.test.framework.TestSecurityConfig;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;

/**
* Verifies that API tokens cannot access protected indices even with wildcard permissions.
*/
public class ApiTokenProtectedIndicesTest {

static final TestSecurityConfig.Role PROTECTED_INDEX_ROLE = new TestSecurityConfig.Role("protected_role");

static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
.referencedRoles(PROTECTED_INDEX_ROLE);

private static final String API_TOKEN_PATH = "_plugins/_security/api/apitokens";

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
.users(ADMIN_USER)
.roles(PROTECTED_INDEX_ROLE)
.nodeSettings(
Map.of(
SECURITY_RESTAPI_ROLES_ENABLED,
List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()),
"plugins.security.unsupported.restapi.allow_securityconfig_modification",
true,
"plugins.security.protected_indices.enabled",
true,
"plugins.security.protected_indices.indices",
"protected-*",
"plugins.security.protected_indices.roles",
"protected_role"
)
)
.authc(AUTHC_HTTPBASIC_INTERNAL)
.apiToken(new ApiTokenConfig().enabled(true))
.privilegesEvaluationType("v4")
.build();

@Test
public void testApiTokenCannotAccessProtectedIndex() {
// Create the protected index as admin
try (TestRestClient adminClient = cluster.getRestClient(ADMIN_USER)) {
adminClient.putJson("protected-secret", "{\"settings\":{\"number_of_shards\":1}}").assertStatusCode(HttpStatus.SC_OK);
}

// Create a token with wildcard index permissions
String payload = """
{
"name": "wildcard-token",
"cluster_permissions": [],
"index_permissions": [{
"index_pattern": ["*"],
"allowed_actions": ["indices:data/read/search"]
}],
"duration_seconds": 3600
}
""";
String token;
try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
TestRestClient.HttpResponse response = client.postJson(API_TOKEN_PATH, payload);
response.assertStatusCode(HttpStatus.SC_OK);
token = response.getTextFromJsonBody("/token");
}

// Token should be denied access to the protected index
Header authHeader = new BasicHeader("Authorization", "ApiKey " + token);
try (TestRestClient client = cluster.getRestClient(authHeader)) {
TestRestClient.HttpResponse response = client.get("protected-secret/_search");
assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
}
}

@Test
public void testApiTokenCanAccessNonProtectedIndex() {
// Create a non-protected index
try (TestRestClient adminClient = cluster.getRestClient(ADMIN_USER)) {
adminClient.putJson("normal-index", "{\"settings\":{\"number_of_shards\":1}}").assertStatusCode(HttpStatus.SC_OK);
}

String payload = """
{
"name": "normal-token",
"cluster_permissions": [],
"index_permissions": [{
"index_pattern": ["normal-*"],
"allowed_actions": ["indices:data/read/search"]
}],
"duration_seconds": 3600
}
""";
String token;
try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) {
TestRestClient.HttpResponse response = client.postJson(API_TOKEN_PATH, payload);
response.assertStatusCode(HttpStatus.SC_OK);
token = response.getTextFromJsonBody("/token");
}

Header authHeader = new BasicHeader("Authorization", "ApiKey " + token);
try (TestRestClient client = cluster.getRestClient(authHeader)) {
TestRestClient.HttpResponse response = client.get("normal-index/_search");
response.assertStatusCode(HttpStatus.SC_OK);
}
}
}
Loading
Loading