Skip to content

Commit

Permalink
Access API Keys across the security domain (#84704)
Browse files Browse the repository at this point in the history
This PR permits authenticating users (but not API Key) with only the
`manage_own_api_key` privilege (so not `manage_api_key` or higher) to
get, invalidate and query API Keys, that have been created by any of the
realms under the same security domain. Previously, users with the
`manage_own_api_key` permission, had access to API Keys only if they've
been created under the same realm (the security domain was not
considered).
  • Loading branch information
albertzaharovits committed Mar 9, 2022
1 parent c672eca commit 32cc055
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 53 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/84704.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 84704
summary: API Key APIs with Security Domain
area: Authorization
type: feature
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,8 @@ public String toString() {
}
}

static SearchResponse empty(Supplier<Long> tookInMillisSupplier, Clusters clusters) {
// public for tests
public static SearchResponse empty(Supplier<Long> tookInMillisSupplier, Clusters clusters) {
SearchHits searchHits = new SearchHits(new SearchHit[0], new TotalHits(0L, TotalHits.Relation.EQUAL_TO), Float.NaN);
InternalSearchResponse internalSearchResponse = new InternalSearchResponse(
searchHits,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyRequest;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
import org.elasticsearch.xpack.core.security.authc.RealmDomain;
import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission;
import org.elasticsearch.xpack.core.security.support.Automatons;

Expand Down Expand Up @@ -122,9 +123,15 @@ private boolean checkIfUserIsOwnerOfApiKeys(
} else if (ownedByAuthenticatedUser) {
return true;
} else if (Strings.hasText(username) && Strings.hasText(realmName)) {
final String sourceUserPrincipal = authentication.getUser().principal();
final String sourceRealmName = authentication.getSourceRealm().getName();
return username.equals(sourceUserPrincipal) && realmName.equals(sourceRealmName);
if (false == username.equals(authentication.getUser().principal())) {
return false;
}
RealmDomain domain = authentication.getSourceRealm().getDomain();
if (domain != null) {
return domain.realms().stream().anyMatch(realmIdentifier -> realmName.equals(realmIdentifier.getName()));
} else {
return realmName.equals(authentication.getSourceRealm().getName());
}
}
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,10 +567,17 @@ public static Authentication randomAuthentication(User user, RealmRef realmRef)
realmRef = randomRealmRef(false);
}
final Version version = VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.CURRENT);
final AuthenticationType authenticationType = randomValueOtherThan(
AuthenticationType.API_KEY,
() -> randomFrom(AuthenticationType.values())
);
final AuthenticationType authenticationType;
if (realmRef.getDomain() != null) {
authenticationType = randomValueOtherThanMany(
authType -> authType == AuthenticationType.API_KEY
|| authType == AuthenticationType.INTERNAL
|| authType == AuthenticationType.ANONYMOUS,
() -> randomFrom(AuthenticationType.values())
);
} else {
authenticationType = randomValueOtherThan(AuthenticationType.API_KEY, () -> randomFrom(AuthenticationType.values()));
}
final Map<String, Object> metadata;
if (randomBoolean()) {
metadata = Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8));
Expand All @@ -582,7 +589,7 @@ public static Authentication randomAuthentication(User user, RealmRef realmRef)
if (randomBoolean()) { // run-as
return new Authentication(
new User(user.principal(), user.roles(), randomUser()),
randomRealmRef(false),
randomRealmRef(randomBoolean()),
realmRef,
version,
authenticationType,
Expand All @@ -598,11 +605,27 @@ public static Authentication randomApiKeyAuthentication(User user, String apiKey
}

public static Authentication randomApiKeyAuthentication(User user, String apiKeyId, Version version) {
return randomApiKeyAuthentication(
user,
apiKeyId,
AuthenticationField.API_KEY_CREATOR_REALM_NAME,
AuthenticationField.API_KEY_CREATOR_REALM_TYPE,
version
);
}

public static Authentication randomApiKeyAuthentication(
User user,
String apiKeyId,
String creatorRealmName,
String creatorRealmType,
Version version
) {
final HashMap<String, Object> metadata = new HashMap<>();
metadata.put(AuthenticationField.API_KEY_ID_KEY, apiKeyId);
metadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLengthBetween(1, 16));
metadata.put(AuthenticationField.API_KEY_CREATOR_REALM_NAME, AuthenticationField.API_KEY_CREATOR_REALM_NAME);
metadata.put(AuthenticationField.API_KEY_CREATOR_REALM_TYPE, AuthenticationField.API_KEY_CREATOR_REALM_TYPE);
metadata.put(AuthenticationField.API_KEY_CREATOR_REALM_NAME, creatorRealmName);
metadata.put(AuthenticationField.API_KEY_CREATOR_REALM_TYPE, creatorRealmType);
metadata.put(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, new BytesArray("{}"));
metadata.put(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, new BytesArray("""
{"x":{"cluster":["all"],"indices":[{"names":["index*"],"privileges":["all"]}]}}"""));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
import org.elasticsearch.xpack.core.security.authc.AuthenticationTests;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.authc.RealmDomain;
import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission;
import org.elasticsearch.xpack.core.security.user.User;

Expand Down Expand Up @@ -69,20 +72,43 @@ public void testAuthenticationWithUserAllowsAccessToApiKeyActionsWhenItIsOwner()
final ClusterPermission clusterPermission = ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder())
.build();

final boolean isRunAs = randomBoolean();
final User userJoe = new User("joe");
final Authentication authentication = createMockAuthentication(
isRunAs ? new User(userJoe, new User("not-joe")) : userJoe,
"realm1",
isRunAs ? randomFrom(AuthenticationType.REALM, AuthenticationType.API_KEY) : AuthenticationType.REALM,
Map.of()
);
final TransportRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName("realm1", "joe");
final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName("realm1", "joe");
final Authentication.RealmRef realmRef = AuthenticationTests.randomRealmRef(randomBoolean());
final Authentication authentication = AuthenticationTests.randomAuthentication(new User("joe"), realmRef);

TransportRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName(realmRef.getName(), "joe");
TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName(realmRef.getName(), "joe");
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication));

assertFalse(clusterPermission.check("cluster:admin/something", mock(TransportRequest.class), authentication));

getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName(realmRef.getName(), "jane");
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName(realmRef.getName(), "jane");
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication));

RealmDomain realmDomain = realmRef.getDomain();
final String otherRealmName;
if (realmDomain != null) {
for (RealmConfig.RealmIdentifier realmIdentifier : realmDomain.realms()) {
getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName(realmIdentifier.getName(), "joe");
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName(realmIdentifier.getName(), "joe");
assertTrue(
clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication)
);
}
otherRealmName = randomValueOtherThanMany(
realmName -> realmDomain.realms().stream().map(ri -> ri.getName()).anyMatch(realmName::equals),
() -> randomAlphaOfLengthBetween(2, 10)
);
} else {
otherRealmName = randomValueOtherThan(realmRef.getName(), () -> randomAlphaOfLengthBetween(2, 10));
}
getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName(otherRealmName, "joe");
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName(otherRealmName, "joe");
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication));
}

public void testAuthenticationWithUserAllowsAccessToApiKeyActionsWhenItIsOwner_WithOwnerFlagOnly() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.tasks.Task;
Expand Down Expand Up @@ -40,24 +41,24 @@ public TransportGetApiKeyAction(

@Override
protected void doExecute(Task task, GetApiKeyRequest request, ActionListener<GetApiKeyResponse> listener) {
String apiKeyId = request.getApiKeyId();
String[] apiKeyIds = Strings.hasText(request.getApiKeyId()) ? new String[] { request.getApiKeyId() } : null;
String apiKeyName = request.getApiKeyName();
String username = request.getUserName();
String realm = request.getRealmName();
String[] realms = Strings.hasText(request.getRealmName()) ? new String[] { request.getRealmName() } : null;

final Authentication authentication = securityContext.getAuthentication();
if (authentication == null) {
listener.onFailure(new IllegalStateException("authentication is required"));
}
if (request.ownedByAuthenticatedUser()) {
assert username == null;
assert realm == null;
assert realms == null;
// restrict username and realm to current authenticated user.
username = authentication.getUser().principal();
realm = ApiKeyService.getCreatorRealmName(authentication);
realms = ApiKeyService.getOwnersRealmNames(authentication);
}

apiKeyService.getApiKeys(realm, username, apiKeyName, apiKeyId, listener);
apiKeyService.getApiKeys(realms, username, apiKeyName, apiKeyIds, listener);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.tasks.Task;
Expand Down Expand Up @@ -48,21 +49,21 @@ protected void doExecute(Task task, InvalidateApiKeyRequest request, ActionListe
String[] apiKeyIds = request.getIds();
String apiKeyName = request.getName();
String username = request.getUserName();
String realm = request.getRealmName();
String[] realms = Strings.hasText(request.getRealmName()) ? new String[] { request.getRealmName() } : null;

final Authentication authentication = securityContext.getAuthentication();
if (authentication == null) {
listener.onFailure(new IllegalStateException("authentication is required"));
}
if (request.ownedByAuthenticatedUser()) {
assert username == null;
assert realm == null;
assert realms == null;
// restrict username and realm to current authenticated user.
username = authentication.getUser().principal();
realm = ApiKeyService.getCreatorRealmName(authentication);
realms = ApiKeyService.getOwnersRealmNames(authentication);
}

apiKeyService.invalidateApiKeys(realm, username, apiKeyName, apiKeyIds, listener);
apiKeyService.invalidateApiKeys(realms, username, apiKeyName, apiKeyIds, listener);
}

}

0 comments on commit 32cc055

Please sign in to comment.