diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-06-20 13:09:55 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-06-20 13:15:11 +0200 |
commit | b38471d94959eb172e82ee102404bc669a14d96b (patch) | |
tree | d1a9e399c6c4f4bb1ee76a1b80454125cfbbd0fe /jdisc-security-filters/src/main/java/com/yahoo/jdisc | |
parent | bf74c1a064739c123921a2e85e9427bae7019290 (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/src/main/java/com/yahoo/jdisc')
4 files changed, 223 insertions, 0 deletions
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 |