diff options
author | Ola Aunrønning <olaa@verizonmedia.com> | 2022-02-14 12:43:35 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-14 12:43:35 +0100 |
commit | 2cc844d5050017b389492274e0b373610bd061ea (patch) | |
tree | e6f1c3e81c6df5b35cd8f1789b99d2bd62bd10c6 /controller-api/src/main/java/com | |
parent | 267b6f417f4f3cfb49d82206f7ae15c4e78f8419 (diff) | |
parent | c419f94e2a0a6b2198a629b29a11b8f667ebd8a3 (diff) |
Merge pull request #21126 from vespa-engine/olaa/ssh-access-requests
Request, list, and approve ssh access
Diffstat (limited to 'controller-api/src/main/java/com')
7 files changed, 103 insertions, 9 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java index a981b11887e..c1d70bf297d 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.athenz; +import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.athenz.api.AthenzUser; import java.time.Instant; @@ -14,5 +15,8 @@ import java.util.Collection; */ public interface AccessControlService { boolean approveDataPlaneAccess(AthenzUser user, Instant expiry); + boolean approveSshAccess(TenantName tenantName, Instant expiry); + boolean requestSshAccess(TenantName tenantName); + boolean hasPendingAccessRequests(TenantName tenantName); Collection<AthenzUser> listMembers(); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java index c2d4d4a5996..f157f88967a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java @@ -2,15 +2,16 @@ package com.yahoo.vespa.hosted.controller.api.integration.athenz; +import com.yahoo.config.provision.TenantName; 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.AthenzUser; import com.yahoo.vespa.athenz.client.zms.ZmsClient; import java.time.Instant; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -19,13 +20,16 @@ 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 ZmsClient zmsClient; private final AthenzRole dataPlaneAccessRole; private final AthenzGroup vespaTeam; + private final ZmsClient vespaZmsClient; //TODO: Merge ZMS clients - public AthenzAccessControlService(ZmsClient zmsClient, AthenzDomain domain) { + public AthenzAccessControlService(ZmsClient zmsClient, AthenzDomain domain, ZmsClient vespaZmsClient) { this.zmsClient = zmsClient; + this.vespaZmsClient = vespaZmsClient; this.dataPlaneAccessRole = new AthenzRole(domain, DATAPLANE_ACCESS_ROLENAME); this.vespaTeam = new AthenzGroup(domain, ALLOWED_OPERATOR_GROUPNAME); } @@ -36,7 +40,7 @@ public class AthenzAccessControlService implements AccessControlService { if(!isVespaTeamMember(user)) { throw new IllegalArgumentException(String.format("User %s requires manual approval, please contact Vespa team", user.getName())); } - Map<AthenzUser, String> users = zmsClient.listPendingRoleApprovals(dataPlaneAccessRole); + Map<AthenzIdentity, String> users = zmsClient.listPendingRoleApprovals(dataPlaneAccessRole); if (users.containsKey(user)) { zmsClient.approvePendingRoleMembership(dataPlaneAccessRole, user, expiry, Optional.empty()); return true; @@ -53,6 +57,59 @@ public class AthenzAccessControlService implements AccessControlService { .collect(Collectors.toList()); } + /** + * @return Whether the ssh access role has any pending role membership requests + */ + @Override + public boolean hasPendingAccessRequests(TenantName tenantName) { + var role = sshRole(tenantName); + var pendingApprovals = vespaZmsClient.listPendingRoleApprovals(role); + return pendingApprovals.containsKey(vespaTeam); + } + + /** + * @return true if access has been granted - false if already member + */ + @Override + public boolean approveSshAccess(TenantName tenantName, Instant expiry) { + var role = sshRole(tenantName); + if (vespaZmsClient.getMembership(role, vespaTeam)) + return false; + + if (!hasPendingAccessRequests(tenantName)) { + vespaZmsClient.addRoleMember(role, vespaTeam, Optional.empty()); + } + // TODO: Pass along auth0 credentials + vespaZmsClient.approvePendingRoleMembership(role, vespaTeam, expiry, Optional.empty()); + return true; + } + + /** + * @return true if access has been requested - false if already member + */ + @Override + public boolean requestSshAccess(TenantName tenantName) { + var role = sshRole(tenantName); + if (vespaZmsClient.getMembership(role, vespaTeam)) + return false; + vespaZmsClient.addRoleMember(role, vespaTeam, Optional.empty()); + return true; + } + + private AthenzRole sshRole(TenantName tenantName) { + return new AthenzRole(getOrCreateTenantDomain(tenantName), "ssh_access"); + } + + private AthenzDomain getOrCreateTenantDomain(TenantName tenantName) { + var domain = new AthenzDomain(TENANT_DOMAIN_PREFIX + "." + tenantName.value()); + + if (vespaZmsClient.getDomainList(domain.getName()).isEmpty()) { + vespaZmsClient.createSubdomain(new AthenzDomain(TENANT_DOMAIN_PREFIX), tenantName.value()); + } + + return domain; + } + public boolean isVespaTeamMember(AthenzUser user) { return zmsClient.getGroupMembership(vespaTeam, user); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java index a0cc0d1ae1c..f906172dba0 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.athenz; +import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.athenz.api.AthenzUser; import java.time.Instant; @@ -28,6 +29,21 @@ public class MockAccessControlService implements AccessControlService { return Set.copyOf(members); } + @Override + public boolean approveSshAccess(TenantName tenantName, Instant expiry) { + return false; + } + + @Override + public boolean requestSshAccess(TenantName tenantName) { + return false; + } + + @Override + public boolean hasPendingAccessRequests(TenantName tenantName) { + return false; + } + public void addPendingMember(AthenzUser user) { pendingMembers.add(user); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java index 1b00368b73e..63212c9c200 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java @@ -200,12 +200,12 @@ public class ZmsClientMock implements ZmsClient { } @Override - public Map<AthenzUser,String> listPendingRoleApprovals(AthenzRole athenzRole) { + public Map<AthenzIdentity,String> listPendingRoleApprovals(AthenzRole athenzRole) { return Map.of(); } @Override - public void approvePendingRoleMembership(AthenzRole athenzRole, AthenzUser athenzUser, Instant expiry, Optional<String> reason) { + public void approvePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, Optional<String> reason) { } @Override @@ -255,6 +255,10 @@ public class ZmsClientMock implements ZmsClient { public void deleteRole(AthenzRole athenzRole) { athenz.domains.get(athenzRole.domain()).roles.removeIf(role -> role.name().equals(athenzRole.roleName())); } + + @Override + public void createSubdomain(AthenzDomain parent, String name) {} + @Override public void close() {} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index 8bd7a9e726d..3a5bb96d985 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -233,7 +233,13 @@ enum PathGroup { secretStore(Matcher.tenant, "/application/v4/tenant/{tenant}/secret-store/{*}"), /** Paths used to proxy Horizon metric requests */ - horizonProxy("/horizon/v1/{*}"); + horizonProxy("/horizon/v1/{*}"), + + /** Paths used to list and request access to tenant resources */ + accessRequests(Matcher.tenant, "/application/v4/tenant/{tenant}/access/request/{*}"), + + /** Paths used to approve requests to access tenant resources */ + accessRequestApproval(Matcher.tenant, "/application/v4/tenant/{tenant}/access/approve/{*}"); final List<String> pathSpecs; final List<Matcher> matchers; @@ -281,9 +287,10 @@ enum PathGroup { return EnumSet.complementOf(EnumSet.copyOf(pathGroups)); } - static Set<PathGroup> billingPaths() { + static Set<PathGroup> operatorRestrictedPaths() { var paths = billingPathsNoToken(); paths.add(PathGroup.billingToken); + paths.add(accessRequestApproval); return paths; } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java index d40027d91a7..4d9a16c34f0 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java @@ -22,7 +22,7 @@ enum Policy { /** Full access to everything. */ operator(Privilege.grant(Action.all()) - .on(PathGroup.allExcept(PathGroup.billingPaths())) + .on(PathGroup.allExcept(PathGroup.operatorRestrictedPaths())) .in(SystemName.all()), Privilege.grant(Action.read) .on(PathGroup.billingPathsNoToken()) @@ -188,6 +188,10 @@ enum Policy { .on(PathGroup.billingList, PathGroup.billing) .in(SystemName.PublicCd, SystemName.Public)), + accessRequests(Privilege.grant(Action.all()) + .on(PathGroup.accessRequests, PathGroup.accessRequestApproval) + .in(SystemName.PublicCd)), + /** Invoice management */ hostedAccountant(Privilege.grant(Action.all()) .on(PathGroup.hostedAccountant, PathGroup.accountant) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java index aed5c08f0db..7f6e77faf03 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java @@ -75,7 +75,9 @@ public enum RoleDefinition { Policy.paymentInstrumentDelete, Policy.paymentInstrumentCreate, Policy.planUpdate, - Policy.billingInformationRead), + Policy.billingInformationRead, + Policy.accessRequests + ), /** Headless — the application specific role identified by deployment keys for production */ headless(Policy.submission), |