Skip to content
Merged
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 @@ -119,21 +119,17 @@ public class RcsCcsCommonYamlTestSuiteIT extends ESClientYamlSuiteTestCase {

private static Map<String, Object> createCrossClusterAccessApiKey() throws IOException {
assert fulfillingCluster != null;
final var createApiKeyRequest = new Request("POST", "/_security/api_key");
final var createApiKeyRequest = new Request("POST", "/_security/cross_cluster/api_key");
createApiKeyRequest.setJsonEntity("""
{
"name": "cross_cluster_access_key",
"role_descriptors": {
"role": {
"cluster": ["cross_cluster_search"],
"index": [
"access": {
"search": [
{
"names": ["*"],
"privileges": ["read", "read_cross_cluster"],
"allow_restricted_indices": true
}
]
}
}
}""");
createApiKeyRequest.setOptions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.XContentParserUtils;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ObjectParser;
Expand All @@ -28,6 +29,8 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
Expand All @@ -51,10 +54,25 @@ public static Type parse(String value) {
return switch (value.toLowerCase(Locale.ROOT)) {
case "rest" -> REST;
case "cross_cluster" -> CROSS_CLUSTER;
default -> throw new IllegalArgumentException("unknown API key type [" + value + "]");
default -> throw new IllegalArgumentException(
"invalid API key type ["
+ value
+ "] expected one of ["
+ Stream.of(values()).map(Type::value).collect(Collectors.joining(","))
+ "]"
);
};
}

public static Type fromXContent(XContentParser parser) throws IOException {
XContentParser.Token token = parser.currentToken();
if (token == null) {
token = parser.nextToken();
}
XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_STRING, token, parser);
return parse(parser.text());
}

public String value() {
return name().toLowerCase(Locale.ROOT);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import org.elasticsearch.core.Assertions;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

Expand Down Expand Up @@ -95,4 +97,16 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(id, name, expiration, metadata, roleDescriptors, refreshPolicy);
}

public static CreateCrossClusterApiKeyRequest withNameAndAccess(String name, String access) throws IOException {
return new CreateCrossClusterApiKeyRequest(
name,
CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(
JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, access),
null
),
null,
null
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public final class AuthenticationField {
public static final String API_KEY_CREATOR_REALM_TYPE = "_security_api_key_creator_realm_type";
public static final String API_KEY_ID_KEY = "_security_api_key_id";
public static final String API_KEY_NAME_KEY = "_security_api_key_name";
public static final String API_KEY_TYPE_KEY = "_security_api_key_type";
public static final String API_KEY_METADATA_KEY = "_security_api_key_metadata";
public static final String API_KEY_ROLE_DESCRIPTORS_KEY = "_security_api_key_role_descriptors";
public static final String API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY = "_security_api_key_limited_by_role_descriptors";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.util.ArrayUtils;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xpack.core.security.action.apikey.ApiKey;
import org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo.RoleDescriptorsBytes;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.core.security.authz.store.RoleReference;
Expand All @@ -26,6 +27,7 @@
import java.util.Map;
import java.util.Objects;

import static org.elasticsearch.transport.RemoteClusterPortSettings.TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY_CCR;
import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY;
Expand Down Expand Up @@ -246,11 +248,14 @@ private RoleReferenceIntersection buildRoleReferencesForApiKey() {
return buildRolesReferenceForApiKeyBwc();
}
final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY);
assert ApiKey.Type.REST == getApiKeyType() : "only a REST API key should have its role built here";

final BytesReference roleDescriptorsBytes = (BytesReference) metadata.get(API_KEY_ROLE_DESCRIPTORS_KEY);
final BytesReference limitedByRoleDescriptorsBytes = getLimitedByRoleDescriptorsBytes();
if (roleDescriptorsBytes == null && limitedByRoleDescriptorsBytes == null) {
throw new ElasticsearchSecurityException("no role descriptors found for API key");
}

final RoleReference.ApiKeyRoleReference limitedByRoleReference = new RoleReference.ApiKeyRoleReference(
apiKeyId,
limitedByRoleDescriptorsBytes,
Expand All @@ -265,7 +270,23 @@ private RoleReferenceIntersection buildRoleReferencesForApiKey() {
);
}

// Package private for testing
RoleReference.ApiKeyRoleReference buildRoleReferenceForCrossClusterApiKey() {
assert version.onOrAfter(TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY_CCR);
final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY);
assert ApiKey.Type.CROSS_CLUSTER == getApiKeyType() : "cross cluster access must use cross-cluster API keys";
final BytesReference roleDescriptorsBytes = (BytesReference) metadata.get(API_KEY_ROLE_DESCRIPTORS_KEY);
if (roleDescriptorsBytes == null) {
throw new ElasticsearchSecurityException("no role descriptors found for API key");
}
final BytesReference limitedByRoleDescriptorsBytes = (BytesReference) metadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY);
assert isEmptyRoleDescriptorsBytes(limitedByRoleDescriptorsBytes)
: "cross cluster API keys must have empty limited-by role descriptors";
return new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, RoleReference.ApiKeyRoleType.ASSIGNED);
}

private RoleReferenceIntersection buildRoleReferencesForCrossClusterAccess() {
assert ApiKey.Type.CROSS_CLUSTER == getApiKeyType() : "cross cluster access must use cross-cluster API keys";
final List<RoleReference> roleReferences = new ArrayList<>(4);
@SuppressWarnings("unchecked")
final var crossClusterAccessRoleDescriptorsBytes = (List<RoleDescriptorsBytes>) metadata.get(
Expand All @@ -292,7 +313,7 @@ private RoleReferenceIntersection buildRoleReferencesForCrossClusterAccess() {
roleReferences.add(new RoleReference.CrossClusterAccessRoleReference(innerUser.principal(), roleDescriptorsBytes));
}
}
roleReferences.addAll(buildRoleReferencesForApiKey().getRoleReferences());
roleReferences.add(buildRoleReferenceForCrossClusterApiKey());
return new RoleReferenceIntersection(List.copyOf(roleReferences));
}

Expand Down Expand Up @@ -342,6 +363,8 @@ private Map<String, Object> getRoleDescriptorMap(String key) {
);

private BytesReference getLimitedByRoleDescriptorsBytes() {
assert ApiKey.Type.REST == getApiKeyType()
: "bug fixing for fleet-server limited-by role descriptors applies only to REST API keys";
final BytesReference bytesReference = (BytesReference) metadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY);
// Unfortunate BWC bug fix code
if (bytesReference.length() == 2 && "{}".equals(bytesReference.utf8ToString())) {
Expand All @@ -352,4 +375,16 @@ private BytesReference getLimitedByRoleDescriptorsBytes() {
}
return bytesReference;
}

private ApiKey.Type getApiKeyType() {
final String typeString = (String) metadata.get(AuthenticationField.API_KEY_TYPE_KEY);
assert (typeString != null) || version.before(TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY_CCR)
: "API key type must be non-null except for versions older than " + TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY_CCR;

// A null type string can only be for the REST type because it is not possible to
// create cross-cluster API keys for mixed cluster with old nodes.
// It is also not possible to send such an API key to an old node because it can only be
// used via the dedicated remote cluster port which means the node must be of a newer version.
return typeString == null ? ApiKey.Type.REST : ApiKey.Type.parse(typeString);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.Objects;

import static org.elasticsearch.xpack.core.security.authz.RoleDescriptorTests.randomUniquelyNamedRoleDescriptors;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.is;
Expand Down Expand Up @@ -108,6 +109,27 @@ public void testXContent() throws IOException {
}
}

public void testParseApiKeyType() throws IOException {
assertThat(parseTypeString(randomFrom("rest", "REST", "Rest")), is(ApiKey.Type.REST));
assertThat(parseTypeString(randomFrom("cross_cluster", "CROSS_CLUSTER", "Cross_Cluster")), is(ApiKey.Type.CROSS_CLUSTER));

final IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
() -> parseTypeString(randomAlphaOfLengthBetween(3, 20))
);
assertThat(e.getMessage(), containsString("invalid API key type"));
}

private ApiKey.Type parseTypeString(String typeString) throws IOException {
if (randomBoolean()) {
return ApiKey.Type.parse(typeString);
} else {
return ApiKey.Type.fromXContent(
JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, "\"" + typeString + "\"")
);
}
}

@SuppressWarnings("unchecked")
public static Map<String, Object> randomMetadata() {
return randomFrom(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.XContentTestUtils;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.action.apikey.ApiKey;
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
Expand Down Expand Up @@ -381,6 +382,25 @@ public AuthenticationTestBuilder apiKey(String apiKeyId) {
realmRef = null;
candidateAuthenticationTypes = EnumSet.of(AuthenticationType.API_KEY);
metadata.put(AuthenticationField.API_KEY_ID_KEY, Objects.requireNonNull(apiKeyId));
metadata.put(AuthenticationField.API_KEY_TYPE_KEY, ApiKey.Type.REST.value());
return this;
}

public AuthenticationTestBuilder crossClusterApiKey(String apiKeyId) {
apiKey(apiKeyId);
candidateAuthenticationTypes = EnumSet.of(AuthenticationType.API_KEY);
metadata.put(AuthenticationField.API_KEY_TYPE_KEY, ApiKey.Type.CROSS_CLUSTER.value());
metadata.put(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, new BytesArray("""
{
"cross_cluster": {
"cluster": ["cross_cluster_search", "cross_cluster_replication"],
"indices": [
{ "names":["logs*"], "privileges":["read","read_cross_cluster","view_index_metadata"] },
{ "names":["archive*"],"privileges":["cross_cluster_replication","cross_cluster_replication_internal"] }
]
}
}"""));
metadata.put(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, new BytesArray("{}"));
return this;
}

Expand Down Expand Up @@ -435,7 +455,7 @@ public AuthenticationTestBuilder crossClusterAccess(
if (authenticatingAuthentication != null) {
throw new IllegalArgumentException("cannot use cross cluster access authentication as run-as target");
}
apiKey(crossClusterAccessApiKeyId);
crossClusterApiKey(crossClusterAccessApiKeyId);
this.crossClusterAccessSubjectInfo = Objects.requireNonNull(crossClusterAccessSubjectInfo);
return this;
}
Expand Down
Loading