diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-07-09 16:44:38 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-07-26 13:07:33 +0200 |
commit | c28a15d4b1bd40aae6097114b215303646f9fc34 (patch) | |
tree | 7332272bbf302f717cc4b55b177d06d94fc099f7 | |
parent | 2525dfe510c7488f7992eac629fd9e69f8aebdae (diff) |
Move AthenzPrincipalFilter to jdisc-security-filters
-rw-r--r-- | controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java | 8 | ||||
-rw-r--r-- | controller-server/src/main/resources/configdefinitions/athenz.def | 3 | ||||
-rw-r--r-- | jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java) | 13 | ||||
-rw-r--r-- | jdisc-security-filters/src/main/resources/configdefinitions/athenz-principal-filter.def | 8 | ||||
-rw-r--r-- | jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilterTest.java (renamed from controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilterTest.java) | 139 |
5 files changed, 85 insertions, 86 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java index 0aa5c89c971..26cd9f2e9b8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java @@ -4,6 +4,8 @@ package com.yahoo.vespa.hosted.controller.athenz.filter; import com.google.inject.Inject; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.security.athenz.AthenzPrincipalFilter; +import com.yahoo.jdisc.http.filter.security.athenz.AthenzPrincipalFilterConfig; import com.yahoo.jdisc.http.filter.security.cors.CorsFilterConfig; import com.yahoo.log.LogLevel; import com.yahoo.vespa.athenz.api.AthenzPrincipal; @@ -36,10 +38,10 @@ public class UserAuthWithAthenzPrincipalFilter extends AthenzPrincipalFilter { private final String principalHeaderName; @Inject - public UserAuthWithAthenzPrincipalFilter(AthenzConfig athenzConfig, CorsFilterConfig corsConfig) { - super(athenzConfig, corsConfig); + public UserAuthWithAthenzPrincipalFilter(AthenzPrincipalFilterConfig filterConfig, AthenzConfig athenzConfig, CorsFilterConfig corsConfig) { + super(filterConfig, corsConfig); this.userAuthenticationPassThruAttribute = athenzConfig.userAuthenticationPassThruAttribute(); - this.principalHeaderName = athenzConfig.principalHeaderName(); + this.principalHeaderName = filterConfig.principalHeaderName(); } @Override diff --git a/controller-server/src/main/resources/configdefinitions/athenz.def b/controller-server/src/main/resources/configdefinitions/athenz.def index 8026c0d7f44..f8d65c25e47 100644 --- a/controller-server/src/main/resources/configdefinitions/athenz.def +++ b/controller-server/src/main/resources/configdefinitions/athenz.def @@ -42,6 +42,3 @@ service.privateKeySecretName string # Expiry of service principal token and certificate service.credentialsExpiryMinutes int default=43200 # 30 days - -# Path to athenz.conf file -athenzConfFile string diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java index b7ede7635c6..ad6c82138e1 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java +++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java @@ -1,5 +1,5 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.athenz.filter; +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.security.athenz; import com.google.inject.Inject; import com.yahoo.jdisc.Response; @@ -9,9 +9,7 @@ import com.yahoo.jdisc.http.filter.security.cors.CorsRequestFilterBase; import com.yahoo.vespa.athenz.api.AthenzPrincipal; import com.yahoo.vespa.athenz.api.NToken; import com.yahoo.vespa.athenz.utils.AthenzIdentities; -import com.yahoo.vespa.athenz.utils.ntoken.AthenzConfTruststore; import com.yahoo.vespa.athenz.utils.ntoken.NTokenValidator; -import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig; import java.nio.file.Paths; import java.security.cert.X509Certificate; @@ -31,15 +29,16 @@ import java.util.Set; * * @author bjorncs */ -// TODO bjorncs: Move this class to jdisc-security-filters bundle public class AthenzPrincipalFilter extends CorsRequestFilterBase { private final NTokenValidator validator; private final String principalTokenHeader; @Inject - public AthenzPrincipalFilter(AthenzConfig athenzConfig, CorsFilterConfig corsConfig) { - this(new NTokenValidator(Paths.get(athenzConfig.athenzConfFile())), athenzConfig.principalHeaderName(), new HashSet<>(corsConfig.allowedUrls())); + public AthenzPrincipalFilter(AthenzPrincipalFilterConfig athenzPrincipalFilterConfig, CorsFilterConfig corsConfig) { + this(new NTokenValidator(Paths.get(athenzPrincipalFilterConfig.athenzConfFile())), + athenzPrincipalFilterConfig.principalHeaderName(), + new HashSet<>(corsConfig.allowedUrls())); } AthenzPrincipalFilter(NTokenValidator validator, diff --git a/jdisc-security-filters/src/main/resources/configdefinitions/athenz-principal-filter.def b/jdisc-security-filters/src/main/resources/configdefinitions/athenz-principal-filter.def new file mode 100644 index 00000000000..59e481b0d80 --- /dev/null +++ b/jdisc-security-filters/src/main/resources/configdefinitions/athenz-principal-filter.def @@ -0,0 +1,8 @@ +# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http.filter.security.athenz + +# Principal header name +principalHeaderName string default="Athenz-Principal-Auth" + +# Path to athenz.conf file +athenzConfFile string diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilterTest.java index d6481b741dd..be5ab9c1d77 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilterTest.java +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilterTest.java @@ -1,7 +1,6 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.athenz.filter; +package com.yahoo.jdisc.http.filter.security.athenz; -import com.yahoo.application.container.handler.Request; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.handler.ContentChannel; import com.yahoo.jdisc.handler.ReadableContentChannel; @@ -14,9 +13,7 @@ import com.yahoo.vespa.athenz.api.NToken; import com.yahoo.vespa.athenz.tls.KeyAlgorithm; import com.yahoo.vespa.athenz.tls.KeyUtils; import com.yahoo.vespa.athenz.tls.X509CertificateBuilder; -import com.yahoo.vespa.athenz.utils.ntoken.AthenzTruststore; import com.yahoo.vespa.athenz.utils.ntoken.NTokenValidator; -import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper; import org.junit.Before; import org.junit.Test; @@ -29,20 +26,22 @@ import java.security.KeyPair; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import static com.yahoo.jdisc.Response.Status.UNAUTHORIZED; import static com.yahoo.vespa.athenz.tls.SignatureAlgorithm.SHA256_WITH_RSA; +import static java.util.Collections.emptyList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.joining; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @author bjorncs @@ -56,98 +55,115 @@ public class AthenzPrincipalFilterTest { private static final String ORIGIN = "http://localhost"; private static final Set<String> CORS_ALLOWED_URLS = singleton(ORIGIN); - private NTokenValidatorMock validator; - private ResponseHandlerMock responseHandler; + private NTokenValidator validator; @Before public void before() { - this.validator = new NTokenValidatorMock(); - this.responseHandler = new ResponseHandlerMock(); + validator = mock(NTokenValidator.class); } @Test public void valid_ntoken_is_accepted() { - Request request = defaultRequest(); - + DiscFilterRequest request = createRequestMock(); AthenzPrincipal principal = new AthenzPrincipal(IDENTITY, NTOKEN); - validator.add(NTOKEN, principal); + when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(NTOKEN.getRawToken()); + when(request.getClientCertificateChain()).thenReturn(emptyList()); + when(validator.validate(NTOKEN)).thenReturn(principal); AthenzPrincipalFilter filter = new AthenzPrincipalFilter(validator, ATHENZ_PRINCIPAL_HEADER, CORS_ALLOWED_URLS); - DiscFilterRequest filterRequest = new ApplicationRequestToDiscFilterRequestWrapper(request); - filter.filter(filterRequest, new ResponseHandlerMock()); + filter.filter(request, new ResponseHandlerMock()); + + verify(request).setUserPrincipal(principal); + } - assertEquals(principal, filterRequest.getUserPrincipal()); + private DiscFilterRequest createRequestMock() { + DiscFilterRequest request = mock(DiscFilterRequest.class); + when(request.getHeader("Origin")).thenReturn(ORIGIN); + return request; } @Test public void missing_token_and_certificate_is_unauthorized() { + DiscFilterRequest request = createRequestMock(); + when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(null); + when(request.getClientCertificateChain()).thenReturn(emptyList()); + + ResponseHandlerMock responseHandler = new ResponseHandlerMock(); + AthenzPrincipalFilter filter = new AthenzPrincipalFilter(validator, ATHENZ_PRINCIPAL_HEADER, CORS_ALLOWED_URLS); - DiscFilterRequest filterRequest = new ApplicationRequestToDiscFilterRequestWrapper(new Request("/")); - filter.filter(filterRequest, responseHandler); + filter.filter(request, responseHandler); assertUnauthorized(responseHandler, "Unable to authenticate Athenz identity"); } @Test public void invalid_token_is_unauthorized() { - Request request = defaultRequest(); + DiscFilterRequest request = createRequestMock(); + String errorMessage = "Invalid token"; + when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(NTOKEN.getRawToken()); + when(request.getClientCertificateChain()).thenReturn(emptyList()); + when(validator.validate(NTOKEN)).thenThrow(new NTokenValidator.InvalidTokenException(errorMessage)); + + ResponseHandlerMock responseHandler = new ResponseHandlerMock(); AthenzPrincipalFilter filter = new AthenzPrincipalFilter(validator, ATHENZ_PRINCIPAL_HEADER, CORS_ALLOWED_URLS); - DiscFilterRequest filterRequest = new ApplicationRequestToDiscFilterRequestWrapper(request); - filter.filter(filterRequest, responseHandler); + filter.filter(request, responseHandler); - String errorMessage = "Invalid token"; assertUnauthorized(responseHandler, errorMessage); } @Test public void certificate_is_accepted() { + DiscFilterRequest request = createRequestMock(); + when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(null); + when(request.getClientCertificateChain()).thenReturn(singletonList(CERTIFICATE)); + + ResponseHandlerMock responseHandler = new ResponseHandlerMock(); + AthenzPrincipalFilter filter = new AthenzPrincipalFilter(validator, ATHENZ_PRINCIPAL_HEADER, CORS_ALLOWED_URLS); - DiscFilterRequest filterRequest = new ApplicationRequestToDiscFilterRequestWrapper(new Request("/"), singletonList(CERTIFICATE)); - filter.filter(filterRequest, responseHandler); + filter.filter(request, responseHandler); AthenzPrincipal expectedPrincipal = new AthenzPrincipal(IDENTITY); - assertEquals(expectedPrincipal, filterRequest.getUserPrincipal()); + verify(request).setUserPrincipal(expectedPrincipal); } @Test public void both_ntoken_and_certificate_is_accepted() { - Request request = defaultRequest(); - + DiscFilterRequest request = createRequestMock(); AthenzPrincipal principalWithToken = new AthenzPrincipal(IDENTITY, NTOKEN); - validator.add(NTOKEN, principalWithToken); + when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(NTOKEN.getRawToken()); + when(request.getClientCertificateChain()).thenReturn(singletonList(CERTIFICATE)); + when(validator.validate(NTOKEN)).thenReturn(principalWithToken); + + ResponseHandlerMock responseHandler = new ResponseHandlerMock(); AthenzPrincipalFilter filter = new AthenzPrincipalFilter(validator, ATHENZ_PRINCIPAL_HEADER, CORS_ALLOWED_URLS); - DiscFilterRequest filterRequest = new ApplicationRequestToDiscFilterRequestWrapper(request, singletonList(CERTIFICATE)); - filter.filter(filterRequest, responseHandler); + filter.filter(request, responseHandler); - assertEquals(principalWithToken, filterRequest.getUserPrincipal()); + verify(request).setUserPrincipal(principalWithToken); } @Test public void conflicting_ntoken_and_certificate_is_unauthorized() { - Request request = defaultRequest(); - validator.add(NTOKEN, new AthenzPrincipal(IDENTITY)); - + DiscFilterRequest request = createRequestMock(); AthenzUser conflictingIdentity = AthenzUser.fromUserId("mallory"); - DiscFilterRequest filterRequest = new ApplicationRequestToDiscFilterRequestWrapper(request, singletonList(createSelfSignedCertificate(conflictingIdentity))); + when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(NTOKEN.getRawToken()); + when(request.getClientCertificateChain()) + .thenReturn(singletonList(createSelfSignedCertificate(conflictingIdentity))); + when(validator.validate(NTOKEN)).thenReturn(new AthenzPrincipal(IDENTITY)); + + ResponseHandlerMock responseHandler = new ResponseHandlerMock(); + AthenzPrincipalFilter filter = new AthenzPrincipalFilter(validator, ATHENZ_PRINCIPAL_HEADER, CORS_ALLOWED_URLS); - filter.filter(filterRequest, responseHandler); + filter.filter(request, responseHandler); assertUnauthorized(responseHandler, "Identity in principal token does not match x509 CN"); } - private static Request defaultRequest() { - Request request = new Request("/"); - request.getHeaders().add("Origin", ORIGIN); - request.getHeaders().add(ATHENZ_PRINCIPAL_HEADER, NTOKEN.getRawToken()); - return request; - } - private static void assertUnauthorized(ResponseHandlerMock responseHandler, String expectedMessageSubstring) { - assertNotNull(responseHandler.response); - assertEquals(UNAUTHORIZED, responseHandler.response.getStatus()); - assertTrue(responseHandler.getResponseContent().contains(expectedMessageSubstring)); + assertThat(responseHandler.response, notNullValue()); + assertThat(responseHandler.response.getStatus(), equalTo(UNAUTHORIZED)); + assertThat(responseHandler.getResponseContent(), containsString(expectedMessageSubstring)); } private static class ResponseHandlerMock implements ResponseHandler { @@ -172,29 +188,6 @@ public class AthenzPrincipalFilterTest { } - private static class NTokenValidatorMock extends NTokenValidator { - - private final Map<NToken, AthenzPrincipal> validTokens = new HashMap<>(); - - NTokenValidatorMock() { - super((AthenzTruststore)null); - } - - public NTokenValidatorMock add(NToken token, AthenzPrincipal principal) { - validTokens.put(token, principal); - return this; - } - - @Override - public AthenzPrincipal validate(NToken token) throws InvalidTokenException { - if (!validTokens.containsKey(token)) { - throw new InvalidTokenException("Invalid token"); - } - return validTokens.get(token); - } - - } - private static X509Certificate createSelfSignedCertificate(AthenzIdentity identity) { KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 512); X500Principal x500Name = new X500Principal("CN="+ identity.getFullName()); |