diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java index f0d8c20ab27f9..a97c6283f681c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java @@ -69,8 +69,12 @@ protected void doExecute(Task task, DelegatePkiAuthenticationRequest request, ActionListener 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 -> { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index be2859cdc4538..ce5b2860090b4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -198,7 +198,14 @@ public void authenticate(AuthenticationToken authToken, ActionListener listener) { - final Map metadata = Map.of("pki_dn", token.dn()); + final Map 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); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java index 0d5021031bcba..57e05c4314229 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java @@ -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; @@ -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 @@ -57,6 +63,10 @@ public void clearCredentials() { } public boolean isDelegated() { - return isDelegated; + return delegateeAuthentication != null; + } + + public Authentication getDelegateeAuthentication() { + return delegateeAuthentication; } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java index 4b5d815bcfa27..97bc9991e45a8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java @@ -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; @@ -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 { @@ -96,6 +103,14 @@ 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")); @@ -103,6 +118,59 @@ public void testDelegatePki() throws Exception { } } + 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"); @@ -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, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index 62637b6408f03..43a4c4c51c47e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -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; @@ -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 { @@ -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(); @@ -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();