Skip to content

Commit 8750e8b

Browse files
authored
Change API token index actions to use action listeners and limit to 100 tokens outstanding (#5147)
Signed-off-by: Derek Ho <[email protected]>
1 parent 3776667 commit 8750e8b

File tree

8 files changed

+433
-239
lines changed

8 files changed

+433
-239
lines changed

src/main/java/org/opensearch/security/action/apitokens/ApiToken.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
public class ApiToken implements ToXContent {
2424
public static final String NAME_FIELD = "name";
25-
public static final String CREATION_TIME_FIELD = "creation_time";
25+
public static final String ISSUED_AT_FIELD = "iat";
2626
public static final String CLUSTER_PERMISSIONS_FIELD = "cluster_permissions";
2727
public static final String INDEX_PERMISSIONS_FIELD = "index_permissions";
2828
public static final String INDEX_PATTERN_FIELD = "index_pattern";
@@ -149,7 +149,7 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException {
149149
case NAME_FIELD:
150150
name = parser.text();
151151
break;
152-
case CREATION_TIME_FIELD:
152+
case ISSUED_AT_FIELD:
153153
creationTime = Instant.ofEpochMilli(parser.longValue());
154154
break;
155155
case EXPIRATION_FIELD:
@@ -227,7 +227,7 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params
227227
xContentBuilder.field(NAME_FIELD, name);
228228
xContentBuilder.field(CLUSTER_PERMISSIONS_FIELD, clusterPermissions);
229229
xContentBuilder.field(INDEX_PERMISSIONS_FIELD, indexPermissions);
230-
xContentBuilder.field(CREATION_TIME_FIELD, creationTime.toEpochMilli());
230+
xContentBuilder.field(ISSUED_AT_FIELD, creationTime.toEpochMilli());
231231
xContentBuilder.endObject();
232232
return xContentBuilder;
233233
}

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

Lines changed: 108 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.apache.logging.log4j.LogManager;
2525
import org.apache.logging.log4j.Logger;
2626

27-
import org.opensearch.client.node.NodeClient;
2827
import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
2928
import org.opensearch.cluster.service.ClusterService;
3029
import org.opensearch.common.settings.Settings;
@@ -48,16 +47,17 @@
4847
import org.opensearch.security.ssl.transport.PrincipalExtractor;
4948
import org.opensearch.security.support.ConfigConstants;
5049
import org.opensearch.threadpool.ThreadPool;
50+
import org.opensearch.transport.client.node.NodeClient;
5151

5252
import static org.opensearch.rest.RestRequest.Method.DELETE;
5353
import static org.opensearch.rest.RestRequest.Method.GET;
5454
import static org.opensearch.rest.RestRequest.Method.POST;
5555
import static org.opensearch.security.action.apitokens.ApiToken.ALLOWED_ACTIONS_FIELD;
5656
import static org.opensearch.security.action.apitokens.ApiToken.CLUSTER_PERMISSIONS_FIELD;
57-
import static org.opensearch.security.action.apitokens.ApiToken.CREATION_TIME_FIELD;
5857
import static org.opensearch.security.action.apitokens.ApiToken.EXPIRATION_FIELD;
5958
import static org.opensearch.security.action.apitokens.ApiToken.INDEX_PATTERN_FIELD;
6059
import static org.opensearch.security.action.apitokens.ApiToken.INDEX_PERMISSIONS_FIELD;
60+
import static org.opensearch.security.action.apitokens.ApiToken.ISSUED_AT_FIELD;
6161
import static org.opensearch.security.action.apitokens.ApiToken.NAME_FIELD;
6262
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix;
6363
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;
@@ -146,30 +146,32 @@ RestChannelConsumer doPrepareRequest(RestRequest request, NodeClient client) {
146146

147147
private RestChannelConsumer handleGet(RestRequest request, NodeClient client) {
148148
return channel -> {
149-
final XContentBuilder builder = channel.newBuilder();
150-
BytesRestResponse response;
151-
try {
152-
Map<String, ApiToken> tokens = apiTokenRepository.getApiTokens();
153-
154-
builder.startArray();
155-
for (ApiToken token : tokens.values()) {
156-
builder.startObject();
157-
builder.field(NAME_FIELD, token.getName());
158-
builder.field(CREATION_TIME_FIELD, token.getCreationTime().toEpochMilli());
159-
builder.field(EXPIRATION_FIELD, token.getExpiration());
160-
builder.field(CLUSTER_PERMISSIONS_FIELD, token.getClusterPermissions());
161-
builder.field(INDEX_PERMISSIONS_FIELD, token.getIndexPermissions());
162-
builder.endObject();
149+
apiTokenRepository.getApiTokens(ActionListener.wrap(tokens -> {
150+
try {
151+
XContentBuilder builder = channel.newBuilder();
152+
builder.startArray();
153+
for (ApiToken token : tokens.values()) {
154+
builder.startObject();
155+
builder.field(NAME_FIELD, token.getName());
156+
builder.field(ISSUED_AT_FIELD, token.getCreationTime().toEpochMilli());
157+
builder.field(EXPIRATION_FIELD, token.getExpiration());
158+
builder.field(CLUSTER_PERMISSIONS_FIELD, token.getClusterPermissions());
159+
builder.field(INDEX_PERMISSIONS_FIELD, token.getIndexPermissions());
160+
builder.endObject();
161+
}
162+
builder.endArray();
163+
164+
BytesRestResponse response = new BytesRestResponse(RestStatus.OK, builder);
165+
builder.close();
166+
channel.sendResponse(response);
167+
} catch (final Exception exception) {
168+
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
163169
}
164-
builder.endArray();
170+
}, exception -> {
171+
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
172+
173+
}));
165174

166-
response = new BytesRestResponse(RestStatus.OK, builder);
167-
} catch (final Exception exception) {
168-
builder.startObject().field("error", exception.getMessage()).endObject();
169-
response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder);
170-
}
171-
builder.close();
172-
channel.sendResponse(response);
173175
};
174176
}
175177

@@ -181,43 +183,76 @@ private RestChannelConsumer handlePost(RestRequest request, NodeClient client) {
181183

182184
List<String> clusterPermissions = extractClusterPermissions(requestBody);
183185
List<ApiToken.IndexPermission> indexPermissions = extractIndexPermissions(requestBody);
184-
185-
String token = apiTokenRepository.createApiToken(
186-
(String) requestBody.get(NAME_FIELD),
187-
clusterPermissions,
188-
indexPermissions,
189-
(Long) requestBody.getOrDefault(EXPIRATION_FIELD, Instant.now().toEpochMilli() + TimeUnit.DAYS.toMillis(30))
186+
String name = (String) requestBody.get(NAME_FIELD);
187+
long expiration = (Long) requestBody.getOrDefault(
188+
EXPIRATION_FIELD,
189+
Instant.now().toEpochMilli() + TimeUnit.DAYS.toMillis(30)
190190
);
191191

192-
// Then trigger the update action
193-
ApiTokenUpdateRequest updateRequest = new ApiTokenUpdateRequest();
194-
client.execute(ApiTokenUpdateAction.INSTANCE, updateRequest, new ActionListener<ApiTokenUpdateResponse>() {
195-
@Override
196-
public void onResponse(ApiTokenUpdateResponse updateResponse) {
197-
try {
192+
// First check token count
193+
apiTokenRepository.getTokenCount(ActionListener.wrap(tokenCount -> {
194+
if (tokenCount >= 100) {
195+
sendErrorResponse(
196+
channel,
197+
RestStatus.TOO_MANY_REQUESTS,
198+
"Maximum limit of 100 API tokens reached. Please delete existing tokens before creating new ones."
199+
);
200+
return;
201+
}
202+
203+
apiTokenRepository.createApiToken(
204+
name,
205+
clusterPermissions,
206+
indexPermissions,
207+
expiration,
208+
wrapWithCacheRefresh(ActionListener.wrap(token -> {
198209
XContentBuilder builder = channel.newBuilder();
199210
builder.startObject();
200-
builder.field("Api Token: ", token);
211+
builder.field("token", token);
201212
builder.endObject();
202-
203-
BytesRestResponse response = new BytesRestResponse(RestStatus.OK, builder);
204-
channel.sendResponse(response);
205-
} catch (IOException e) {
206-
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to send response after token creation");
207-
}
208-
}
209-
210-
@Override
211-
public void onFailure(Exception e) {
212-
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to propagate token creation");
213-
}
214-
});
215-
} catch (final Exception exception) {
216-
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
213+
channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder));
214+
builder.close();
215+
216+
},
217+
createException -> sendErrorResponse(
218+
channel,
219+
RestStatus.INTERNAL_SERVER_ERROR,
220+
"Failed to create token: " + createException.getMessage()
221+
)
222+
), client)
223+
);
224+
},
225+
countException -> sendErrorResponse(
226+
channel,
227+
RestStatus.INTERNAL_SERVER_ERROR,
228+
"Failed to get token count: " + countException.getMessage()
229+
)
230+
));
231+
232+
} catch (Exception e) {
233+
sendErrorResponse(channel, RestStatus.BAD_REQUEST, "Invalid request: " + e.getMessage());
217234
}
218235
};
219236
}
220237

238+
private <T> ActionListener<T> wrapWithCacheRefresh(ActionListener<T> listener, NodeClient client) {
239+
return ActionListener.wrap(response -> {
240+
try {
241+
ApiTokenUpdateRequest updateRequest = new ApiTokenUpdateRequest();
242+
client.execute(
243+
ApiTokenUpdateAction.INSTANCE,
244+
updateRequest,
245+
ActionListener.wrap(
246+
updateResponse -> listener.onResponse(response),
247+
exception -> listener.onFailure(new ApiTokenException("Failed to refresh cache", exception))
248+
)
249+
);
250+
} catch (Exception e) {
251+
listener.onFailure(new ApiTokenException("Failed to refresh cache after operation", e));
252+
}
253+
}, listener::onFailure);
254+
}
255+
221256
/**
222257
* Extracts cluster permissions from the request body
223258
*/
@@ -303,37 +338,30 @@ private RestChannelConsumer handleDelete(RestRequest request, NodeClient client)
303338
final Map<String, Object> requestBody = request.contentOrSourceParamParser().map();
304339

305340
validateRequestParameters(requestBody);
306-
apiTokenRepository.deleteApiToken((String) requestBody.get(NAME_FIELD));
307-
308-
ApiTokenUpdateRequest updateRequest = new ApiTokenUpdateRequest();
309-
client.execute(ApiTokenUpdateAction.INSTANCE, updateRequest, new ActionListener<ApiTokenUpdateResponse>() {
310-
@Override
311-
public void onResponse(ApiTokenUpdateResponse updateResponse) {
312-
try {
313-
XContentBuilder builder = channel.newBuilder();
314-
builder.startObject();
315-
builder.field("message", "token " + requestBody.get(NAME_FIELD) + " deleted successfully.");
316-
builder.endObject();
317-
318-
BytesRestResponse response = new BytesRestResponse(RestStatus.OK, builder);
319-
channel.sendResponse(response);
320-
} catch (Exception e) {
321-
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to send response after token update");
322-
}
323-
}
324-
325-
@Override
326-
public void onFailure(Exception e) {
327-
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to propagate token deletion");
328-
}
329-
});
330-
} catch (final ApiTokenException exception) {
331-
sendErrorResponse(channel, RestStatus.NOT_FOUND, exception.getMessage());
341+
apiTokenRepository.deleteApiToken(
342+
(String) requestBody.get(NAME_FIELD),
343+
wrapWithCacheRefresh(ActionListener.wrap(ignored -> {
344+
XContentBuilder builder = channel.newBuilder();
345+
builder.startObject();
346+
builder.field("message", "Token " + requestBody.get(NAME_FIELD) + " deleted successfully.");
347+
builder.endObject();
348+
channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder));
349+
},
350+
deleteException -> sendErrorResponse(
351+
channel,
352+
RestStatus.INTERNAL_SERVER_ERROR,
353+
"Failed to delete token: " + deleteException.getMessage()
354+
)
355+
), client)
356+
);
332357
} catch (final Exception exception) {
333-
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
358+
RestStatus status = RestStatus.INTERNAL_SERVER_ERROR;
359+
if (exception instanceof ApiTokenException) {
360+
status = RestStatus.NOT_FOUND;
361+
}
362+
sendErrorResponse(channel, status, exception.getMessage());
334363
}
335364
};
336-
337365
}
338366

339367
private void sendErrorResponse(RestChannel channel, RestStatus status, String errorMessage) {

0 commit comments

Comments
 (0)