Skip to content

Commit

Permalink
Merge branch 'feature/api-tokens' of github.com:opensearch-project/se…
Browse files Browse the repository at this point in the history
…curity into authcz

Signed-off-by: Derek Ho <[email protected]>
  • Loading branch information
derek-ho committed Dec 16, 2024
2 parents 98d3847 + 3177c34 commit 2152448
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,11 @@ public List<RestHandler> getRestHandlers(
)
);
handlers.add(new CreateOnBehalfOfTokenAction(tokenManager));
<<<<<<< HEAD
handlers.add(new ApiTokenAction(cs, localClient, settings));
=======
handlers.add(new ApiTokenAction(cs, threadPool, localClient));
>>>>>>> 3177c349d27b0a3758dfdbba417def1b85902ed1
handlers.addAll(
SecurityRestApiActions.getHandler(
settings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,24 @@ public ApiToken(String name, String jti, List<String> clusterPermissions, List<I
this.expiration = expiration;
}

public ApiToken(String name, String jti, List<String> clusterPermissions, List<IndexPermission> indexPermissions) {
this.creationTime = Instant.now();
this.name = name;
this.jti = jti;
this.clusterPermissions = clusterPermissions;
this.indexPermissions = indexPermissions;
this.expiration = Long.MAX_VALUE;
}

public ApiToken(
String description,
String name,
String jti,
List<String> clusterPermissions,
List<IndexPermission> indexPermissions,
Instant creationTime,
Long expiration
) {
this.name = description;
this.name = name;
this.jti = jti;
this.clusterPermissions = clusterPermissions;
this.indexPermissions = indexPermissions;
Expand Down Expand Up @@ -90,13 +99,30 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
}
}

/**
* Class represents an API token.
* Expected class structure
* {
* name: "token_name",
* jti: "encrypted_token",
* creation_time: 1234567890,
* cluster_permissions: ["cluster_permission1", "cluster_permission2"],
* index_permissions: [
* {
* index_pattern: ["index_pattern1", "index_pattern2"],
* allowed_actions: ["allowed_action1", "allowed_action2"]
* }
* ],
* expiration: 1234567890
* }
*/
public static ApiToken fromXContent(XContentParser parser) throws IOException {
String name = null;
String jti = null;
List<String> clusterPermissions = new ArrayList<>();
List<IndexPermission> indexPermissions = new ArrayList<>();
Instant creationTime = null;
Long expiration = Long.MAX_VALUE;
long expiration = Long.MAX_VALUE;

XContentParser.Token token;
String currentFieldName = null;
Expand Down Expand Up @@ -137,16 +163,6 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException {
}
}

if (name == null) {
throw new IllegalArgumentException(NAME_FIELD + " is required");
}
if (jti == null) {
throw new IllegalArgumentException(JTI_FIELD + " is required");
}
if (creationTime == null) {
throw new IllegalArgumentException(CREATION_TIME_FIELD + " is required");
}

return new ApiToken(name, jti, clusterPermissions, indexPermissions, creationTime, expiration);
}

Expand All @@ -172,15 +188,9 @@ private static IndexPermission parseIndexPermission(XContentParser parser) throw
allowedActions.add(parser.text());
}
break;

}
}
}

if (indexPatterns.isEmpty()) {
throw new IllegalArgumentException(INDEX_PATTERN_FIELD + " is required for index permission");
}

return new IndexPermission(indexPatterns, allowedActions);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
import org.opensearch.client.Client;
import org.opensearch.client.node.NodeClient;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.settings.Settings;
import org.opensearch.core.rest.RestStatus;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.rest.BaseRestHandler;
import org.opensearch.rest.BytesRestResponse;
import org.opensearch.rest.RestHandler;
import org.opensearch.rest.RestRequest;
import org.opensearch.threadpool.ThreadPool;

import static org.opensearch.rest.RestRequest.Method.DELETE;
import static org.opensearch.rest.RestRequest.Method.GET;
Expand All @@ -41,6 +41,8 @@
import static org.opensearch.security.action.apitokens.ApiToken.INDEX_PERMISSIONS_FIELD;
import static org.opensearch.security.action.apitokens.ApiToken.NAME_FIELD;
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix;
import static org.opensearch.security.util.ParsingUtils.safeMapList;
import static org.opensearch.security.util.ParsingUtils.safeStringList;

public class ApiTokenAction extends BaseRestHandler {
private final ApiTokenRepository apiTokenRepository;
Expand All @@ -53,13 +55,14 @@ public class ApiTokenAction extends BaseRestHandler {
)
);

public ApiTokenAction(ClusterService clusterService, Client client, Settings settings) {
this.apiTokenRepository = new ApiTokenRepository(client, clusterService, settings);

public ApiTokenAction(ClusterService clusterService, ThreadPool threadPool, Client client) {
this.apiTokenRepository = new ApiTokenRepository(client, clusterService);
}

@Override
public String getName() {
return "Actions to get and create API tokens.";
return "api_token_action";
}

@Override
Expand Down Expand Up @@ -100,7 +103,7 @@ private RestChannelConsumer handleGet(RestRequest request, NodeClient client) {

response = new BytesRestResponse(RestStatus.OK, builder);
} catch (final Exception exception) {
builder.startObject().field("error", "An unexpected error occurred. Please check the input and try again.").endObject();
builder.startObject().field("error", exception.getMessage()).endObject();
response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder);
}
builder.close();
Expand All @@ -124,7 +127,6 @@ private RestChannelConsumer handlePost(RestRequest request, NodeClient client) {
clusterPermissions,
indexPermissions,
(Long) requestBody.getOrDefault(EXPIRATION_FIELD, Long.MAX_VALUE)

);

builder.startObject();
Expand All @@ -144,61 +146,20 @@ private RestChannelConsumer handlePost(RestRequest request, NodeClient client) {
};
}

/**
* Safely casts an Object to List<String> with validation
*/
List<String> safeStringList(Object obj, String fieldName) {
if (!(obj instanceof List<?> list)) {
throw new IllegalArgumentException(fieldName + " must be an array");
}

for (Object item : list) {
if (!(item instanceof String)) {
throw new IllegalArgumentException(fieldName + " must contain only strings");
}
}

return list.stream().map(String.class::cast).collect(Collectors.toList());
}

/**
* Safely casts an Object to List<Map<String, Object>> with validation
*/
@SuppressWarnings("unchecked")
List<Map<String, Object>> safeMapList(Object obj, String fieldName) {
if (!(obj instanceof List<?> list)) {
throw new IllegalArgumentException(fieldName + " must be an array");
}

for (Object item : list) {
if (!(item instanceof Map)) {
throw new IllegalArgumentException(fieldName + " must contain object entries");
}
}
return list.stream().map(item -> (Map<String, Object>) item).collect(Collectors.toList());
}

/**
* Extracts cluster permissions from the request body
*/
List<String> extractClusterPermissions(Map<String, Object> requestBody) {
if (!requestBody.containsKey(CLUSTER_PERMISSIONS_FIELD)) {
return Collections.emptyList();
}

return safeStringList(requestBody.get(CLUSTER_PERMISSIONS_FIELD), CLUSTER_PERMISSIONS_FIELD);
}

/**
* Extracts and builds index permissions from the request body
*/
List<ApiToken.IndexPermission> extractIndexPermissions(Map<String, Object> requestBody) {
if (!requestBody.containsKey(INDEX_PERMISSIONS_FIELD)) {
return Collections.emptyList();
}

List<Map<String, Object>> indexPerms = safeMapList(requestBody.get(INDEX_PERMISSIONS_FIELD), INDEX_PERMISSIONS_FIELD);

return indexPerms.stream().map(this::createIndexPermission).collect(Collectors.toList());
}

Expand All @@ -216,6 +177,7 @@ ApiToken.IndexPermission createIndexPermission(Map<String, Object> indexPerm) {

List<String> allowedActions = safeStringList(indexPerm.get(ALLOWED_ACTIONS_FIELD), ALLOWED_ACTIONS_FIELD);


return new ApiToken.IndexPermission(indexPatterns, allowedActions);
}

Expand Down Expand Up @@ -285,7 +247,7 @@ private RestChannelConsumer handleDelete(RestRequest request, NodeClient client)
builder.startObject().field("error", exception.getMessage()).endObject();
response = new BytesRestResponse(RestStatus.NOT_FOUND, builder);
} catch (final Exception exception) {
builder.startObject().field("error", "An unexpected error occurred. Please check the input and try again.").endObject();
builder.startObject().field("error", exception.getMessage()).endObject();
response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder);
}
builder.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.opensearch.index.reindex.DeleteByQueryRequest;
import org.opensearch.search.SearchHit;
import org.opensearch.search.builder.SearchSourceBuilder;
import org.opensearch.security.dlic.rest.support.Utils;
import org.opensearch.security.support.ConfigConstants;

import static org.opensearch.security.action.apitokens.ApiToken.NAME_FIELD;
Expand All @@ -56,8 +57,14 @@ public ApiTokenIndexHandler(Client client, ClusterService clusterService) {
this.clusterService = clusterService;
}

public String indexTokenPayload(ApiToken token) {
public String indexTokenMetadata(ApiToken token) {
// TODO: move this out of index handler class, potentially create a layer in between baseresthandler and abstractapiaction which can
// abstract this complexity away
final var originalUserAndRemoteAddress = Utils.userAndRemoteAddressFrom(client.threadPool().getThreadContext());
try (final ThreadContext.StoredContext ctx = client.threadPool().getThreadContext().stashContext()) {
client.threadPool()
.getThreadContext()
.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUserAndRemoteAddress.getLeft());

XContentBuilder builder = XContentFactory.jsonBuilder();
String jsonString = token.toXContent(builder, ToXContent.EMPTY_PARAMS).toString();
Expand All @@ -81,7 +88,11 @@ public String indexTokenPayload(ApiToken token) {
}

public void deleteToken(String name) throws ApiTokenException {
final var originalUserAndRemoteAddress = Utils.userAndRemoteAddressFrom(client.threadPool().getThreadContext());
try (final ThreadContext.StoredContext ctx = client.threadPool().getThreadContext().stashContext()) {
client.threadPool()
.getThreadContext()
.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUserAndRemoteAddress.getLeft());
DeleteByQueryRequest request = new DeleteByQueryRequest(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX).setQuery(
QueryBuilders.matchQuery(NAME_FIELD, name)
).setRefresh(true);
Expand All @@ -97,8 +108,13 @@ public void deleteToken(String name) throws ApiTokenException {
}
}

public Map<String, ApiToken> getTokenPayloads() {

public Map<String, ApiToken> getTokenMetadatas() {
final var originalUserAndRemoteAddress = Utils.userAndRemoteAddressFrom(client.threadPool().getThreadContext());
try (final ThreadContext.StoredContext ctx = client.threadPool().getThreadContext().stashContext()) {
client.threadPool()
.getThreadContext()
.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUserAndRemoteAddress.getLeft());
SearchRequest searchRequest = new SearchRequest(ConfigConstants.OPENSEARCH_API_TOKENS_INDEX);
searchRequest.source(new SearchSourceBuilder());

Expand Down Expand Up @@ -130,8 +146,13 @@ public Boolean apiTokenIndexExists() {
}

public void createApiTokenIndexIfAbsent() {
// TODO: Decide if this should be done at bootstrap
if (!apiTokenIndexExists()) {
final var originalUserAndRemoteAddress = Utils.userAndRemoteAddressFrom(client.threadPool().getThreadContext());
try (final ThreadContext.StoredContext ctx = client.threadPool().getThreadContext().stashContext()) {
client.threadPool()
.getThreadContext()
.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUserAndRemoteAddress.getLeft());
final Map<String, Object> indexSettings = ImmutableMap.of(
"index.number_of_shards",
1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

import org.opensearch.client.Client;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.settings.Settings;
import org.opensearch.index.IndexNotFoundException;

public class ApiTokenRepository {
private final ApiTokenIndexHandler apiTokenIndexHandler;

public ApiTokenRepository(Client client, ClusterService clusterService, Settings settings) {
public ApiTokenRepository(Client client, ClusterService clusterService) {
apiTokenIndexHandler = new ApiTokenIndexHandler(client, clusterService);
}

Expand All @@ -34,17 +34,15 @@ public String createApiToken(
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
return apiTokenIndexHandler.indexTokenPayload(new ApiToken(name, "test-token", clusterPermissions, indexPermissions, expiration));
return apiTokenIndexHandler.indexTokenMetadata(new ApiToken(name, "test-token", clusterPermissions, indexPermissions, expiration));
}

public void deleteApiToken(String name) throws ApiTokenException {
apiTokenIndexHandler.createApiTokenIndexIfAbsent();
public void deleteApiToken(String name) throws ApiTokenException, IndexNotFoundException {
apiTokenIndexHandler.deleteToken(name);
}

public Map<String, ApiToken> getApiTokens() {
apiTokenIndexHandler.createApiTokenIndexIfAbsent();
return apiTokenIndexHandler.getTokenPayloads();
public Map<String, ApiToken> getApiTokens() throws IndexNotFoundException {
return apiTokenIndexHandler.getTokenMetadatas();
}

}
Loading

0 comments on commit 2152448

Please sign in to comment.