summaryrefslogtreecommitdiffstats
path: root/jdisc-security-filters
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2018-06-20 13:09:55 +0200
committerBjørn Christian Seime <bjorncs@oath.com>2018-06-20 13:15:11 +0200
commitb38471d94959eb172e82ee102404bc669a14d96b (patch)
treed1a9e399c6c4f4bb1ee76a1b80454125cfbbd0fe /jdisc-security-filters
parentbf74c1a064739c123921a2e85e9427bae7019290 (diff)
Add new Athenz security filter based on ZPE
- Allow flexible configuration of filter using a resource mapper - Add helper class to extract role and identity from role certificates
Diffstat (limited to 'jdisc-security-filters')
-rw-r--r--jdisc-security-filters/pom.xml7
-rw-r--r--jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilter.java145
-rw-r--r--jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/RequestResourceMapper.java37
-rw-r--r--jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/StaticRequestResourceMapper.java33
-rw-r--r--jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/package-info.java8
-rw-r--r--jdisc-security-filters/src/main/resources/configdefinitions/athenz-authorization-filter.def8
-rw-r--r--jdisc-security-filters/src/main/resources/configdefinitions/static-request-resource-mapper.def8
-rw-r--r--jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilterTest.java103
8 files changed, 349 insertions, 0 deletions
diff --git a/jdisc-security-filters/pom.xml b/jdisc-security-filters/pom.xml
index bcee244ef69..5e8356e94f1 100644
--- a/jdisc-security-filters/pom.xml
+++ b/jdisc-security-filters/pom.xml
@@ -23,6 +23,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespa-athenz</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<!-- test -->
<dependency>
@@ -41,6 +47,7 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
+
</dependencies>
<build>
diff --git a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilter.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilter.java
new file mode 100644
index 00000000000..5bbf111e5b2
--- /dev/null
+++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilter.java
@@ -0,0 +1,145 @@
+// 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;
+import com.yahoo.jdisc.http.filter.DiscFilterRequest;
+import com.yahoo.jdisc.http.filter.security.athenz.AthenzAuthorizationFilterConfig.CredentialsToVerify;
+import com.yahoo.jdisc.http.filter.security.athenz.RequestResourceMapper.ResourceNameAndAction;
+import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
+import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.athenz.api.AthenzPrincipal;
+import com.yahoo.vespa.athenz.api.AthenzResourceName;
+import com.yahoo.vespa.athenz.api.AthenzRole;
+import com.yahoo.vespa.athenz.api.ZToken;
+import com.yahoo.vespa.athenz.tls.AthenzX509CertificateUtils;
+import com.yahoo.vespa.athenz.zpe.AccessCheckResult;
+import com.yahoo.vespa.athenz.zpe.DefaultZpe;
+import com.yahoo.vespa.athenz.zpe.Zpe;
+
+import java.security.cert.X509Certificate;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * An Athenz security filter that uses a configured action and resource name to control access.
+ *
+ * @author bjorncs
+ */
+public class AthenzAuthorizationFilter extends JsonSecurityRequestFilterBase {
+
+ private final String headerName;
+ private final Zpe zpe;
+ private final RequestResourceMapper requestResourceMapper;
+ private final CredentialsToVerify.Enum credentialsToVerify;
+
+ @Inject
+ public AthenzAuthorizationFilter(AthenzAuthorizationFilterConfig config, RequestResourceMapper resourceMapper) {
+ this(config, resourceMapper, new DefaultZpe());
+ }
+
+ AthenzAuthorizationFilter(AthenzAuthorizationFilterConfig config,
+ RequestResourceMapper resourceMapper,
+ Zpe zpe) {
+ this.headerName = config.roleTokenHeaderName();
+ this.credentialsToVerify = config.credentialsToVerify();
+ this.requestResourceMapper = resourceMapper;
+ this.zpe = zpe;
+ }
+
+ @Override
+ protected Optional<ErrorResponse> filter(DiscFilterRequest request) {
+ Optional<ResourceNameAndAction> resourceMapping =
+ requestResourceMapper.getResourceNameAndMapping(request.getMethod(), request.getRequestURI(), request.getQueryString());
+ if (!resourceMapping.isPresent()) {
+ return Optional.empty();
+ }
+ Optional<X509Certificate> roleCertificate = getRoleCertificate(request);
+ Optional<ZToken> roleToken = getRoleToken(request, headerName);
+ switch (credentialsToVerify) {
+ case CERTIFICATE_ONLY: {
+ if (!roleCertificate.isPresent()) {
+ return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, "Missing client certificate"));
+ }
+ return checkAccessAllowed(roleCertificate.get(), resourceMapping.get(), request);
+ }
+ case TOKEN_ONLY: {
+ if (!roleToken.isPresent()) {
+ return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED,
+ String.format("Role token header '%s' is missing or does not have a value.", headerName)));
+ }
+ return checkAccessAllowed(roleToken.get(), resourceMapping.get(), request);
+ }
+ case ANY: {
+ if (!roleCertificate.isPresent() && !roleToken.isPresent()) {
+ return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, "Both role token and role certificate is missing"));
+ }
+ if (roleCertificate.isPresent()) {
+ return checkAccessAllowed(roleCertificate.get(), resourceMapping.get(), request);
+ } else {
+ return checkAccessAllowed(roleToken.get(), resourceMapping.get(), request);
+ }
+ }
+ default: {
+ throw new IllegalStateException("Unexpected mode: " + credentialsToVerify);
+ }
+ }
+ }
+
+ private static Optional<X509Certificate> getRoleCertificate(DiscFilterRequest request) {
+ return Optional.of(request.getClientCertificateChain())
+ .filter(chain -> !chain.isEmpty())
+ .map(chain -> chain.get(0))
+ .filter(AthenzX509CertificateUtils::isAthenzRoleCertificate);
+ }
+
+ private static Optional<ZToken> getRoleToken(DiscFilterRequest request, String headerName) {
+ return Optional.ofNullable(request.getHeader(headerName))
+ .filter(token -> !token.isEmpty())
+ .map(ZToken::new);
+ }
+
+ private Optional<ErrorResponse> checkAccessAllowed(X509Certificate certificate,
+ ResourceNameAndAction resourceNameAndAction,
+ DiscFilterRequest request) {
+ return checkAccessAllowed(
+ certificate, resourceNameAndAction, request, zpe::checkAccessAllowed, AthenzAuthorizationFilter::createPrincipal);
+ }
+
+ private Optional<ErrorResponse> checkAccessAllowed(ZToken roleToken,
+ ResourceNameAndAction resourceNameAndAction,
+ DiscFilterRequest request) {
+ return checkAccessAllowed(
+ roleToken, resourceNameAndAction, request, zpe::checkAccessAllowed, AthenzAuthorizationFilter::createPrincipal);
+ }
+
+ private static <C> Optional<ErrorResponse> checkAccessAllowed(C credentials,
+ ResourceNameAndAction resAndAction,
+ DiscFilterRequest request,
+ ZpeCheck<C> accessCheck,
+ Function<C, AthenzPrincipal> principalFactory) {
+ AccessCheckResult accessCheckResult = accessCheck.checkAccess(credentials, resAndAction.resourceName(), resAndAction.action());
+ if (accessCheckResult == AccessCheckResult.ALLOW) {
+ request.setUserPrincipal(principalFactory.apply(credentials));
+ return Optional.empty();
+ }
+ return Optional.of(new ErrorResponse(Response.Status.FORBIDDEN, "Access forbidden: " + accessCheckResult.getDescription()));
+ }
+
+ private static AthenzPrincipal createPrincipal(X509Certificate certificate) {
+ AthenzIdentity identity = AthenzX509CertificateUtils.getIdentityFromRoleCertificate(certificate);
+ AthenzRole role = AthenzX509CertificateUtils.getRolesFromRoleCertificate(certificate);
+ return new AthenzPrincipal(identity, singletonList(role));
+ }
+
+ private static AthenzPrincipal createPrincipal(ZToken roleToken) {
+ return new AthenzPrincipal(roleToken.getIdentity(), roleToken.getRoles());
+ }
+
+ @FunctionalInterface private interface ZpeCheck<C> {
+ AccessCheckResult checkAccess(C credentials, AthenzResourceName resourceName, String action);
+ }
+
+}
diff --git a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/RequestResourceMapper.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/RequestResourceMapper.java
new file mode 100644
index 00000000000..00c3c27d57b
--- /dev/null
+++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/RequestResourceMapper.java
@@ -0,0 +1,37 @@
+// 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.yahoo.vespa.athenz.api.AthenzResourceName;
+
+import java.util.Optional;
+
+/**
+ * Maps a request to an {@link AthenzResourceName} and an action.
+ *
+ * @author bjorncs
+ */
+public interface RequestResourceMapper {
+
+ /**
+ * @return A resource name + action to use for access control, empty if no access control should be performed.
+ */
+ Optional<ResourceNameAndAction> getResourceNameAndMapping(String method, String uriPath, String uriQuery);
+
+ class ResourceNameAndAction {
+ private final AthenzResourceName resourceName;
+ private final String action;
+
+ public ResourceNameAndAction(AthenzResourceName resourceName, String action) {
+ this.resourceName = resourceName;
+ this.action = action;
+ }
+
+ public AthenzResourceName resourceName() {
+ return resourceName;
+ }
+
+ public String action() {
+ return action;
+ }
+ }
+}
diff --git a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/StaticRequestResourceMapper.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/StaticRequestResourceMapper.java
new file mode 100644
index 00000000000..1f9090876f7
--- /dev/null
+++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/StaticRequestResourceMapper.java
@@ -0,0 +1,33 @@
+// 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.vespa.athenz.api.AthenzResourceName;
+
+import java.util.Optional;
+
+/**
+ * A simple {@link RequestResourceMapper} that uses a fixed resource name and action
+ *
+ * @author bjorncs
+ */
+public class StaticRequestResourceMapper implements RequestResourceMapper {
+
+ private final AthenzResourceName resourceName;
+ private final String action;
+
+ @Inject
+ public StaticRequestResourceMapper(StaticRequestResourceMapperConfig config) {
+ this(AthenzResourceName.fromString(config.resourceName()), config.action());
+ }
+
+ StaticRequestResourceMapper(AthenzResourceName resourceName, String action) {
+ this.resourceName = resourceName;
+ this.action = action;
+ }
+
+ @Override
+ public Optional<ResourceNameAndAction> getResourceNameAndMapping(String method, String uriPath, String uriQuery) {
+ return Optional.of(new ResourceNameAndAction(resourceName, action));
+ }
+}
diff --git a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/package-info.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/package-info.java
new file mode 100644
index 00000000000..6ec5bd4322e
--- /dev/null
+++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author bjorncs
+ */
+@ExportPackage
+package com.yahoo.jdisc.http.filter.security.athenz;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/jdisc-security-filters/src/main/resources/configdefinitions/athenz-authorization-filter.def b/jdisc-security-filters/src/main/resources/configdefinitions/athenz-authorization-filter.def
new file mode 100644
index 00000000000..c60b7a125f8
--- /dev/null
+++ b/jdisc-security-filters/src/main/resources/configdefinitions/athenz-authorization-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
+
+# Which credentials to verify. Note: ANY will prioritize token over certificate if both are present.
+credentialsToVerify enum { CERTIFICATE_ONLY, TOKEN_ONLY, ANY } default=ANY
+
+# Name of header which includes role token. Must be set if 'credentialsTypeRequired' is set to TOKEN_ONLY or ANY.
+roleTokenHeaderName string default=""
diff --git a/jdisc-security-filters/src/main/resources/configdefinitions/static-request-resource-mapper.def b/jdisc-security-filters/src/main/resources/configdefinitions/static-request-resource-mapper.def
new file mode 100644
index 00000000000..de89c1f9198
--- /dev/null
+++ b/jdisc-security-filters/src/main/resources/configdefinitions/static-request-resource-mapper.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
+
+# Athenz resource name on format '<domain-name>:<entity-name>'
+resourceName string
+
+# Action name
+action string \ No newline at end of file
diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilterTest.java
new file mode 100644
index 00000000000..ff7f9af11c6
--- /dev/null
+++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilterTest.java
@@ -0,0 +1,103 @@
+package com.yahoo.jdisc.http.filter.security.athenz;
+
+import com.yahoo.container.jdisc.RequestHandlerTestDriver;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.http.filter.DiscFilterRequest;
+import com.yahoo.vespa.athenz.api.AthenzResourceName;
+import com.yahoo.vespa.athenz.api.ZToken;
+import com.yahoo.vespa.athenz.zpe.AccessCheckResult;
+import com.yahoo.vespa.athenz.zpe.Zpe;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.security.cert.X509Certificate;
+
+import static com.yahoo.jdisc.http.filter.security.athenz.AthenzAuthorizationFilterConfig.CredentialsToVerify.Enum.ANY;
+import static java.util.Collections.emptyList;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author bjorncs
+ */
+public class AthenzAuthorizationFilterTest {
+
+ private static final AthenzResourceName RESOURCE_NAME = new AthenzResourceName("domain", "my-resource-name");
+ private static final String ACTION = "update";
+ private static final String HEADER_NAME = "Athenz-Role-Token";
+ private static final AthenzAuthorizationFilterConfig CONFIG = createConfig();
+
+ private static AthenzAuthorizationFilterConfig createConfig() {
+ return new AthenzAuthorizationFilterConfig(
+ new AthenzAuthorizationFilterConfig.Builder()
+ .roleTokenHeaderName(HEADER_NAME)
+ .credentialsToVerify(ANY));
+ }
+
+ @Test
+ public void accepts_valid_requests() {
+ AthenzAuthorizationFilter filter =
+ new AthenzAuthorizationFilter(
+ CONFIG, new StaticRequestResourceMapper(RESOURCE_NAME, ACTION), new AllowingZpe());
+
+ RequestHandlerTestDriver.MockResponseHandler responseHandler = new RequestHandlerTestDriver.MockResponseHandler();
+ filter.filter(createRequest(), responseHandler);
+
+ assertNull(responseHandler.getResponse());
+ }
+
+ @Test
+ public void returns_error_on_forbidden_requests() {
+ AthenzAuthorizationFilter filter =
+ new AthenzAuthorizationFilter(
+ CONFIG, new StaticRequestResourceMapper(RESOURCE_NAME, ACTION), new DenyingZpe());
+
+ RequestHandlerTestDriver.MockResponseHandler responseHandler = new RequestHandlerTestDriver.MockResponseHandler();
+ filter.filter(createRequest(), responseHandler);
+
+ Response response = responseHandler.getResponse();
+ assertNotNull(response);
+ assertEquals(403, response.getStatus());
+ String content = responseHandler.readAll();
+ assertThat(content, containsString(AccessCheckResult.DENY.getDescription()));
+ }
+
+ private static DiscFilterRequest createRequest() {
+ DiscFilterRequest request = Mockito.mock(DiscFilterRequest.class);
+ when(request.getHeader(HEADER_NAME)).thenReturn("v=Z1;d=domain;r=my-role;p=my-domain.my-service");
+ when(request.getMethod()).thenReturn("GET");
+ when(request.getRequestURI()).thenReturn("/my/path");
+ when(request.getQueryString()).thenReturn(null);
+ when(request.getClientCertificateChain()).thenReturn(emptyList());
+ return request;
+ }
+
+ static class AllowingZpe implements Zpe {
+ @Override
+ public AccessCheckResult checkAccessAllowed(ZToken roleToken, AthenzResourceName resourceName, String action) {
+ return AccessCheckResult.ALLOW;
+ }
+
+ @Override
+ public AccessCheckResult checkAccessAllowed(X509Certificate roleCertificate, AthenzResourceName resourceName, String action) {
+ return AccessCheckResult.ALLOW;
+ }
+ }
+
+ static class DenyingZpe implements Zpe {
+ @Override
+ public AccessCheckResult checkAccessAllowed(ZToken roleToken, AthenzResourceName resourceName, String action) {
+ return AccessCheckResult.DENY;
+ }
+
+ @Override
+ public AccessCheckResult checkAccessAllowed(X509Certificate roleCertificate, AthenzResourceName resourceName, String action) {
+ return AccessCheckResult.ALLOW;
+ }
+ }
+
+} \ No newline at end of file