Skip to content

Commit

Permalink
Add to dynamic config model and expiration
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 13, 2024
1 parent dad767b commit 98d3847
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 23 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, settings));
handlers.addAll(
SecurityRestApiActions.getHandler(
settings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,38 @@ public class ApiToken implements ToXContent {
public static final String INDEX_PERMISSIONS_FIELD = "index_permissions";
public static final String INDEX_PATTERN_FIELD = "index_pattern";
public static final String ALLOWED_ACTIONS_FIELD = "allowed_actions";
public static final String EXPIRATION_FIELD = "expiration";

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

}

Expand Down Expand Up @@ -92,6 +96,7 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException {
List<String> clusterPermissions = new ArrayList<>();
List<IndexPermission> indexPermissions = new ArrayList<>();
Instant creationTime = null;
Long expiration = Long.MAX_VALUE;

XContentParser.Token token;
String currentFieldName = null;
Expand All @@ -110,6 +115,9 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException {
case CREATION_TIME_FIELD:
creationTime = Instant.ofEpochMilli(parser.longValue());
break;
case EXPIRATION_FIELD:
expiration = parser.longValue();
break;
}
} else if (token == XContentParser.Token.START_ARRAY) {
switch (currentFieldName) {
Expand Down Expand Up @@ -139,7 +147,7 @@ public static ApiToken fromXContent(XContentParser parser) throws IOException {
throw new IllegalArgumentException(CREATION_TIME_FIELD + " is required");
}

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

private static IndexPermission parseIndexPermission(XContentParser parser) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,21 @@
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;
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.NAME_FIELD;
Expand All @@ -52,8 +53,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, Settings settings) {
this.apiTokenRepository = new ApiTokenRepository(client, clusterService, settings);
}

@Override
Expand Down Expand Up @@ -121,7 +122,9 @@ private RestChannelConsumer handlePost(RestRequest request, NodeClient client) {
String token = apiTokenRepository.createApiToken(
(String) requestBody.get(NAME_FIELD),
clusterPermissions,
indexPermissions
indexPermissions,
(Long) requestBody.getOrDefault(EXPIRATION_FIELD, Long.MAX_VALUE)

);

builder.startObject();
Expand Down Expand Up @@ -224,6 +227,13 @@ void validateRequestParameters(Map<String, Object> requestBody) {
throw new IllegalArgumentException("Missing required parameter: " + NAME_FIELD);
}

if (requestBody.containsKey(EXPIRATION_FIELD)) {
Object permissions = requestBody.get(EXPIRATION_FIELD);
if (!(permissions instanceof Long)) {
throw new IllegalArgumentException(EXPIRATION_FIELD + " must be an long");
}
}

if (requestBody.containsKey(CLUSTER_PERMISSIONS_FIELD)) {
Object permissions = requestBody.get(CLUSTER_PERMISSIONS_FIELD);
if (!(permissions instanceof List)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,25 @@

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

public class ApiTokenRepository {
private final ApiTokenIndexHandler apiTokenIndexHandler;

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

public String createApiToken(String name, List<String> clusterPermissions, List<ApiToken.IndexPermission> indexPermissions) {
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
return apiTokenIndexHandler.indexTokenPayload(new ApiToken(name, "test-token", clusterPermissions, indexPermissions));
return apiTokenIndexHandler.indexTokenPayload(new ApiToken(name, "test-token", clusterPermissions, indexPermissions, expiration));
}

public void deleteApiToken(String name) throws ApiTokenException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public class SecurityTokenManager implements TokenManager {
private final ThreadPool threadPool;
private final UserService userService;

private JwtVendor jwtVendor = null;
private JwtVendor oboJwtVendor = null;
private JwtVendor apiTokenJwtVendor = null;
private ConfigModel configModel = null;

public SecurityTokenManager(final ClusterService cs, final ThreadPool threadPool, final UserService userService) {
Expand All @@ -67,11 +68,14 @@ public void onConfigModelChanged(final ConfigModel configModel) {
@Subscribe
public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) {
final Settings oboSettings = dcm.getDynamicOnBehalfOfSettings();
final Boolean enabled = oboSettings.getAsBoolean("enabled", false);
if (enabled) {
jwtVendor = createJwtVendor(oboSettings);
} else {
jwtVendor = null;
final Boolean oboEnabled = oboSettings.getAsBoolean("enabled", false);
if (oboEnabled) {
oboJwtVendor = createJwtVendor(oboSettings);
}
final Settings apiTokenSettings = dcm.getDynamicApiTokenSettings();
final Boolean apiTokenEnabled = apiTokenSettings.getAsBoolean("enabled", false);
if (apiTokenEnabled) {
apiTokenJwtVendor = createJwtVendor(apiTokenSettings);
}
}

Expand All @@ -86,7 +90,11 @@ JwtVendor createJwtVendor(final Settings settings) {
}

public boolean issueOnBehalfOfTokenAllowed() {
return jwtVendor != null && configModel != null;
return oboJwtVendor != null && configModel != null;
}

public boolean issueApiTokenAllowed() {
return apiTokenJwtVendor != null && configModel != null;
}

@Override
Expand Down Expand Up @@ -116,7 +124,7 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final
final Set<String> mappedRoles = configModel.mapSecurityRoles(user, callerAddress);

try {
return jwtVendor.createJwt(
return oboJwtVendor.createJwt(
cs.getClusterName().value(),
user.getName(),
claims.getAudience(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public abstract class DynamicConfigModel {

public abstract Settings getDynamicOnBehalfOfSettings();

public abstract Settings getDynamicApiTokenSettings();

protected final Map<String, String> authImplMap = new HashMap<>();

public DynamicConfigModel() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ public Settings getDynamicOnBehalfOfSettings() {
.build();
}

@Override
public Settings getDynamicApiTokenSettings() {
return Settings.builder()
.put(Settings.builder().loadFromSource(config.dynamic.api_token_settings.configAsJson(), XContentType.JSON).build())
.build();
}

private void buildAAA() {

final SortedSet<AuthDomain> restAuthDomains0 = new TreeSet<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +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();

@Override
public String toString() {
Expand All @@ -101,6 +102,8 @@ public String toString() {
+ authz
+ ", on_behalf_of="
+ on_behalf_of
+ ", api_tokens="
+ api_token_settings
+ "]";
}
}
Expand Down Expand Up @@ -495,4 +498,52 @@ public String toString() {
}
}

public static class ApiTokenSettings {
@JsonProperty("enabled")
private Boolean enabled = Boolean.FALSE;
@JsonProperty("signing_key")
private String signingKey;
@JsonProperty("encryption_key")
private String encryptionKey;

@JsonIgnore
public String configAsJson() {
try {
return DefaultObjectMapper.writeValueAsString(this, false);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

public Boolean getEnabled() {
return enabled;
}

public void setEnabled(Boolean oboEnabled) {
this.enabled = oboEnabled;
}

public String getSigningKey() {
return signingKey;
}

public void setSigningKey(String signingKey) {
this.signingKey = signingKey;
}

public String getEncryptionKey() {
return encryptionKey;
}

public void setEncryptionKey(String encryptionKey) {
this.encryptionKey = encryptionKey;
}

@Override
public String toString() {
return "ApiTokens [ enabled=" + enabled + ", signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]";
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ public void testIndexTokenStoresTokenPayload() {
"test-jti",
clusterPermissions,
indexPermissions,
Instant.now()
Instant.now(),
Long.MAX_VALUE
);

// Mock the index method with ActionListener
Expand Down Expand Up @@ -249,7 +250,8 @@ public void testGetTokenPayloads() throws IOException {
Arrays.asList("index1-*"),
Arrays.asList("read")
)),
Instant.now()
Instant.now(),
Long.MAX_VALUE
);

// Second token
Expand All @@ -261,7 +263,8 @@ public void testGetTokenPayloads() throws IOException {
Arrays.asList("index2-*"),
Arrays.asList("write")
)),
Instant.now()
Instant.now(),
Long.MAX_VALUE
);

// Convert tokens to XContent and create SearchHits
Expand Down

0 comments on commit 98d3847

Please sign in to comment.