Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change API token index actions to use action listeners and limit to 100 tokens outstanding #5147

Draft
wants to merge 4 commits into
base: feature/api-tokens
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -22,7 +22,7 @@

public class ApiToken implements ToXContent {
public static final String NAME_FIELD = "name";
public static final String CREATION_TIME_FIELD = "creation_time";
public static final String ISSUED_AT_FIELD = "iat";
public static final String CLUSTER_PERMISSIONS_FIELD = "cluster_permissions";
public static final String INDEX_PERMISSIONS_FIELD = "index_permissions";
public static final String INDEX_PATTERN_FIELD = "index_pattern";
Expand Down Expand Up @@ -149,7 +149,7 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException {
case NAME_FIELD:
name = parser.text();
break;
case CREATION_TIME_FIELD:
case ISSUED_AT_FIELD:
creationTime = Instant.ofEpochMilli(parser.longValue());
break;
case EXPIRATION_FIELD:
Expand Down Expand Up @@ -227,7 +227,7 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params
xContentBuilder.field(NAME_FIELD, name);
xContentBuilder.field(CLUSTER_PERMISSIONS_FIELD, clusterPermissions);
xContentBuilder.field(INDEX_PERMISSIONS_FIELD, indexPermissions);
xContentBuilder.field(CREATION_TIME_FIELD, creationTime.toEpochMilli());
xContentBuilder.field(ISSUED_AT_FIELD, creationTime.toEpochMilli());
xContentBuilder.endObject();
return xContentBuilder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.opensearch.client.node.NodeClient;
import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.settings.Settings;
Expand All @@ -48,16 +47,17 @@
import org.opensearch.security.ssl.transport.PrincipalExtractor;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.threadpool.ThreadPool;
import org.opensearch.transport.client.node.NodeClient;

import static org.opensearch.rest.RestRequest.Method.DELETE;
import static org.opensearch.rest.RestRequest.Method.GET;
import static org.opensearch.rest.RestRequest.Method.POST;
import static org.opensearch.security.action.apitokens.ApiToken.ALLOWED_ACTIONS_FIELD;
import static org.opensearch.security.action.apitokens.ApiToken.CLUSTER_PERMISSIONS_FIELD;
import static org.opensearch.security.action.apitokens.ApiToken.CREATION_TIME_FIELD;
import static org.opensearch.security.action.apitokens.ApiToken.EXPIRATION_FIELD;
import static org.opensearch.security.action.apitokens.ApiToken.INDEX_PATTERN_FIELD;
import static org.opensearch.security.action.apitokens.ApiToken.INDEX_PERMISSIONS_FIELD;
import static org.opensearch.security.action.apitokens.ApiToken.ISSUED_AT_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.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;
Expand Down Expand Up @@ -146,30 +146,32 @@

private RestChannelConsumer handleGet(RestRequest request, NodeClient client) {
return channel -> {
final XContentBuilder builder = channel.newBuilder();
BytesRestResponse response;
try {
Map<String, ApiToken> tokens = apiTokenRepository.getApiTokens();

builder.startArray();
for (ApiToken token : tokens.values()) {
builder.startObject();
builder.field(NAME_FIELD, token.getName());
builder.field(CREATION_TIME_FIELD, token.getCreationTime().toEpochMilli());
builder.field(EXPIRATION_FIELD, token.getExpiration());
builder.field(CLUSTER_PERMISSIONS_FIELD, token.getClusterPermissions());
builder.field(INDEX_PERMISSIONS_FIELD, token.getIndexPermissions());
builder.endObject();
apiTokenRepository.getApiTokens(ActionListener.wrap(tokens -> {

Check warning on line 149 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L149 was not covered by tests
try {
XContentBuilder builder = channel.newBuilder();
builder.startArray();

Check warning on line 152 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L151-L152

Added lines #L151 - L152 were not covered by tests
for (ApiToken token : tokens.values()) {
builder.startObject();
builder.field(NAME_FIELD, token.getName());
builder.field(ISSUED_AT_FIELD, token.getCreationTime().toEpochMilli());
builder.field(EXPIRATION_FIELD, token.getExpiration());
builder.field(CLUSTER_PERMISSIONS_FIELD, token.getClusterPermissions());
builder.field(INDEX_PERMISSIONS_FIELD, token.getIndexPermissions());
builder.endObject();
}
builder.endArray();

Check warning on line 162 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L154-L162

Added lines #L154 - L162 were not covered by tests

BytesRestResponse response = new BytesRestResponse(RestStatus.OK, builder);
builder.close();
channel.sendResponse(response);
} catch (final Exception exception) {
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage());

Check warning on line 168 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L164-L168

Added lines #L164 - L168 were not covered by tests
}
builder.endArray();
}, exception -> {
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage());

Check warning on line 171 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L170-L171

Added lines #L170 - L171 were not covered by tests

}));

Check warning on line 173 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L173 was not covered by tests

response = new BytesRestResponse(RestStatus.OK, builder);
} catch (final Exception exception) {
builder.startObject().field("error", exception.getMessage()).endObject();
response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder);
}
builder.close();
channel.sendResponse(response);
};
}

Expand All @@ -181,43 +183,76 @@

List<String> clusterPermissions = extractClusterPermissions(requestBody);
List<ApiToken.IndexPermission> indexPermissions = extractIndexPermissions(requestBody);

String token = apiTokenRepository.createApiToken(
(String) requestBody.get(NAME_FIELD),
clusterPermissions,
indexPermissions,
(Long) requestBody.getOrDefault(EXPIRATION_FIELD, Instant.now().toEpochMilli() + TimeUnit.DAYS.toMillis(30))
String name = (String) requestBody.get(NAME_FIELD);
long expiration = (Long) requestBody.getOrDefault(

Check warning on line 187 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L186-L187

Added lines #L186 - L187 were not covered by tests
EXPIRATION_FIELD,
Instant.now().toEpochMilli() + TimeUnit.DAYS.toMillis(30)

Check warning on line 189 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L189 was not covered by tests
);

// Then trigger the update action
ApiTokenUpdateRequest updateRequest = new ApiTokenUpdateRequest();
client.execute(ApiTokenUpdateAction.INSTANCE, updateRequest, new ActionListener<ApiTokenUpdateResponse>() {
@Override
public void onResponse(ApiTokenUpdateResponse updateResponse) {
try {
// First check token count
apiTokenRepository.getTokenCount(ActionListener.wrap(tokenCount -> {

Check warning on line 193 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L193 was not covered by tests
if (tokenCount >= 100) {
sendErrorResponse(

Check warning on line 195 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L195 was not covered by tests
channel,
RestStatus.TOO_MANY_REQUESTS,
"Maximum limit of 100 API tokens reached. Please delete existing tokens before creating new ones."
);
return;

Check warning on line 200 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L200 was not covered by tests
}

apiTokenRepository.createApiToken(

Check warning on line 203 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L203 was not covered by tests
name,
clusterPermissions,
indexPermissions,
expiration,
wrapWithCacheRefresh(ActionListener.wrap(token -> {

Check warning on line 208 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L207-L208

Added lines #L207 - L208 were not covered by tests
XContentBuilder builder = channel.newBuilder();
builder.startObject();
builder.field("Api Token: ", token);
builder.field("token", token);

Check warning on line 211 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L211 was not covered by tests
builder.endObject();

BytesRestResponse response = new BytesRestResponse(RestStatus.OK, builder);
channel.sendResponse(response);
} catch (IOException e) {
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to send response after token creation");
}
}

@Override
public void onFailure(Exception e) {
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to propagate token creation");
}
});
} catch (final Exception exception) {
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder));
builder.close();

Check warning on line 214 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L213-L214

Added lines #L213 - L214 were not covered by tests

},
createException -> sendErrorResponse(

Check warning on line 217 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L216-L217

Added lines #L216 - L217 were not covered by tests
channel,
RestStatus.INTERNAL_SERVER_ERROR,
"Failed to create token: " + createException.getMessage()

Check warning on line 220 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L220 was not covered by tests
)
), client)
);
},
countException -> sendErrorResponse(

Check warning on line 225 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L224-L225

Added lines #L224 - L225 were not covered by tests
channel,
RestStatus.INTERNAL_SERVER_ERROR,
"Failed to get token count: " + countException.getMessage()

Check warning on line 228 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L228 was not covered by tests
)
));

} catch (Exception e) {
sendErrorResponse(channel, RestStatus.BAD_REQUEST, "Invalid request: " + e.getMessage());

Check warning on line 233 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L232-L233

Added lines #L232 - L233 were not covered by tests
}
};
}

private <T> ActionListener<T> wrapWithCacheRefresh(ActionListener<T> listener, NodeClient client) {
return ActionListener.wrap(response -> {

Check warning on line 239 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L239 was not covered by tests
try {
ApiTokenUpdateRequest updateRequest = new ApiTokenUpdateRequest();
client.execute(

Check warning on line 242 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L241-L242

Added lines #L241 - L242 were not covered by tests
ApiTokenUpdateAction.INSTANCE,
updateRequest,
ActionListener.wrap(
updateResponse -> listener.onResponse(response),
exception -> listener.onFailure(new ApiTokenException("Failed to refresh cache", exception))

Check warning on line 247 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L245-L247

Added lines #L245 - L247 were not covered by tests
)
);
} catch (Exception e) {
listener.onFailure(new ApiTokenException("Failed to refresh cache after operation", e));
}
}, listener::onFailure);

Check warning on line 253 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L250-L253

Added lines #L250 - L253 were not covered by tests
}

/**
* Extracts cluster permissions from the request body
*/
Expand Down Expand Up @@ -303,37 +338,30 @@
final Map<String, Object> requestBody = request.contentOrSourceParamParser().map();

validateRequestParameters(requestBody);
apiTokenRepository.deleteApiToken((String) requestBody.get(NAME_FIELD));

ApiTokenUpdateRequest updateRequest = new ApiTokenUpdateRequest();
client.execute(ApiTokenUpdateAction.INSTANCE, updateRequest, new ActionListener<ApiTokenUpdateResponse>() {
@Override
public void onResponse(ApiTokenUpdateResponse updateResponse) {
try {
XContentBuilder builder = channel.newBuilder();
builder.startObject();
builder.field("message", "token " + requestBody.get(NAME_FIELD) + " deleted successfully.");
builder.endObject();

BytesRestResponse response = new BytesRestResponse(RestStatus.OK, builder);
channel.sendResponse(response);
} catch (Exception e) {
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to send response after token update");
}
}

@Override
public void onFailure(Exception e) {
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, "Failed to propagate token deletion");
}
});
} catch (final ApiTokenException exception) {
sendErrorResponse(channel, RestStatus.NOT_FOUND, exception.getMessage());
apiTokenRepository.deleteApiToken(
(String) requestBody.get(NAME_FIELD),
wrapWithCacheRefresh(ActionListener.wrap(ignored -> {
XContentBuilder builder = channel.newBuilder();
builder.startObject();
builder.field("message", "Token " + requestBody.get(NAME_FIELD) + " deleted successfully.");
builder.endObject();
channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder));
},
deleteException -> sendErrorResponse(

Check warning on line 350 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java#L341-L350

Added lines #L341 - L350 were not covered by tests
channel,
RestStatus.INTERNAL_SERVER_ERROR,
"Failed to delete token: " + deleteException.getMessage()

Check warning on line 353 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L353 was not covered by tests
)
), client)
);
} catch (final Exception exception) {
sendErrorResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
RestStatus status = RestStatus.INTERNAL_SERVER_ERROR;

Check warning on line 358 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L358 was not covered by tests
if (exception instanceof ApiTokenException) {
status = RestStatus.NOT_FOUND;

Check warning on line 360 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L360 was not covered by tests
}
sendErrorResponse(channel, status, exception.getMessage());

Check warning on line 362 in src/main/java/org/opensearch/security/action/apitokens/ApiTokenAction.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L362 was not covered by tests
}
};

}

private void sendErrorResponse(RestChannel channel, RestStatus status, String errorMessage) {
Expand Down
Loading
Loading