From eed3e5deaf3fd13c353361e45420735a93d0f3d0 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Wed, 13 Jul 2022 16:53:43 +0200 Subject: Return granted capabilities from PeerAuthorizer Introduce new ConnectionAuthContext as replacement for AuthorizationResult/SecurityContext. --- .../rpc/security/MultiTenantRpcAuthorizer.java | 8 ++-- .../rpc/security/MultiTenantRpcAuthorizerTest.java | 10 +++-- jrt/src/com/yahoo/jrt/Connection.java | 6 ++- jrt/src/com/yahoo/jrt/CryptoSocket.java | 6 ++- jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java | 6 ++- jrt/src/com/yahoo/jrt/SecurityContext.java | 24 ------------ jrt/src/com/yahoo/jrt/Target.java | 6 ++- jrt/src/com/yahoo/jrt/TlsCryptoSocket.java | 25 +++--------- jrt/tests/com/yahoo/jrt/EchoTest.java | 27 ++++++------- .../security/tls/authz/AuthorizationResult.java | 45 ---------------------- .../security/tls/authz/ConnectionAuthContext.java | 23 +++++++++++ .../yahoo/security/tls/authz/PeerAuthorizer.java | 11 ++++-- .../tls/authz/PeerAuthorizerTrustManager.java | 10 ++--- .../yahoo/security/tls/policy/CapabilitySet.java | 10 +++++ .../security/tls/authz/PeerAuthorizerTest.java | 39 ++++++++++++++++--- 15 files changed, 125 insertions(+), 131 deletions(-) delete mode 100644 jrt/src/com/yahoo/jrt/SecurityContext.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/authz/AuthorizationResult.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/authz/ConnectionAuthContext.java diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizer.java index f5b570fed40..6ca39a25d9c 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizer.java @@ -10,9 +10,9 @@ import com.yahoo.config.provision.security.NodeIdentifier; import com.yahoo.config.provision.security.NodeIdentifierException; import com.yahoo.config.provision.security.NodeIdentity; import com.yahoo.jrt.Request; -import com.yahoo.jrt.SecurityContext; import com.yahoo.security.tls.MixedMode; import com.yahoo.security.tls.TransportSecurityUtils; +import com.yahoo.security.tls.authz.ConnectionAuthContext; import com.yahoo.vespa.config.ConfigKey; import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3; import com.yahoo.vespa.config.server.RequestHandler; @@ -166,14 +166,14 @@ public class MultiTenantRpcAuthorizer implements RpcAuthorizer { // TODO Make peer identity mandatory once TLS mixed mode is removed private Optional getPeerIdentity(Request request) { - Optional securityContext = request.target().getSecurityContext(); - if (securityContext.isEmpty()) { + Optional authCtx = request.target().getConnectionAuthContext(); + if (authCtx.isEmpty()) { if (TransportSecurityUtils.getInsecureMixedMode() == MixedMode.DISABLED) { throw new IllegalStateException("Security context missing"); // security context should always be present } return Optional.empty(); // client choose to communicate over insecure channel } - List certChain = securityContext.get().peerCertificateChain(); + List certChain = authCtx.get().peerCertificate(); if (certChain.isEmpty()) { throw new IllegalStateException("Client authentication is not enforced!"); // clients should be required to authenticate when TLS is enabled } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java index 2650b23a38e..7a2e0f00433 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java @@ -11,7 +11,6 @@ import com.yahoo.config.provision.security.NodeIdentifier; import com.yahoo.config.provision.security.NodeIdentifierException; import com.yahoo.config.provision.security.NodeIdentity; import com.yahoo.jrt.Request; -import com.yahoo.jrt.SecurityContext; import com.yahoo.jrt.StringValue; import com.yahoo.jrt.Target; import com.yahoo.jrt.Values; @@ -19,6 +18,8 @@ import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; import com.yahoo.security.SignatureAlgorithm; import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.tls.authz.ConnectionAuthContext; +import com.yahoo.security.tls.policy.CapabilitySet; import com.yahoo.slime.Cursor; import com.yahoo.slime.JsonFormat; import com.yahoo.slime.Slime; @@ -40,6 +41,7 @@ import java.time.Instant; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -248,10 +250,10 @@ public class MultiTenantRpcAuthorizerTest { } private static Request mockJrtRpcRequest(String payload) { - SecurityContext securityContext = mock(SecurityContext.class); - when(securityContext.peerCertificateChain()).thenReturn(PEER_CERTIFICATE_CHAIN); + ConnectionAuthContext authContext = + new ConnectionAuthContext(PEER_CERTIFICATE_CHAIN, CapabilitySet.none(), new TreeSet<>()); Target target = mock(Target.class); - when(target.getSecurityContext()).thenReturn(Optional.of(securityContext)); + when(target.getConnectionAuthContext()).thenReturn(Optional.of(authContext)); Request request = mock(Request.class); when(request.target()).thenReturn(target); Values values = new Values(); diff --git a/jrt/src/com/yahoo/jrt/Connection.java b/jrt/src/com/yahoo/jrt/Connection.java index 00aceb7e352..644e2ef4ff3 100644 --- a/jrt/src/com/yahoo/jrt/Connection.java +++ b/jrt/src/com/yahoo/jrt/Connection.java @@ -1,6 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.jrt; +import com.yahoo.security.tls.authz.ConnectionAuthContext; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; @@ -436,9 +438,9 @@ class Connection extends Target { } @Override - public Optional getSecurityContext() { + public Optional getConnectionAuthContext() { return Optional.ofNullable(socket) - .flatMap(CryptoSocket::getSecurityContext); + .flatMap(CryptoSocket::getConnectionAuthContext); } public boolean isClient() { diff --git a/jrt/src/com/yahoo/jrt/CryptoSocket.java b/jrt/src/com/yahoo/jrt/CryptoSocket.java index 78308b76624..aac91362405 100644 --- a/jrt/src/com/yahoo/jrt/CryptoSocket.java +++ b/jrt/src/com/yahoo/jrt/CryptoSocket.java @@ -2,6 +2,8 @@ package com.yahoo.jrt; +import com.yahoo.security.tls.authz.ConnectionAuthContext; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; @@ -103,10 +105,10 @@ public interface CryptoSocket { public void dropEmptyBuffers(); /** - * Returns the security context for the current connection (given handshake completed), + * Returns the auth context for the current connection (given handshake completed), * or empty if the current connection is not secure. */ - default public Optional getSecurityContext() { + default public Optional getConnectionAuthContext() { return Optional.empty(); } } diff --git a/jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java b/jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java index df01f4f2fa7..42442289cd1 100644 --- a/jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java +++ b/jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java @@ -1,6 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.jrt; +import com.yahoo.security.tls.authz.ConnectionAuthContext; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; @@ -130,5 +132,7 @@ public class MaybeTlsCryptoSocket implements CryptoSocket { @Override public int write(ByteBuffer src) throws IOException { return socket.write(src); } @Override public FlushResult flush() throws IOException { return socket.flush(); } @Override public void dropEmptyBuffers() { socket.dropEmptyBuffers(); } - @Override public Optional getSecurityContext() { return Optional.ofNullable(socket).flatMap(CryptoSocket::getSecurityContext); } + @Override public Optional getConnectionAuthContext() { + return Optional.ofNullable(socket).flatMap(CryptoSocket::getConnectionAuthContext); + } } diff --git a/jrt/src/com/yahoo/jrt/SecurityContext.java b/jrt/src/com/yahoo/jrt/SecurityContext.java deleted file mode 100644 index 4eef99cb93f..00000000000 --- a/jrt/src/com/yahoo/jrt/SecurityContext.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jrt; - -import java.security.cert.X509Certificate; -import java.util.List; - -/** - * @author bjorncs - */ -public class SecurityContext { - - private final List peerCertificateChain; - - public SecurityContext(List peerCertificateChain) { - this.peerCertificateChain = peerCertificateChain; - } - - /** - * @return the peer certificate chain if the peer was authenticated, empty list if not. - */ - public List peerCertificateChain() { - return peerCertificateChain; - } -} diff --git a/jrt/src/com/yahoo/jrt/Target.java b/jrt/src/com/yahoo/jrt/Target.java index a59aa341fe0..239a71f53b3 100644 --- a/jrt/src/com/yahoo/jrt/Target.java +++ b/jrt/src/com/yahoo/jrt/Target.java @@ -1,6 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.jrt; +import com.yahoo.security.tls.authz.ConnectionAuthContext; + import java.util.Optional; /** @@ -69,9 +71,9 @@ public abstract class Target { public Exception getConnectionLostReason() { return null; } /** - * Returns the security context associated with this target, or empty if no connection or is insecure. + * Returns the connection auth context associated with this target, or empty if no connection or is insecure. */ - public abstract Optional getSecurityContext(); + public abstract Optional getConnectionAuthContext(); /** * Check if this target represents the client side of a diff --git a/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java b/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java index a899938dd45..40cb7c3938a 100644 --- a/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java +++ b/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java @@ -1,7 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.jrt; -import com.yahoo.security.tls.authz.AuthorizationResult; +import com.yahoo.security.tls.authz.ConnectionAuthContext; import com.yahoo.security.tls.authz.PeerAuthorizerTrustManager; import javax.net.ssl.SSLEngine; @@ -9,19 +9,14 @@ import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SocketChannel; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; import java.util.Optional; import java.util.logging.Logger; -import static java.util.stream.Collectors.toList; import static javax.net.ssl.SSLEngineResult.Status; /** @@ -46,7 +41,7 @@ public class TlsCryptoSocket implements CryptoSocket { private int sessionApplicationBufferSize; private ByteBuffer handshakeDummyBuffer; private HandshakeState handshakeState; - private AuthorizationResult authorizationResult; + private ConnectionAuthContext authorizationResult; public TlsCryptoSocket(SocketChannel channel, SSLEngine sslEngine) { this.channel = channel; @@ -224,19 +219,9 @@ public class TlsCryptoSocket implements CryptoSocket { } @Override - public Optional getSecurityContext() { - try { - if (handshakeState != HandshakeState.COMPLETED) { - return Optional.empty(); - } - List peerCertificateChain = - Arrays.stream(sslEngine.getSession().getPeerCertificates()) - .map(X509Certificate.class::cast) - .collect(toList()); - return Optional.of(new SecurityContext(peerCertificateChain)); - } catch (SSLPeerUnverifiedException e) { // unverified peer: non-certificate based ciphers or peer did not provide a certificate - return Optional.of(new SecurityContext(List.of())); // secure connection, but peer does not have a certificate chain. - } + public Optional getConnectionAuthContext() { + if (handshakeState != HandshakeState.COMPLETED) return Optional.empty(); + return Optional.ofNullable(authorizationResult); } private boolean handshakeWrap() throws IOException { diff --git a/jrt/tests/com/yahoo/jrt/EchoTest.java b/jrt/tests/com/yahoo/jrt/EchoTest.java index 26d4315fad6..7213068c0f9 100644 --- a/jrt/tests/com/yahoo/jrt/EchoTest.java +++ b/jrt/tests/com/yahoo/jrt/EchoTest.java @@ -2,6 +2,7 @@ package com.yahoo.jrt; +import com.yahoo.security.tls.authz.ConnectionAuthContext; import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; @@ -28,19 +29,19 @@ public class EchoTest { Supervisor client; Target target; Values refValues; - SecurityContext securityContext; + ConnectionAuthContext connAuthCtx; private interface MetricsAssertions { void assertMetrics(TransportMetrics.Snapshot snapshot) throws AssertionError; } - private interface SecurityContextAssertion { - void assertSecurityContext(SecurityContext securityContext) throws AssertionError; + private interface ConnectionAuthContextAssertion { + void assertConnectionAuthContext(ConnectionAuthContext authContext) throws AssertionError; } @Parameter(value = 0) public CryptoEngine crypto; @Parameter(value = 1) public MetricsAssertions metricsAssertions; - @Parameter(value = 2) public SecurityContextAssertion securityContextAssertion; + @Parameter(value = 2) public ConnectionAuthContextAssertion connAuthCtxAssertion; @Parameters(name = "{0}") public static Object[] engines() { @@ -62,8 +63,8 @@ public class EchoTest { assertEquals(1, metrics.serverTlsConnectionsEstablished()); assertEquals(1, metrics.clientTlsConnectionsEstablished()); }, - (SecurityContextAssertion) context -> { - List chain = context.peerCertificateChain(); + (ConnectionAuthContextAssertion) context -> { + List chain = context.peerCertificate(); assertEquals(1, chain.size()); assertEquals(CryptoUtils.certificate, chain.get(0)); }}, @@ -80,8 +81,8 @@ public class EchoTest { assertEquals(1, metrics.serverTlsConnectionsEstablished()); assertEquals(1, metrics.clientTlsConnectionsEstablished()); }, - (SecurityContextAssertion) context -> { - List chain = context.peerCertificateChain(); + (ConnectionAuthContextAssertion) context -> { + List chain = context.peerCertificate(); assertEquals(1, chain.size()); assertEquals(CryptoUtils.certificate, chain.get(0)); }}}; @@ -146,7 +147,7 @@ public class EchoTest { for (int i = 0; i < p.size(); i++) { r.add(p.get(i)); } - securityContext = req.target().getSecurityContext().orElse(null); + connAuthCtx = req.target().getConnectionAuthContext().orElse(null); } @org.junit.Test @@ -164,11 +165,11 @@ public class EchoTest { if (metricsAssertions != null) { metricsAssertions.assertMetrics(metrics.snapshot().changesSince(startSnapshot)); } - if (securityContextAssertion != null) { - assertNotNull(securityContext); - securityContextAssertion.assertSecurityContext(securityContext); + if (connAuthCtxAssertion != null) { + assertNotNull(connAuthCtx); + connAuthCtxAssertion.assertConnectionAuthContext(connAuthCtx); } else { - assertNull(securityContext); + assertNull(connAuthCtx); } } } diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/AuthorizationResult.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/AuthorizationResult.java deleted file mode 100644 index 6fa97e30d63..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/authz/AuthorizationResult.java +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.authz; - -import java.util.Collections; -import java.util.Objects; -import java.util.Set; - -/** - * @author bjorncs - */ -public class AuthorizationResult { - private final Set matchedPolicies; - - public AuthorizationResult(Set matchedPolicies) { - this.matchedPolicies = Collections.unmodifiableSet(matchedPolicies); - } - - public Set matchedPolicies() { - return matchedPolicies; - } - - public boolean succeeded() { - return matchedPolicies.size() > 0; - } - - @Override - public String toString() { - return "AuthorizationResult{" + - ", matchedPolicies=" + matchedPolicies + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AuthorizationResult that = (AuthorizationResult) o; - return Objects.equals(matchedPolicies, that.matchedPolicies); - } - - @Override - public int hashCode() { - return Objects.hash(matchedPolicies); - } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/ConnectionAuthContext.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/ConnectionAuthContext.java new file mode 100644 index 00000000000..a5fb51da763 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/ConnectionAuthContext.java @@ -0,0 +1,23 @@ +package com.yahoo.security.tls.authz; + +import com.yahoo.security.tls.policy.CapabilitySet; + +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * @author bjorncs + */ +public record ConnectionAuthContext(List peerCertificate, + CapabilitySet capabilities, + SortedSet matchedPolicies) { + + public ConnectionAuthContext { + matchedPolicies = new TreeSet<>(matchedPolicies); + } + + public boolean succeeded() { return matchedPolicies.size() > 0; } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java index 8c4e87c1de2..353f704b136 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java @@ -4,6 +4,7 @@ package com.yahoo.security.tls.authz; import com.yahoo.security.SubjectAlternativeName; import com.yahoo.security.X509CertificateUtils; import com.yahoo.security.tls.policy.AuthorizedPeers; +import com.yahoo.security.tls.policy.CapabilitySet; import com.yahoo.security.tls.policy.PeerPolicy; import com.yahoo.security.tls.policy.RequiredPeerCredential; @@ -12,6 +13,8 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.logging.Logger; import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME; @@ -34,17 +37,19 @@ public class PeerAuthorizer { this.authorizedPeers = authorizedPeers; } - public AuthorizationResult authorizePeer(X509Certificate peerCertificate) { - Set matchedPolicies = new HashSet<>(); + public ConnectionAuthContext authorizePeer(X509Certificate peerCertificate) { + SortedSet matchedPolicies = new TreeSet<>(); + Set grantedCapabilities = new HashSet<>(); String cn = getCommonName(peerCertificate).orElse(null); List sans = getSubjectAlternativeNames(peerCertificate); log.fine(() -> String.format("Subject info from x509 certificate: CN=[%s], 'SAN=%s", cn, sans)); for (PeerPolicy peerPolicy : authorizedPeers.peerPolicies()) { if (matchesPolicy(peerPolicy, cn, sans)) { matchedPolicies.add(peerPolicy.policyName()); + grantedCapabilities.add(peerPolicy.capabilities()); } } - return new AuthorizationResult(matchedPolicies); + return new ConnectionAuthContext(List.of(peerCertificate), CapabilitySet.unionOf(grantedCapabilities), matchedPolicies); } private static boolean matchesPolicy(PeerPolicy peerPolicy, String cn, List sans) { diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java index e8d558205c4..925e21c63ff 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java @@ -26,7 +26,7 @@ import java.util.logging.Logger; // Note: Implementation assumes that provided X509ExtendedTrustManager will throw IllegalArgumentException when chain is empty or null public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { - public static final String HANDSHAKE_SESSION_AUTHZ_RESULT_PROPERTY = "vespa.tls.authorization.result"; + public static final String HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY = "vespa.tls.auth.ctx"; private static final Logger log = Logger.getLogger(PeerAuthorizerTrustManager.class.getName()); @@ -98,18 +98,18 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { /** * Note: The authorization result is only available during handshake. The underlying handshake session is removed once handshake is complete. */ - public static Optional getAuthorizationResult(SSLEngine sslEngine) { + public static Optional getAuthorizationResult(SSLEngine sslEngine) { return Optional.ofNullable(sslEngine.getHandshakeSession()) - .flatMap(session -> Optional.ofNullable((AuthorizationResult) session.getValue(HANDSHAKE_SESSION_AUTHZ_RESULT_PROPERTY))); + .flatMap(session -> Optional.ofNullable((ConnectionAuthContext) session.getValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY))); } private void authorizePeer(X509Certificate certificate, String authType, boolean isVerifyingClient, SSLEngine sslEngine) throws CertificateException { if (mode == AuthorizationMode.DISABLE) return; log.fine(() -> "Verifying certificate: " + createInfoString(certificate, authType, isVerifyingClient)); - AuthorizationResult result = authorizer.authorizePeer(certificate); + ConnectionAuthContext result = authorizer.authorizePeer(certificate); if (sslEngine != null) { // getHandshakeSession() will never return null in this context - sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTHZ_RESULT_PROPERTY, result); + sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY, result); } if (result.succeeded()) { log.fine(() -> String.format("Verification result: %s", result)); diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/CapabilitySet.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/CapabilitySet.java index 44ff1eedfb0..46a07972fc3 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/CapabilitySet.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/policy/CapabilitySet.java @@ -3,10 +3,12 @@ package com.yahoo.security.tls.policy; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.stream.Collectors; @@ -56,6 +58,12 @@ public class CapabilitySet { return new CapabilitySet(caps); } + public static CapabilitySet unionOf(Collection capSets) { + EnumSet union = EnumSet.noneOf(Capability.class); + capSets.forEach(cs -> union.addAll(cs.caps)); + return new CapabilitySet(union); + } + public static CapabilitySet from(EnumSet caps) { return new CapabilitySet(EnumSet.copyOf(caps)); } public static CapabilitySet from(Collection caps) { return new CapabilitySet(EnumSet.copyOf(caps)); } public static CapabilitySet from(Capability... caps) { return new CapabilitySet(EnumSet.copyOf(List.of(caps))); } @@ -68,6 +76,8 @@ public class CapabilitySet { return caps.stream().map(Capability::asString).collect(Collectors.toCollection(TreeSet::new)); } + public Set asSet() { return Collections.unmodifiableSet(caps); } + @Override public String toString() { return "CapabilitySet{" + diff --git a/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java index 5a4ae1f4ff6..a2f27ba42bc 100644 --- a/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java +++ b/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java @@ -6,6 +6,8 @@ import com.yahoo.security.KeyUtils; import com.yahoo.security.SubjectAlternativeName.Type; import com.yahoo.security.X509CertificateBuilder; import com.yahoo.security.tls.policy.AuthorizedPeers; +import com.yahoo.security.tls.policy.Capability; +import com.yahoo.security.tls.policy.CapabilitySet; import com.yahoo.security.tls.policy.PeerPolicy; import com.yahoo.security.tls.policy.RequiredPeerCredential; import com.yahoo.security.tls.policy.RequiredPeerCredential.Field; @@ -19,6 +21,8 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.List; +import java.util.Optional; +import java.util.Set; import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.CN; @@ -46,7 +50,7 @@ public class PeerAuthorizerTest { RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(POLICY_1, cnRequirement, sanRequirement)); - AuthorizationResult result = authorizer.authorizePeer(createCertificate("foo.matching.cn", asList("foo.matching.san", "foo.invalid.san"), emptyList())); + ConnectionAuthContext result = authorizer.authorizePeer(createCertificate("foo.matching.cn", asList("foo.matching.san", "foo.invalid.san"), emptyList())); assertAuthorized(result); assertThat(result.matchedPolicies()).containsOnly(POLICY_1); @@ -64,7 +68,7 @@ public class PeerAuthorizerTest { createPolicy(POLICY_1, cnRequirement, sanRequirement), createPolicy(POLICY_2, cnRequirement, sanRequirement)); - AuthorizationResult result = peerAuthorizer + ConnectionAuthContext result = peerAuthorizer .authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.matching.san"), emptyList())); assertAuthorized(result); assertThat(result.matchedPolicies()).containsOnly(POLICY_1, POLICY_2); @@ -76,7 +80,7 @@ public class PeerAuthorizerTest { createPolicy(POLICY_1, createRequiredCredential(CN, "*.matching.cn")), createPolicy(POLICY_2, createRequiredCredential(SAN_DNS, "*.matching.san"))); - AuthorizationResult result = peerAuthorizer.authorizePeer(createCertificate("foo.invalid.cn", singletonList("foo.matching.san"), emptyList())); + ConnectionAuthContext result = peerAuthorizer.authorizePeer(createCertificate("foo.invalid.cn", singletonList("foo.matching.san"), emptyList())); assertAuthorized(result); assertThat(result.matchedPolicies()).containsOnly(POLICY_2); } @@ -101,13 +105,28 @@ public class PeerAuthorizerTest { RequiredPeerCredential sanUriRequirement = createRequiredCredential(SAN_URI, "myscheme://my/*/uri"); PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(POLICY_1, cnRequirement, sanUriRequirement)); - AuthorizationResult result = authorizer.authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.irrelevant.san"), singletonList("myscheme://my/matching/uri"))); + ConnectionAuthContext result = authorizer.authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.irrelevant.san"), singletonList("myscheme://my/matching/uri"))); assertAuthorized(result); assertThat(result.matchedPolicies()).containsOnly(POLICY_1); assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.matching.cn", emptyList(), singletonList("myscheme://my/nonmatching/url")))); } + @Test + public void auth_context_contains_union_of_granted_capabilities_from_policies() { + RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); + RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); + + PeerAuthorizer peerAuthorizer = createPeerAuthorizer( + createPolicy(POLICY_1, List.of(Capability.SLOBROK__API, Capability.CONTENT__DOCUMENT_API), List.of(cnRequirement)), + createPolicy(POLICY_2, List.of(Capability.SLOBROK__API, Capability.CONTENT__SEARCH_API), List.of(sanRequirement))); + + var result = peerAuthorizer + .authorizePeer(createCertificate("foo.matching.cn", List.of("foo.matching.san"), List.of())); + assertAuthorized(result); + assertCapabiltiesGranted(result, Set.of(Capability.SLOBROK__API, Capability.CONTENT__DOCUMENT_API, Capability.CONTENT__SEARCH_API)); + } + private static X509Certificate createCertificate(String subjectCn, List sanDns, List sanUri) { X509CertificateBuilder builder = X509CertificateBuilder.fromKeypair( @@ -134,12 +153,20 @@ public class PeerAuthorizerTest { return new PeerPolicy(name, asList(requiredCredentials)); } - private static void assertAuthorized(AuthorizationResult result) { + private static PeerPolicy createPolicy(String name, List caps, List creds) { + return new PeerPolicy(name, Optional.empty(), CapabilitySet.from(caps), creds); + } + + private static void assertAuthorized(ConnectionAuthContext result) { assertTrue(result.succeeded()); } - private static void assertUnauthorized(AuthorizationResult result) { + private static void assertUnauthorized(ConnectionAuthContext result) { assertFalse(result.succeeded()); } + private static void assertCapabiltiesGranted(ConnectionAuthContext ctx, Set expected) { + assertThat(ctx.capabilities().asSet()).containsOnly(expected.toArray(new Capability[0])); + } + } -- cgit v1.2.3