Skip to content

Commit

Permalink
Add pki_delegated_by_* to User metadata (#44873)
Browse files Browse the repository at this point in the history
This adds the `pki_delegated_by_user` and `pki_delegated_by_realm` fields to the `User#metadata` map.
These fields contains the authentication of the proxy client (eg. `kibana_system`).
The intention is for the `es-admin` to be able to write role mapping rules that
distinguish between users authenticated directly or by delegation.
  • Loading branch information
albertzaharovits authored Aug 5, 2019
1 parent 433a331 commit e599560
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,12 @@ protected void doExecute(Task task, DelegatePkiAuthenticationRequest request,
ActionListener<DelegatePkiAuthenticationResponse> listener) {
final ThreadContext threadContext = threadPool.getThreadContext();
Authentication delegateeAuthentication = Authentication.getAuthentication(threadContext);
final X509AuthenticationToken x509DelegatedToken = new X509AuthenticationToken(
request.getCertificateChain().toArray(new X509Certificate[0]), true);
if (delegateeAuthentication == null) {
listener.onFailure(new IllegalStateException("Delegatee authentication cannot be null"));
return;
}
final X509AuthenticationToken x509DelegatedToken = X509AuthenticationToken
.delegated(request.getCertificateChain().toArray(new X509Certificate[0]), delegateeAuthentication);
logger.trace("Attempting to authenticate delegated x509Token [{}]", x509DelegatedToken);
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
authenticationService.authenticate(ACTION_NAME, request, x509DelegatedToken, ActionListener.wrap(authentication -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,14 @@ public void authenticate(AuthenticationToken authToken, ActionListener<Authentic
}

private void buildUser(X509AuthenticationToken token, String principal, ActionListener<AuthenticationResult> listener) {
final Map<String, Object> metadata = Map.of("pki_dn", token.dn());
final Map<String, Object> metadata;
if (token.isDelegated()) {
metadata = Map.of("pki_dn", token.dn(),
"pki_delegated_by_user", token.getDelegateeAuthentication().getUser().principal(),
"pki_delegated_by_realm", token.getDelegateeAuthentication().getAuthenticatedBy().getName());
} else {
metadata = Map.of("pki_dn", token.dn());
}
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(principal, token.dn(), Set.of(), metadata, config);
roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
final User computedUser = new User(principal, roles.toArray(new String[roles.size()]), null, null, metadata, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package org.elasticsearch.xpack.security.authc.pki;

import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.ssl.CertParsingUtils;

Expand All @@ -16,21 +17,26 @@ public class X509AuthenticationToken implements AuthenticationToken {

private final String dn;
private final X509Certificate[] credentials;
private final boolean isDelegated;
private final Authentication delegateeAuthentication;
private String principal;

public X509AuthenticationToken(X509Certificate[] certificates) {
this(certificates, false);
this(certificates, null);
}

public X509AuthenticationToken(X509Certificate[] certificates, boolean isDelegated) {
private X509AuthenticationToken(X509Certificate[] certificates, Authentication delegateeAuthentication) {
this.credentials = Objects.requireNonNull(certificates);
if (false == CertParsingUtils.isOrderedCertificateChain(Arrays.asList(certificates))) {
throw new IllegalArgumentException("certificates chain array is not ordered");
}
this.dn = certificates.length == 0 ? "" : certificates[0].getSubjectX500Principal().toString();
this.principal = this.dn;
this.isDelegated = isDelegated;
this.delegateeAuthentication = delegateeAuthentication;
}

public static X509AuthenticationToken delegated(X509Certificate[] certificates, Authentication delegateeAuthentication) {
Objects.requireNonNull(delegateeAuthentication);
return new X509AuthenticationToken(certificates, delegateeAuthentication);
}

@Override
Expand All @@ -57,6 +63,10 @@ public void clearCredentials() {
}

public boolean isDelegated() {
return isDelegated;
return delegateeAuthentication != null;
}

public Authentication getDelegateeAuthentication() {
return delegateeAuthentication;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.ValidationException;
import org.elasticsearch.client.security.AuthenticateResponse;
import org.elasticsearch.client.security.PutRoleMappingRequest;
import org.elasticsearch.client.security.RefreshPolicy;
import org.elasticsearch.client.security.AuthenticateResponse.RealmInfo;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression;
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse;
import org.elasticsearch.client.security.user.User;
Expand All @@ -27,11 +31,14 @@
import java.nio.file.Path;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Arrays;

import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.emptyCollectionOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.startsWith;

public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
Expand Down Expand Up @@ -96,13 +103,74 @@ public void testDelegatePki() throws Exception {
User user = resp.getUser();
assertThat(user, is(notNullValue()));
assertThat(user.getUsername(), is("Elasticsearch Test Client"));
assertThat(user.getMetadata().get("pki_dn"), is(notNullValue()));
assertThat(user.getMetadata().get("pki_dn"), is("O=org, OU=Elasticsearch, CN=Elasticsearch Test Client"));
assertThat(user.getMetadata().get("pki_delegated_by_user"), is(notNullValue()));
assertThat(user.getMetadata().get("pki_delegated_by_user"), is("test_user"));
assertThat(user.getMetadata().get("pki_delegated_by_realm"), is(notNullValue()));
assertThat(user.getMetadata().get("pki_delegated_by_realm"), is("file"));
// no roles because no role mappings
assertThat(user.getRoles(), is(emptyCollectionOf(String.class)));
RealmInfo authnRealm = resp.getAuthenticationRealm();
assertThat(authnRealm, is(notNullValue()));
assertThat(authnRealm.getName(), is("pki3"));
assertThat(authnRealm.getType(), is("pki"));
}
}

public void testDelegatePkiWithRoleMapping() throws Exception {
X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt");
DelegatePkiAuthenticationRequest delegatePkiRequest;
if (randomBoolean()) {
delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA));
} else {
delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA));
}
final RequestOptions testUserOptions = RequestOptions.DEFAULT.toBuilder()
.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME,
new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray())))
.build();
try (RestHighLevelClient restClient = new TestRestHighLevelClient()) {
// put role mappings for delegated PKI
PutRoleMappingRequest request = new PutRoleMappingRequest("role_by_delegated_user", true,
Collections.singletonList("role_by_delegated_user"), Collections.emptyList(),
new FieldRoleMapperExpression("metadata.pki_delegated_by_user", "test_user"), null, RefreshPolicy.IMMEDIATE);
restClient.security().putRoleMapping(request, testUserOptions);
request = new PutRoleMappingRequest("role_by_delegated_realm", true, Collections.singletonList("role_by_delegated_realm"),
Collections.emptyList(), new FieldRoleMapperExpression("metadata.pki_delegated_by_realm", "file"), null,
RefreshPolicy.IMMEDIATE);
restClient.security().putRoleMapping(request, testUserOptions);
// delegate
DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security().delegatePkiAuthentication(delegatePkiRequest,
testUserOptions);
// authenticate
AuthenticateResponse resp = restClient.security().authenticate(RequestOptions.DEFAULT.toBuilder()
.addHeader("Authorization", "Bearer " + delegatePkiResponse.getAccessToken()).build());
User user = resp.getUser();
assertThat(user, is(notNullValue()));
assertThat(user.getUsername(), is("Elasticsearch Test Client"));
assertThat(user.getMetadata().get("pki_dn"), is(notNullValue()));
assertThat(user.getMetadata().get("pki_dn"), is("O=org, OU=Elasticsearch, CN=Elasticsearch Test Client"));
assertThat(user.getMetadata().get("pki_delegated_by_user"), is(notNullValue()));
assertThat(user.getMetadata().get("pki_delegated_by_user"), is("test_user"));
assertThat(user.getMetadata().get("pki_delegated_by_realm"), is(notNullValue()));
assertThat(user.getMetadata().get("pki_delegated_by_realm"), is("file"));
// assert roles
assertThat(user.getRoles(), containsInAnyOrder("role_by_delegated_user", "role_by_delegated_realm"));
RealmInfo authnRealm = resp.getAuthenticationRealm();
assertThat(authnRealm, is(notNullValue()));
assertThat(authnRealm.getName(), is("pki3"));
assertThat(authnRealm.getType(), is("pki"));
// delete role mappings for delegated PKI
restClient.security().deleteRoleMapping(new DeleteRoleMappingRequest("role_by_delegated_user", RefreshPolicy.IMMEDIATE),
testUserOptions);
restClient.security().deleteRoleMapping(new DeleteRoleMappingRequest("role_by_delegated_realm", RefreshPolicy.IMMEDIATE),
testUserOptions);
}
}

public void testDelegatePkiFailure() throws Exception {
X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
Expand All @@ -117,14 +185,12 @@ public void testDelegatePkiFailure() throws Exception {
() -> restClient.security().delegatePkiAuthentication(delegatePkiRequest1, optionsBuilder.build()));
assertThat(e1.getMessage(), is("Elasticsearch exception [type=security_exception, reason=unable to authenticate user"
+ " [O=org, OU=Elasticsearch, CN=Elasticsearch Test Client] for action [cluster:admin/xpack/security/delegate_pki]]"));

// swapped order
DelegatePkiAuthenticationRequest delegatePkiRequest2 = new DelegatePkiAuthenticationRequest(
Arrays.asList(intermediateCA, clientCertificate));
ValidationException e2 = expectThrows(ValidationException.class,
() -> restClient.security().delegatePkiAuthentication(delegatePkiRequest2, optionsBuilder.build()));
assertThat(e2.getMessage(), is("Validation Failed: 1: certificates chain must be an ordered chain;"));

// bogus certificate
DelegatePkiAuthenticationRequest delegatePkiRequest3 = new DelegatePkiAuthenticationRequest(Arrays.asList(bogusCertificate));
ElasticsearchStatusException e3 = expectThrows(ElasticsearchStatusException.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.InternalRealmsSettings;
import org.elasticsearch.xpack.core.security.authc.Realm;
Expand Down Expand Up @@ -81,7 +83,10 @@ public void testTokenSupport() {

assertThat(realm.supports(null), is(false));
assertThat(realm.supports(new UsernamePasswordToken("", new SecureString(new char[0]))), is(false));
assertThat(realm.supports(new X509AuthenticationToken(new X509Certificate[0], randomBoolean())), is(true));
X509AuthenticationToken token = randomBoolean()
? X509AuthenticationToken.delegated(new X509Certificate[0], mock(Authentication.class))
: new X509AuthenticationToken(new X509Certificate[0]);
assertThat(realm.supports(token), is(true));
}

public void testExtractToken() throws Exception {
Expand Down Expand Up @@ -286,7 +291,15 @@ public void testAuthenticationDelegationFailsWithoutTruststore() throws Exceptio

public void testAuthenticationDelegationSuccess() throws Exception {
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
X509AuthenticationToken delegatedToken = new X509AuthenticationToken(new X509Certificate[] { certificate }, true);
Authentication mockAuthentication = mock(Authentication.class);
User mockUser = mock(User.class);
when(mockUser.principal()).thenReturn("mockup_delegate_username");
RealmRef mockRealmRef = mock(RealmRef.class);
when(mockRealmRef.getName()).thenReturn("mockup_delegate_realm");
when(mockAuthentication.getUser()).thenReturn(mockUser);
when(mockAuthentication.getAuthenticatedBy()).thenReturn(mockRealmRef);
X509AuthenticationToken delegatedToken = X509AuthenticationToken.delegated(new X509Certificate[] { certificate },
mockAuthentication);

UserRoleMapper roleMapper = buildRoleMapper();
MockSecureSettings secureSettings = new MockSecureSettings();
Expand All @@ -306,11 +319,14 @@ public void testAuthenticationDelegationSuccess() throws Exception {
assertThat(result.getUser().principal(), is("Elasticsearch Test Node"));
assertThat(result.getUser().roles(), is(notNullValue()));
assertThat(result.getUser().roles().length, is(0));
assertThat(result.getUser().metadata().get("pki_delegated_by_user"), is("mockup_delegate_username"));
assertThat(result.getUser().metadata().get("pki_delegated_by_realm"), is("mockup_delegate_realm"));
}

public void testAuthenticationDelegationFailure() throws Exception {
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
X509AuthenticationToken delegatedToken = new X509AuthenticationToken(new X509Certificate[] { certificate }, true);
X509AuthenticationToken delegatedToken = X509AuthenticationToken.delegated(new X509Certificate[] { certificate },
mock(Authentication.class));

UserRoleMapper roleMapper = buildRoleMapper();
MockSecureSettings secureSettings = new MockSecureSettings();
Expand Down

0 comments on commit e599560

Please sign in to comment.