summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMorten Tokle <morten.tokle@gmail.com>2018-06-21 13:04:58 +0200
committerGitHub <noreply@github.com>2018-06-21 13:04:58 +0200
commit50ec63c3a06da7bef8cc8706decb1872a10f4389 (patch)
tree14f5b40fb0c7e4cbd358df0366f3dff0e4843262
parent44820b516a16cda3e82ba7beab8e2fe5bd3f4831 (diff)
parent12afa2bee25c6c22448f9d79acc3a38e68a4cf45 (diff)
Merge pull request #6245 from vespa-engine/bjorncs/athenz-filter
Bjorncs/athenz filter
-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/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBase.java1
-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
-rw-r--r--parent/pom.xml5
-rw-r--r--vespa-athenz/pom.xml28
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentityCertificate.java27
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java22
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzResourceName.java74
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java30
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java58
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/AuthorizationResult.java46
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/DefaultZpe.java29
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/Zpe.java17
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/package-info.java8
-rw-r--r--vespa-athenz/src/test/java/com/yahoo/vespa/athenz/api/AthenzResourceNameTest.java21
21 files changed, 679 insertions, 36 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..74e0ee36959
--- /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.AuthorizationResult;
+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.getResourceNameAndAction(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) {
+ AuthorizationResult authorizationResult = accessCheck.checkAccess(credentials, resAndAction.resourceName(), resAndAction.action());
+ if (authorizationResult == AuthorizationResult.ALLOW) {
+ request.setUserPrincipal(principalFactory.apply(credentials));
+ return Optional.empty();
+ }
+ return Optional.of(new ErrorResponse(Response.Status.FORBIDDEN, "Access forbidden: " + authorizationResult.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> {
+ AuthorizationResult 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..77709975cba
--- /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> getResourceNameAndAction(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..ded13a9def4
--- /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> getResourceNameAndAction(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/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBase.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBase.java
index e2440bc4c5f..ec8a93019b0 100644
--- a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBase.java
+++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBase.java
@@ -36,6 +36,7 @@ public abstract class JsonSecurityRequestFilterBase implements SecurityRequestFi
errorMessage.put("code", error.errorCode);
errorMessage.put("message", error.message);
error.response.headers().put("Content-Type", "application/json"); // Note: Overwrites header if already exists
+ error.response.headers().put("Cache-Control", "must-revalidate,no-cache,no-store");
try (FastContentWriter writer = ResponseDispatch.newInstance(error.response).connectFastWriter(responseHandler)) {
writer.write(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(errorMessage));
} catch (JsonProcessingException e) {
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..a5242571130
--- /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.AuthorizationResult;
+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(AuthorizationResult.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 AuthorizationResult checkAccessAllowed(ZToken roleToken, AthenzResourceName resourceName, String action) {
+ return AuthorizationResult.ALLOW;
+ }
+
+ @Override
+ public AuthorizationResult checkAccessAllowed(X509Certificate roleCertificate, AthenzResourceName resourceName, String action) {
+ return AuthorizationResult.ALLOW;
+ }
+ }
+
+ static class DenyingZpe implements Zpe {
+ @Override
+ public AuthorizationResult checkAccessAllowed(ZToken roleToken, AthenzResourceName resourceName, String action) {
+ return AuthorizationResult.DENY;
+ }
+
+ @Override
+ public AuthorizationResult checkAccessAllowed(X509Certificate roleCertificate, AthenzResourceName resourceName, String action) {
+ return AuthorizationResult.DENY;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/parent/pom.xml b/parent/pom.xml
index 10e93d4ffbf..aa75c124579 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -671,6 +671,11 @@
<version>${athenz.version}</version>
</dependency>
<dependency>
+ <groupId>com.yahoo.athenz</groupId>
+ <artifactId>athenz-zpe-java-client</artifactId>
+ <version>${athenz.version}</version>
+ </dependency>
+ <dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>2.6.0</version>
diff --git a/vespa-athenz/pom.xml b/vespa-athenz/pom.xml
index 7721d1829e5..75116812915 100644
--- a/vespa-athenz/pom.xml
+++ b/vespa-athenz/pom.xml
@@ -111,6 +111,34 @@
</exclusions>
</dependency>
<dependency>
+ <groupId>com.yahoo.athenz</groupId>
+ <artifactId>athenz-zpe-java-client</artifactId>
+ <scope>compile</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ </exclusion>
+ <!--Exclude all Jackson bundles provided by JDisc -->
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.1</version>
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentityCertificate.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentityCertificate.java
deleted file mode 100644
index 0e9e9432790..00000000000
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentityCertificate.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.athenz.api;
-
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-
-/**
- * @author bjorncs
- */
-public class AthenzIdentityCertificate {
-
- private final X509Certificate certificate;
- private final PrivateKey privateKey;
-
- public AthenzIdentityCertificate(X509Certificate certificate, PrivateKey privateKey) {
- this.certificate = certificate;
- this.privateKey = privateKey;
- }
-
- public X509Certificate getCertificate() {
- return certificate;
- }
-
- public PrivateKey getPrivateKey() {
- return privateKey;
- }
-}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java
index e96f5bd72d4..2330b1e439f 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java
@@ -2,9 +2,12 @@
package com.yahoo.vespa.athenz.api;
import java.security.Principal;
+import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import static java.util.Collections.emptyList;
+
/**
* @author bjorncs
*/
@@ -12,15 +15,24 @@ public class AthenzPrincipal implements Principal {
private final AthenzIdentity athenzIdentity;
private final NToken nToken;
+ private final List<AthenzRole> roles;
public AthenzPrincipal(AthenzIdentity athenzIdentity) {
- this(athenzIdentity, null);
+ this(athenzIdentity, null, emptyList());
+ }
+
+ public AthenzPrincipal(AthenzIdentity athenzIdentity, NToken nToken) {
+ this(athenzIdentity, nToken, emptyList());
}
- public AthenzPrincipal(AthenzIdentity athenzIdentity,
- NToken nToken) {
+ public AthenzPrincipal(AthenzIdentity identity, List<AthenzRole> roles) {
+ this(identity, null, roles);
+ }
+
+ private AthenzPrincipal(AthenzIdentity athenzIdentity, NToken nToken, List<AthenzRole> roles) {
this.athenzIdentity = athenzIdentity;
this.nToken = nToken;
+ this.roles = roles;
}
public AthenzIdentity getIdentity() {
@@ -40,6 +52,10 @@ public class AthenzPrincipal implements Principal {
return Optional.ofNullable(nToken);
}
+ public List<AthenzRole> getRoles() {
+ return roles;
+ }
+
@Override
public String toString() {
return "AthenzPrincipal{" +
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzResourceName.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzResourceName.java
new file mode 100644
index 00000000000..f7aa2affc86
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzResourceName.java
@@ -0,0 +1,74 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.api;
+
+import java.util.Objects;
+
+/**
+ * Athenz resource name
+ *
+ * @author bjorncs
+ */
+public class AthenzResourceName {
+
+ private final AthenzDomain domain;
+ private final String entityName;
+
+ public AthenzResourceName(AthenzDomain domain, String entityName) {
+ this.domain = domain;
+ this.entityName = entityName;
+ }
+
+ public AthenzResourceName(String domain, String entityName) {
+ this(new AthenzDomain(domain), entityName);
+ }
+
+ /**
+ * @param resourceName A resource name string on format 'domain:entity'
+ * @return the parsed resource name
+ */
+ public static AthenzResourceName fromString(String resourceName) {
+ String[] split = resourceName.split(":");
+ if (split.length != 2 || split[0].isEmpty() || split[1].isEmpty()) {
+ throw new IllegalArgumentException("Invalid resource name: " + resourceName);
+ }
+ return new AthenzResourceName(split[0], split[1]);
+ }
+
+ public AthenzDomain getDomain() {
+ return domain;
+ }
+
+ public String getDomainName() {
+ return domain.getName();
+ }
+
+ public String getEntityName() {
+ return entityName;
+ }
+
+ public String toResourceNameString() {
+ return String.format("%s:%s", domain.getName(), entityName);
+ }
+
+ @Override
+ public String toString() {
+ return "AthenzResourceName{" +
+ "domain=" + domain +
+ ", entityName='" + entityName + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AthenzResourceName that = (AthenzResourceName) o;
+ return Objects.equals(domain, that.domain) &&
+ Objects.equals(entityName, that.entityName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(domain, entityName);
+ }
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java
index ae520e66429..36c06132532 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java
@@ -1,7 +1,14 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.athenz.api;
+import com.yahoo.athenz.auth.token.RoleToken;
+import com.yahoo.vespa.athenz.utils.AthenzIdentities;
+
+import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static java.util.stream.Collectors.toList;
/**
* Represents an Athenz ZToken (role token)
@@ -10,27 +17,38 @@ import java.util.Objects;
*/
public class ZToken {
- private final String rawToken;
+ private final RoleToken token;
public ZToken(String rawToken) {
- this.rawToken = rawToken;
+ this.token = new RoleToken(rawToken);
}
public String getRawToken() {
- return rawToken;
+ return token.getSignedToken();
+ }
+
+ public AthenzIdentity getIdentity() {
+ return AthenzIdentities.from(token.getPrincipal());
}
+ public List<AthenzRole> getRoles() {
+ String domain = token.getDomain();
+ return token.getRoles().stream()
+ .map(roleName -> new AthenzRole(domain, roleName))
+ .collect(toList());}
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ZToken zToken = (ZToken) o;
- return Objects.equals(rawToken, zToken.rawToken);
+ return Objects.equals(getRawToken(), zToken.getRawToken());
}
@Override
public int hashCode() {
- return Objects.hash(rawToken);
+ return Objects.hash(getRawToken());
}
+
}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java
new file mode 100644
index 00000000000..46aca707be1
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java
@@ -0,0 +1,58 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.tls;
+
+import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.athenz.api.AthenzRole;
+import com.yahoo.vespa.athenz.utils.AthenzIdentities;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import static com.yahoo.vespa.athenz.tls.SubjectAlternativeName.Type.RFC822_NAME;
+
+/**
+ * Utility methods for Athenz issued x509 certificates
+ *
+ * @author bjorncs
+ */
+public class AthenzX509CertificateUtils {
+
+ private static final String COMMON_NAME_ROLE_DELIMITER = ":role.";
+
+ private AthenzX509CertificateUtils() {}
+
+ public static boolean isAthenzRoleCertificate(X509Certificate certificate) {
+ return isAthenzIssuedCertificate(certificate) &&
+ X509CertificateUtils.getSubjectCommonNames(certificate).get(0).contains(COMMON_NAME_ROLE_DELIMITER);
+ }
+
+ public static boolean isAthenzIssuedCertificate(X509Certificate certificate) {
+ return X509CertificateUtils.getIssuerCommonNames(certificate).stream()
+ .anyMatch(cn -> cn.equalsIgnoreCase("Yahoo Athenz CA") || cn.equalsIgnoreCase("Athenz AWS CA"));
+ }
+
+ public static AthenzIdentity getIdentityFromRoleCertificate(X509Certificate certificate) {
+ List<SubjectAlternativeName> sans = X509CertificateUtils.getSubjectAlternativeNames(certificate);
+ return sans.stream()
+ .filter(san -> san.getType() == RFC822_NAME)
+ .map(SubjectAlternativeName::getValue)
+ .map(AthenzX509CertificateUtils::getIdentityFromSanEmail)
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("Could not find identity in SAN: " + sans));
+ }
+
+ public static AthenzRole getRolesFromRoleCertificate(X509Certificate certificate) {
+ String commonName = X509CertificateUtils.getSubjectCommonNames(certificate).get(0);
+ int delimiterIndex = commonName.indexOf(COMMON_NAME_ROLE_DELIMITER);
+ String domain = commonName.substring(0, delimiterIndex);
+ String roleName = commonName.substring(delimiterIndex + COMMON_NAME_ROLE_DELIMITER.length());
+ return new AthenzRole(domain, roleName);
+ }
+
+ private static AthenzIdentity getIdentityFromSanEmail(String email) {
+ int separator = email.indexOf('@');
+ if (separator == -1) throw new IllegalArgumentException("Invalid SAN email: " + email);
+ return AthenzIdentities.from(email.substring(0, separator));
+ }
+
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/AuthorizationResult.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/AuthorizationResult.java
new file mode 100644
index 00000000000..faf05011af9
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/AuthorizationResult.java
@@ -0,0 +1,46 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.zpe;
+
+import com.yahoo.athenz.zpe.AuthZpeClient.AccessCheckStatus;
+
+import java.util.Arrays;
+
+/**
+ * The various types of access control results.
+ *
+ * @author bjorncs
+ */
+public enum AuthorizationResult {
+ ALLOW(AccessCheckStatus.ALLOW),
+ DENY(AccessCheckStatus.DENY),
+ DENY_NO_MATCH(AccessCheckStatus.DENY_NO_MATCH),
+ DENY_ROLETOKEN_EXPIRED(AccessCheckStatus.DENY_ROLETOKEN_EXPIRED),
+ DENY_ROLETOKEN_INVALID(AccessCheckStatus.DENY_ROLETOKEN_INVALID),
+ DENY_DOMAIN_MISMATCH(AccessCheckStatus.DENY_DOMAIN_MISMATCH),
+ DENY_DOMAIN_NOT_FOUND(AccessCheckStatus.DENY_DOMAIN_NOT_FOUND),
+ DENY_DOMAIN_EXPIRED(AccessCheckStatus.DENY_DOMAIN_EXPIRED),
+ DENY_DOMAIN_EMPTY(AccessCheckStatus.DENY_DOMAIN_EMPTY),
+ DENY_INVALID_PARAMETERS(AccessCheckStatus.DENY_INVALID_PARAMETERS),
+ DENY_CERT_MISMATCH_ISSUER(AccessCheckStatus.DENY_CERT_MISMATCH_ISSUER),
+ DENY_CERT_MISSING_SUBJECT(AccessCheckStatus.DENY_CERT_MISSING_SUBJECT),
+ DENY_CERT_MISSING_DOMAIN(AccessCheckStatus.DENY_CERT_MISSING_DOMAIN),
+ DENY_CERT_MISSING_ROLE_NAME(AccessCheckStatus.DENY_CERT_MISSING_ROLE_NAME);
+
+ private final AccessCheckStatus wrappedElement;
+
+ AuthorizationResult(AccessCheckStatus wrappedElement) {
+ this.wrappedElement = wrappedElement;
+ }
+
+ public String getDescription() {
+ return wrappedElement.toString();
+ }
+
+ static AuthorizationResult fromAccessCheckStatus(AccessCheckStatus accessCheckStatus) {
+ return Arrays.stream(values())
+ .filter(value -> value.wrappedElement == accessCheckStatus)
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("Unknown status: " + accessCheckStatus));
+ }
+
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/DefaultZpe.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/DefaultZpe.java
new file mode 100644
index 00000000000..a02d9c7a97a
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/DefaultZpe.java
@@ -0,0 +1,29 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.zpe;
+
+import com.yahoo.athenz.zpe.AuthZpeClient;
+import com.yahoo.vespa.athenz.api.AthenzResourceName;
+import com.yahoo.vespa.athenz.api.ZToken;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * The default implementation of {@link Zpe}.
+ * This implementation is currently based on the official Athenz ZPE library.
+ *
+ * @author bjorncs
+ */
+public class DefaultZpe implements Zpe {
+ @Override
+ public AuthorizationResult checkAccessAllowed(ZToken roleToken, AthenzResourceName resourceName, String action) {
+ return AuthorizationResult.fromAccessCheckStatus(
+ AuthZpeClient.allowAccess(roleToken.getRawToken(), resourceName.toResourceNameString(), action));
+ }
+
+ @Override
+ public AuthorizationResult checkAccessAllowed(X509Certificate roleCertificate, AthenzResourceName resourceName, String action) {
+ return AuthorizationResult.fromAccessCheckStatus(
+ AuthZpeClient.allowAccess(roleCertificate, resourceName.toResourceNameString(), action));
+ }
+
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/Zpe.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/Zpe.java
new file mode 100644
index 00000000000..e22e27f1508
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/Zpe.java
@@ -0,0 +1,17 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.zpe;
+
+import com.yahoo.vespa.athenz.api.AthenzResourceName;
+import com.yahoo.vespa.athenz.api.ZToken;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * Interface for interacting with ZPE (Authorization Policy Engine)
+ *
+ * @author bjorncs
+ */
+public interface Zpe {
+ AuthorizationResult checkAccessAllowed(ZToken roleToken, AthenzResourceName resourceName, String action);
+ AuthorizationResult checkAccessAllowed(X509Certificate roleCertificate, AthenzResourceName resourceName, String action);
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/package-info.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/package-info.java
new file mode 100644
index 00000000000..341eb887021
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/zpe/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.vespa.athenz.zpe;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/api/AthenzResourceNameTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/api/AthenzResourceNameTest.java
new file mode 100644
index 00000000000..b9f4bd5369e
--- /dev/null
+++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/api/AthenzResourceNameTest.java
@@ -0,0 +1,21 @@
+package com.yahoo.vespa.athenz.api;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author bjorncs
+ */
+public class AthenzResourceNameTest {
+
+ @Test
+ public void can_serialize_and_deserialize_to_string() {
+ AthenzResourceName resourceName = new AthenzResourceName(new AthenzDomain("domain"), "entity");
+ String resourceNameString = resourceName.toResourceNameString();
+ assertEquals("domain:entity", resourceNameString);
+ AthenzResourceName deserializedResourceName = AthenzResourceName.fromString(resourceNameString);
+ assertEquals(deserializedResourceName, resourceName);
+ }
+
+} \ No newline at end of file