Skip to content

Commit

Permalink
Inject security token manager and now test jti gets indexed
Browse files Browse the repository at this point in the history
Signed-off-by: Derek Ho <[email protected]>
  • Loading branch information
derek-ho committed Dec 17, 2024
1 parent fddd37c commit ae4e8f8
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ public List<RestHandler> getRestHandlers(
)
);
handlers.add(new CreateOnBehalfOfTokenAction(tokenManager));
handlers.add(new ApiTokenAction(cs, threadPool, localClient));
handlers.add(new ApiTokenAction(cs, localClient, tokenManager));
handlers.addAll(
SecurityRestApiActions.getHandler(
settings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnore;

import org.opensearch.core.xcontent.ToXContent;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.core.xcontent.XContentParser;
Expand All @@ -31,47 +33,47 @@ public class ApiToken implements ToXContent {
public static final String EXPIRATION_FIELD = "expiration";

private String name;
private final String jti;
private String jti;
private final Instant creationTime;
private List<String> clusterPermissions;
private List<IndexPermission> indexPermissions;
private final long expiration;

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

public ApiToken(String name, String jti, List<String> clusterPermissions, List<IndexPermission> indexPermissions) {
public ApiToken(String name, 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 name,
String jti,
List<String> clusterPermissions,
List<IndexPermission> indexPermissions,
Instant creationTime,
Long expiration
) {
this.name = name;
this.jti = jti;
this.clusterPermissions = clusterPermissions;
this.indexPermissions = indexPermissions;
this.creationTime = creationTime;
this.expiration = expiration;

}

public void setJti(String jti) {
this.jti = jti;
}

public static class IndexPermission implements ToXContent {
private final List<String> indexPatterns;
private final List<String> allowedActions;
Expand Down Expand Up @@ -118,7 +120,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
*/
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;
Expand All @@ -135,9 +136,6 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException {
case NAME_FIELD:
name = parser.text();
break;
case JTI_FIELD:
jti = parser.text();
break;
case CREATION_TIME_FIELD:
creationTime = Instant.ofEpochMilli(parser.longValue());
break;
Expand All @@ -163,7 +161,7 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException {
}
}

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

private static IndexPermission parseIndexPermission(XContentParser parser) throws IOException {
Expand Down Expand Up @@ -202,6 +200,11 @@ public void setName(String name) {
this.name = name;
}

public Long getExpiration() {
return expiration;
}

@JsonIgnore
public String getJti() {
return jti;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import org.opensearch.rest.BytesRestResponse;
import org.opensearch.rest.RestHandler;
import org.opensearch.rest.RestRequest;
import org.opensearch.threadpool.ThreadPool;
import org.opensearch.security.identity.SecurityTokenManager;

import static org.opensearch.rest.RestRequest.Method.DELETE;
import static org.opensearch.rest.RestRequest.Method.GET;
Expand All @@ -55,8 +55,8 @@ public class ApiTokenAction extends BaseRestHandler {
)
);

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

@Override
Expand Down Expand Up @@ -96,6 +96,9 @@ private RestChannelConsumer handleGet(RestRequest request, NodeClient client) {
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();
}
builder.endArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@
import org.opensearch.client.Client;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.index.IndexNotFoundException;
import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken;
import org.opensearch.security.identity.SecurityTokenManager;

public class ApiTokenRepository {
private final ApiTokenIndexHandler apiTokenIndexHandler;
private final SecurityTokenManager securityTokenManager;

public ApiTokenRepository(Client client, ClusterService clusterService) {
public ApiTokenRepository(Client client, ClusterService clusterService, SecurityTokenManager tokenManager) {
apiTokenIndexHandler = new ApiTokenIndexHandler(client, clusterService);
securityTokenManager = tokenManager;
}

public String createApiToken(
Expand All @@ -34,7 +38,10 @@ 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.indexTokenMetadata(new ApiToken(name, "test-token", clusterPermissions, indexPermissions, expiration));
ApiToken apiToken = new ApiToken(name, clusterPermissions, indexPermissions, expiration);
ExpiringBearerAuthToken token = securityTokenManager.issueApiToken(apiToken);
apiToken.setJti(token.getCompleteToken());
return apiTokenIndexHandler.indexTokenMetadata(apiToken);
}

public void deleteApiToken(String name) throws ApiTokenException, IndexNotFoundException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
*/
package org.opensearch.security.authtoken.jwt;

import java.time.Duration;
import java.time.Instant;
import java.util.Date;

import org.opensearch.identity.tokens.BearerAuthToken;
Expand All @@ -26,6 +28,13 @@ public ExpiringBearerAuthToken(final String serializedToken, final String subjec
this.expiresInSeconds = expiresInSeconds;
}

public ExpiringBearerAuthToken(final String serializedToken, final String subject, final Date expiry) {
super(serializedToken);
this.subject = subject;
this.expiry = expiry;
this.expiresInSeconds = Duration.between(Instant.now(), expiry.toInstant()).getSeconds();
}

public String getSubject() {
return subject;
}
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

package org.opensearch.security.authtoken.jwt;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.ParseException;
import java.util.Base64;
import java.util.Date;
Expand All @@ -24,6 +26,7 @@
import org.opensearch.OpenSearchException;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.settings.Settings;
import org.opensearch.security.action.apitokens.ApiToken;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
Expand Down Expand Up @@ -148,4 +151,54 @@ public ExpiringBearerAuthToken createJwt(

return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime, expirySeconds);
}

@SuppressWarnings("removal")
public ExpiringBearerAuthToken createJwt(
final String issuer,
final String subject,
final String audience,
final long expiration,
final List<String> clusterPermissions,
final List<ApiToken.IndexPermission> indexPermissions
) throws JOSEException, ParseException {
final long currentTimeMs = timeProvider.getAsLong();
final Date now = new Date(currentTimeMs);

final JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder();
claimsBuilder.issuer(issuer);
claimsBuilder.issueTime(now);
claimsBuilder.subject(subject);
claimsBuilder.audience(audience);
claimsBuilder.notBeforeTime(now);

final Date expiryTime = new Date(expiration);
claimsBuilder.expirationTime(expiryTime);

if (clusterPermissions != null) {
final String listOfClusterPermissions = String.join(",", clusterPermissions);
claimsBuilder.claim("cp", encryptionDecryptionUtil.encrypt(listOfClusterPermissions));
}

if (indexPermissions != null) {
final String listOfIndexPermissions = String.join(", ", indexPermissions.toString());
claimsBuilder.claim("ip", encryptionDecryptionUtil.encrypt(listOfIndexPermissions));
}

final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build();

final SignedJWT signedJwt = AccessController.doPrivileged(
(PrivilegedAction<SignedJWT>) () -> new SignedJWT(header, claimsBuilder.build())
);

// Sign the JWT so it can be serialized
signedJwt.sign(signer);

if (logger.isDebugEnabled()) {
logger.debug(
"Created JWT: " + signedJwt.serialize() + "\n" + signedJwt.getHeader().toJSONObject() + "\n" + signedJwt.getJWTClaimsSet()
);
}

return new ExpiringBearerAuthToken(signedJwt.serialize(), subject, expiryTime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.opensearch.identity.tokens.AuthToken;
import org.opensearch.identity.tokens.OnBehalfOfClaims;
import org.opensearch.identity.tokens.TokenManager;
import org.opensearch.security.action.apitokens.ApiToken;
import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken;
import org.opensearch.security.authtoken.jwt.JwtVendor;
import org.opensearch.security.securityconf.ConfigModel;
Expand Down Expand Up @@ -139,6 +140,27 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final
}
}

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");
}

try {
return apiTokenJwtVendor.createJwt(
cs.getClusterName().value(),
apiToken.getName(),
apiToken.getName(),
apiToken.getExpiration(),
apiToken.getClusterPermissions(),
apiToken.getIndexPermissions()
);
} catch (final Exception ex) {
logger.error("Error creating OnBehalfOfToken for " + user.getName(), ex);
throw new OpenSearchSecurityException("Unable to generate OnBehalfOfToken");
}
}

@Override
public AuthToken issueServiceAccountToken(final String serviceId) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ public Settings getDynamicOnBehalfOfSettings() {
@Override
public Settings getDynamicApiTokenSettings() {
return Settings.builder()
.put(Settings.builder().loadFromSource(config.dynamic.api_token_settings.configAsJson(), XContentType.JSON).build())
.put(Settings.builder().loadFromSource(config.dynamic.api_tokens.configAsJson(), XContentType.JSON).build())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public static class Dynamic {
public String transport_userrname_attribute;
public boolean do_not_fail_on_forbidden_empty;
public OnBehalfOfSettings on_behalf_of = new OnBehalfOfSettings();
public ApiTokenSettings api_token_settings = new ApiTokenSettings();
public ApiTokenSettings api_tokens = new ApiTokenSettings();

@Override
public String toString() {
Expand All @@ -103,7 +103,7 @@ public String toString() {
+ ", on_behalf_of="
+ on_behalf_of
+ ", api_tokens="
+ api_token_settings
+ api_tokens
+ "]";
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class ApiTokenActionTest {

private final ApiTokenAction apiTokenAction = new ApiTokenAction(null, null, null);

@Test
public void testCreateIndexPermission() {
Map<String, Object> validPermission = new HashMap<>();
validPermission.put("index_pattern", "test-*");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,12 @@ public void testIndexTokenStoresTokenPayload() {
);
ApiToken token = new ApiToken(
"test-token-description",
"test-jti",
clusterPermissions,
indexPermissions,
Instant.now(),
Long.MAX_VALUE
);
token.setJti("test-token-jti");

// Mock the index method with ActionListener
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -230,8 +230,8 @@ public void testIndexTokenStoresTokenPayload() {
// verify contents
String source = capturedRequest.source().utf8ToString();
assertThat(source, containsString("test-token-description"));
assertThat(source, containsString("test-jti"));
assertThat(source, containsString("cluster:admin/something"));
assertThat(source, containsString("test-token-jti"));
assertThat(source, containsString("test-index-*"));
}

Expand All @@ -245,7 +245,6 @@ public void testGetTokenPayloads() throws IOException {
// First token
ApiToken token1 = new ApiToken(
"token1-description",
"jti1",
Arrays.asList("cluster:admin/something"),
Arrays.asList(new ApiToken.IndexPermission(
Arrays.asList("index1-*"),
Expand All @@ -258,7 +257,6 @@ public void testGetTokenPayloads() throws IOException {
// Second token
ApiToken token2 = new ApiToken(
"token2-description",
"jti2",
Arrays.asList("cluster:admin/other"),
Arrays.asList(new ApiToken.IndexPermission(
Arrays.asList("index2-*"),
Expand Down Expand Up @@ -297,11 +295,9 @@ public void testGetTokenPayloads() throws IOException {
assertThat(resultTokens.containsKey("token2-description"), is(true));

ApiToken resultToken1 = resultTokens.get("token1-description");
assertThat(resultToken1.getJti(), equalTo("jti1"));
assertThat(resultToken1.getClusterPermissions(), contains("cluster:admin/something"));

ApiToken resultToken2 = resultTokens.get("token2-description");
assertThat(resultToken2.getJti(), equalTo("jti2"));
assertThat(resultToken2.getClusterPermissions(), contains("cluster:admin/other"));
}

Expand Down
Loading

0 comments on commit ae4e8f8

Please sign in to comment.