aboutsummaryrefslogtreecommitdiffstats
path: root/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java
blob: 16d8b72ff523c830c95bfefb912a0c204e82ea6e (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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.

package com.yahoo.vespa.hosted.controller.api.integration.athenz;

import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.athenz.api.AthenzAssertion;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.AthenzGroup;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzRole;
import com.yahoo.vespa.athenz.api.AthenzRoleInformation;
import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.athenz.api.OAuthCredentials;
import com.yahoo.vespa.athenz.client.zms.ZmsClient;

import java.time.Instant;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class AthenzAccessControlService implements AccessControlService {

    private static final String ALLOWED_OPERATOR_GROUPNAME = "vespa-team";
    private static final String DATAPLANE_ACCESS_ROLENAME = "operator-data-plane";
    private final String TENANT_DOMAIN_PREFIX = "vespa.tenant";
    private final String ACCESS_APPROVAL_POLICY = "vespa-access-requester";
    private final ZmsClient zmsClient;
    private final AthenzRole dataPlaneAccessRole;
    private final AthenzGroup vespaTeam;
    private final Optional<ZmsClient> vespaZmsClient;
    private final AthenzInstanceSynchronizer athenzInstanceSynchronizer;


    public AthenzAccessControlService(ZmsClient zmsClient, AthenzDomain domain, Optional<ZmsClient> vespaZmsClient, AthenzInstanceSynchronizer athenzInstanceSynchronizer) {
        this.zmsClient = zmsClient;
        this.vespaZmsClient = vespaZmsClient;
        this.athenzInstanceSynchronizer = athenzInstanceSynchronizer;
        this.dataPlaneAccessRole = new AthenzRole(domain, DATAPLANE_ACCESS_ROLENAME);
        this.vespaTeam = new AthenzGroup(domain, ALLOWED_OPERATOR_GROUPNAME);
    }

    @Override
    public boolean approveDataPlaneAccess(AthenzUser user, Instant expiry) {
        // Can only approve team members, other members must be manually approved
        if(!isVespaTeamMember(user)) {
            throw new IllegalArgumentException(String.format("User %s requires manual approval, please contact Vespa team", user.getName()));
        }
        Map<AthenzIdentity, String> users = zmsClient.listPendingRoleApprovals(dataPlaneAccessRole);
        if (users.containsKey(user)) {
            zmsClient.decidePendingRoleMembership(dataPlaneAccessRole, user, expiry, Optional.empty(), Optional.empty(), true);
            return true;
        }
        return false;
    }

    @Override
    // Return list of approved members (users, excluding services) of data plane role
    public Collection<AthenzUser> listMembers() {
        return zmsClient.listMembers(dataPlaneAccessRole)
                .stream().filter(AthenzUser.class::isInstance)
                .map(AthenzUser.class::cast)
                .toList();
    }

    /**
     * @return Whether the ssh access role has any pending role membership requests
     */
    @Override
    public AthenzRoleInformation getAccessRoleInformation(TenantName tenantName) {
        return vespaZmsClient.map(
                zms -> {
                    var role = sshRole(tenantName);
                    return zms.getFullRoleInformation(role);
                }
        ).orElseThrow(() -> new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance"));

    }

    /**
     * @return true if access has been granted - false if already member
     */
    @Override
    public boolean decideSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials, boolean approve) {
        return vespaZmsClient.map(
                zms -> {
                    var role = sshRole(tenantName);

                    var roleInformation = zms.getFullRoleInformation(role);
                    if (roleInformation.getPendingRequest().isEmpty())
                        return false;
                    var reason = roleInformation.getPendingRequest().get().getReason();

                    zms.decidePendingRoleMembership(role, vespaTeam, expiry, Optional.of(reason), Optional.of(oAuthCredentials), approve);
                    if (approve) athenzInstanceSynchronizer.synchronizeInstances(tenantName);
                    return true;
                }
        ).orElseThrow(() -> new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance"));
    }

    /**
     * @return true if access has been requested - false if already member
     */
    @Override
    public boolean requestSshAccess(TenantName tenantName) {
        return vespaZmsClient.map(
                zms -> {
                    var role = sshRole(tenantName);

                    if (zms.getMembership(role, vespaTeam))
                        return false;

                    zms.addRoleMember(role, vespaTeam, Optional.empty());
                    return true;
                }
        ).orElseThrow(() -> new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance"));
    }

    public void setManagedAccess(TenantName tenantName, boolean managedAccess) {
        vespaZmsClient.ifPresentOrElse(
                zms -> {
                    var role = sshRole(tenantName);
                    var assertion = getApprovalAssertion(role);
                    if (managedAccess) {
                        zms.deletePolicyRule(role.domain(), ACCESS_APPROVAL_POLICY, assertion.action(), assertion.resource(), assertion.role());
                    } else {
                        zms.addPolicyRule(role.domain(), ACCESS_APPROVAL_POLICY, assertion.action(), assertion.resource(), assertion.role());
                    }
                },() -> { throw new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance"); });
    }

    public boolean getManagedAccess(TenantName tenantName) {
        return vespaZmsClient.map(
                zms -> {
                    var role = sshRole(tenantName);
                    var approvalAssertion = getApprovalAssertion(role);
                    return zms.getPolicy(role.domain(), ACCESS_APPROVAL_POLICY)
                            .map(policy -> policy.assertions().stream().noneMatch(assertion -> assertion.satisfies(approvalAssertion)))
                            .orElse(true);
                }).orElseThrow(() -> new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance") );
    }

    private AthenzRole sshRole(TenantName tenantName) {
        return new AthenzRole(getTenantDomain(tenantName), "ssh_access");
    }

    private AthenzDomain getTenantDomain(TenantName tenantName) {
        return new AthenzDomain(TENANT_DOMAIN_PREFIX + "." + tenantName.value());
    }

    public boolean isVespaTeamMember(AthenzUser user) {
        return zmsClient.getGroupMembership(vespaTeam, user);
    }

    private AthenzAssertion getApprovalAssertion(AthenzRole accessRole) {
        var approverRole = new AthenzRole(accessRole.domain(), "vespa-access-approver");
        return AthenzAssertion.newBuilder(approverRole, accessRole.toResourceName(), "update_members")
                .effect(AthenzAssertion.Effect.ALLOW)
                .build();
    }
}