Skip to content

Commit

Permalink
Clean up issuing, encrypting, and decrypting logic, add tests for the…
Browse files Browse the repository at this point in the history
… feature

Signed-off-by: Derek Ho <[email protected]>
  • Loading branch information
derek-ho committed Dec 18, 2024
1 parent 3a2e483 commit 9abd1d0
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.core.xcontent.XContentParser;

import com.nimbusds.jose.shaded.gson.JsonArray;
import com.nimbusds.jose.shaded.gson.JsonElement;
import com.nimbusds.jose.shaded.gson.JsonObject;
import com.nimbusds.jose.shaded.gson.JsonParseException;
import com.nimbusds.jose.shaded.gson.JsonParser;

public class ApiToken implements ToXContent {
public static final String NAME_FIELD = "name";
public static final String JTI_FIELD = "jti";
Expand Down Expand Up @@ -99,6 +105,37 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.endObject();
return builder;
}

@Override
public String toString() {
JsonObject json = new JsonObject();
JsonArray patternsArray = new JsonArray();
JsonArray actionsArray = new JsonArray();

for (String pattern : indexPatterns) {
patternsArray.add(pattern);
}
for (String action : allowedActions) {
actionsArray.add(action);
}

json.add(INDEX_PATTERN_FIELD, patternsArray);
json.add(ALLOWED_ACTIONS_FIELD, actionsArray);

return json.toString();
}

public static IndexPermission fromString(String str) {
try {
JsonObject json = JsonParser.parseString(str).getAsJsonObject();
return new IndexPermission(
json.get(INDEX_PATTERN_FIELD).getAsJsonArray().asList().stream().map(JsonElement::getAsString).toList(),
json.get(ALLOWED_ACTIONS_FIELD).getAsJsonArray().asList().stream().map(JsonElement::getAsString).toList()
);
} catch (JsonParseException e) {
throw new IllegalArgumentException("Invalid IndexPermission format", e);
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import org.opensearch.client.Client;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.collect.Tuple;
import org.opensearch.index.IndexNotFoundException;
import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken;
import org.opensearch.security.identity.SecurityTokenManager;
Expand All @@ -30,21 +29,24 @@ public ApiTokenRepository(Client client, ClusterService clusterService, Security
securityTokenManager = tokenManager;
}

public ApiTokenRepository(ApiTokenIndexHandler apiTokenIndexHandler1, SecurityTokenManager tokenManager) {
apiTokenIndexHandler = apiTokenIndexHandler1;
securityTokenManager = tokenManager;
}

public String createApiToken(
String name,
List<String> clusterPermissions,
List<ApiToken.IndexPermission> indexPermissions,
Long expiration
) {
apiTokenIndexHandler.createApiTokenIndexIfAbsent();
// TODO: Implement logic of creating JTI to match against during authc/z
// TODO: Add validation on whether user is creating a token with a subset of their permissions
ApiToken apiToken = new ApiToken(name, clusterPermissions, indexPermissions, expiration);
Tuple<ExpiringBearerAuthToken, String> token = securityTokenManager.issueApiToken(apiToken);
apiToken.setJti(token.v2());
ExpiringBearerAuthToken token = securityTokenManager.issueApiToken(apiToken);
apiToken.setJti(securityTokenManager.encryptToken(token.getCompleteToken()));
apiTokenIndexHandler.indexTokenMetadata(apiToken);

return token.v1().getCompleteToken();
return token.getCompleteToken();
}

public void deleteApiToken(String name) throws ApiTokenException, IndexNotFoundException {
Expand Down
24 changes: 18 additions & 6 deletions src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;
Expand Down Expand Up @@ -153,7 +154,7 @@ public ExpiringBearerAuthToken createJwt(
}

@SuppressWarnings("removal")
public Tuple<ExpiringBearerAuthToken, String> createJwt(
public ExpiringBearerAuthToken createJwt(
final String issuer,
final String subject,
final String audience,
Expand All @@ -180,7 +181,11 @@ public Tuple<ExpiringBearerAuthToken, String> createJwt(
}

if (indexPermissions != null) {
final String listOfIndexPermissions = String.join(", ", indexPermissions.toString());
List<String> permissionStrings = new ArrayList<>();
for (ApiToken.IndexPermission permission : indexPermissions) {
permissionStrings.add(permission.toString());
}
final String listOfIndexPermissions = String.join(",", permissionStrings);
claimsBuilder.claim("ip", encryptionDecryptionUtil.encrypt(listOfIndexPermissions));
}

Expand All @@ -199,9 +204,16 @@ public Tuple<ExpiringBearerAuthToken, String> createJwt(
);
}

return Tuple.tuple(
new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime),
encryptionDecryptionUtil.encrypt(signedJwt.serialize())
);
return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime);
}

/* Returns the encrypted string based on encryption settings */
public String encryptString(final String input) {
return encryptionDecryptionUtil.encrypt(input);
}

/* Returns the decrypted string based on encryption settings */
public String decryptString(final String input) {
return encryptionDecryptionUtil.decrypt(input);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import org.opensearch.OpenSearchSecurityException;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.settings.Settings;
import org.opensearch.core.common.transport.TransportAddress;
import org.opensearch.identity.Subject;
Expand Down Expand Up @@ -141,7 +140,7 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final
}
}

public Tuple<ExpiringBearerAuthToken, String> issueApiToken(final ApiToken apiToken) {
public ExpiringBearerAuthToken issueApiToken(final ApiToken apiToken) {
final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);
if (user == null) {
throw new OpenSearchSecurityException("Unsupported user to generate Api Token");
Expand All @@ -157,11 +156,19 @@ public Tuple<ExpiringBearerAuthToken, String> issueApiToken(final ApiToken apiTo
apiToken.getIndexPermissions()
);
} catch (final Exception ex) {
logger.error("Error creating OnBehalfOfToken for " + user.getName(), ex);
throw new OpenSearchSecurityException("Unable to generate OnBehalfOfToken");
logger.error("Error creating Api Token for " + user.getName(), ex);
throw new OpenSearchSecurityException("Unable to generate Api Token");
}
}

public String encryptToken(final String token) {
return apiTokenJwtVendor.encryptString(token);
}

public String decryptString(final String input) {
return apiTokenJwtVendor.decryptString(input);
}

@Override
public AuthToken issueServiceAccountToken(final String serviceId) {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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.action.apitokens;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;

import org.opensearch.index.IndexNotFoundException;
import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken;
import org.opensearch.security.identity.SecurityTokenManager;

import org.mockito.Mock;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class ApiTokenRepositoryTest {
@Mock
private SecurityTokenManager securityTokenManager;

@Mock
private ApiTokenIndexHandler apiTokenIndexHandler;

private ApiTokenRepository repository;

@Before
public void setUp() {
apiTokenIndexHandler = mock(ApiTokenIndexHandler.class);
securityTokenManager = mock(SecurityTokenManager.class);
repository = new ApiTokenRepository(apiTokenIndexHandler, securityTokenManager);
}

@Test
public void testDeleteApiToken() throws ApiTokenException {
String tokenName = "test-token";

repository.deleteApiToken(tokenName);

verify(apiTokenIndexHandler).deleteToken(tokenName);
}

@Test
public void testGetApiTokens() throws IndexNotFoundException {
Map<String, ApiToken> expectedTokens = new HashMap<>();
expectedTokens.put("token1", new ApiToken("token1", Arrays.asList("perm1"), Arrays.asList(), Long.MAX_VALUE));
when(apiTokenIndexHandler.getTokenMetadatas()).thenReturn(expectedTokens);

Map<String, ApiToken> result = repository.getApiTokens();

assertThat(result, equalTo(expectedTokens));
verify(apiTokenIndexHandler).getTokenMetadatas();
}

@Test
public void testCreateApiToken() {
String tokenName = "test-token";
List<String> clusterPermissions = Arrays.asList("cluster:admin");
List<ApiToken.IndexPermission> indexPermissions = Arrays.asList(
new ApiToken.IndexPermission(Arrays.asList("test-*"), Arrays.asList("read"))
);
Long expiration = 3600L;

String completeToken = "complete-token";
String encryptedToken = "encrypted-token";
ExpiringBearerAuthToken bearerToken = mock(ExpiringBearerAuthToken.class);
when(bearerToken.getCompleteToken()).thenReturn(completeToken);
when(securityTokenManager.issueApiToken(any())).thenReturn(bearerToken);
when(securityTokenManager.encryptToken(completeToken)).thenReturn(encryptedToken);

String result = repository.createApiToken(tokenName, clusterPermissions, indexPermissions, expiration);

verify(apiTokenIndexHandler).createApiTokenIndexIfAbsent();
verify(securityTokenManager).issueApiToken(any(ApiToken.class));
verify(securityTokenManager).encryptToken(completeToken);
verify(apiTokenIndexHandler).indexTokenMetadata(
argThat(
token -> token.getName().equals(tokenName)
&& token.getJti().equals(encryptedToken)
&& token.getClusterPermissions().equals(clusterPermissions)
&& token.getIndexPermissions().equals(indexPermissions)
)
);
assertThat(result, equalTo(completeToken));
}

@Test(expected = IndexNotFoundException.class)
public void testGetApiTokensThrowsIndexNotFoundException() throws IndexNotFoundException {
when(apiTokenIndexHandler.getTokenMetadatas()).thenThrow(new IndexNotFoundException("test-index"));

repository.getApiTokens();

}

@Test(expected = ApiTokenException.class)
public void testDeleteApiTokenThrowsApiTokenException() throws ApiTokenException {
String tokenName = "test-token";
doThrow(new ApiTokenException("Token not found")).when(apiTokenIndexHandler).deleteToken(tokenName);

repository.deleteApiToken(tokenName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.action.apitokens;

import java.util.Arrays;
import java.util.List;

import org.junit.Before;
import org.junit.Test;

import org.opensearch.client.Client;
import org.opensearch.client.IndicesAdminClient;
import org.opensearch.cluster.metadata.Metadata;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.settings.Settings;
import org.opensearch.common.util.concurrent.ThreadContext;

import org.mockito.Mock;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ApiTokenTest {

@Mock
private Client client;

@Mock
private IndicesAdminClient indicesAdminClient;

@Mock
private ClusterService clusterService;

@Mock
private Metadata metadata;

private ApiTokenIndexHandler indexHandler;

@Before
public void setup() {

client = mock(Client.class, RETURNS_DEEP_STUBS);
indicesAdminClient = mock(IndicesAdminClient.class);
clusterService = mock(ClusterService.class, RETURNS_DEEP_STUBS);
metadata = mock(Metadata.class);

when(client.admin().indices()).thenReturn(indicesAdminClient);

when(clusterService.state().metadata()).thenReturn(metadata);

ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
when(client.threadPool().getThreadContext()).thenReturn(threadContext);

indexHandler = new ApiTokenIndexHandler(client, clusterService);
}

@Test
public void testIndexPermissionToStringFromString() {
String indexPermissionString = "{\"index_pattern\":[\"index1\",\"index2\"],\"allowed_actions\":[\"action1\",\"action2\"]}";
ApiToken.IndexPermission indexPermission = new ApiToken.IndexPermission(
Arrays.asList("index1", "index2"),
Arrays.asList("action1", "action2")
);
assertThat(indexPermission.toString(), equalTo(indexPermissionString));

ApiToken.IndexPermission indexPermissionFromString = ApiToken.IndexPermission.fromString(indexPermissionString);
assertThat(indexPermissionFromString.getIndexPatterns(), equalTo(List.of("index1", "index2")));
assertThat(indexPermissionFromString.getAllowedActions(), equalTo(List.of("action1", "action2")));
}

}
Loading

0 comments on commit 9abd1d0

Please sign in to comment.