summaryrefslogtreecommitdiffstats
path: root/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilter.java
blob: 5bbf111e5b2f9f4af7ec336ae37da1b2942e87d9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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);
    }

}