diff options
author | Ola Aunrønning <olaa@verizonmedia.com> | 2022-02-09 20:06:42 +0100 |
---|---|---|
committer | Ola Aunrønning <olaa@verizonmedia.com> | 2022-02-09 20:06:42 +0100 |
commit | 8789e0b449fc7b32f3924da7c8a3f86e734f2289 (patch) | |
tree | 220300d80d6993407ba882bda1c04ab109e31496 | |
parent | f99a9a1005f35117e90f0df0d6f9c2bb36357d07 (diff) |
Request, list, and approve ssh access
7 files changed, 128 insertions, 7 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..3f0418b1a9e 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,6 +2,7 @@ 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.AthenzRole; @@ -10,7 +11,6 @@ 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 +19,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); } @@ -53,6 +56,50 @@ public class AthenzAccessControlService implements AccessControlService { .collect(Collectors.toList()); } + /** + * @return Whether the ssh access role has any pending role membership requests + */ + public boolean hasPendingAccessRequests(TenantName tenantName) { + var role = sshRole(tenantName); + var pendingApprovals = vespaZmsClient.listPendingRoleApprovals(role); + return !pendingApprovals.isEmpty(); + } + + /** + * @return true if access has been granted - false if already member + */ + 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 + */ + 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(tenantDomain(tenantName), "ssh_access"); + } + + private AthenzDomain tenantDomain(TenantName tenantName) { + return new AthenzDomain(TENANT_DOMAIN_PREFIX + tenantName.value()); + } + 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..d960c46cacd 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 @@ -205,7 +205,7 @@ public class ZmsClientMock implements ZmsClient { } @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 diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 32381e6a123..71b5984c7cd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -235,6 +235,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/")) return root(request); if (path.matches("/application/v4/tenant")) return tenants(request); if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/access/ssh/request")) return accessRequests(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/info")) return tenantInfo(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/notifications")) return notifications(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}/validate")) return validateSecretStore(path.get("tenant"), path.get("name"), request); @@ -286,6 +287,8 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private HttpResponse handlePUT(Path path, HttpRequest request) { if (path.matches("/application/v4/tenant/{tenant}")) return updateTenant(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/access/ssh/request")) return requestSshAccess(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/access/ssh/approve")) return approveAccessRequest(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/info")) return updateTenantInfo(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return allowArchiveAccess(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return addSecretStore(path.get("tenant"), path.get("name"), request); @@ -402,6 +405,47 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { return new SlimeJsonResponse(slime); } + private HttpResponse accessRequests(String tenantName, HttpRequest request) { + if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) + return ErrorResponse.badRequest("Can only see access requests for cloud tenants"); + + var pendingRequests = controller.serviceRegistry().accessControlService().hasPendingAccessRequests(TenantName.from(tenantName)); + var slime = new Slime(); + slime.setObject().setBool("hasPendingRequests", pendingRequests); + return new SlimeJsonResponse(slime); + } + + private HttpResponse requestSshAccess(String tenantName, HttpRequest request) { + if (!isOperator(request)) { + return ErrorResponse.forbidden("Only operators are allowed to request ssh access"); + } + + if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) + return ErrorResponse.badRequest("Can only request access for cloud tenants"); + + controller.serviceRegistry().accessControlService().requestSshAccess(TenantName.from(tenantName)); + return new MessageResponse("OK"); + + } + + private HttpResponse approveAccessRequest(String tenantName, HttpRequest request) { + var tenant = TenantName.from(tenantName); + + if (controller.tenants().require(tenant).type() != Tenant.Type.cloud) + return ErrorResponse.badRequest("Can only see access requests for cloud tenants"); + + if (!isTenantAdmin(tenant, request)) { + return ErrorResponse.forbidden("Only tenant admins are allowed to approve access requests"); + } + var inspector = toSlime(request.getData()).get(); + var expiry = inspector.field("expiry").valid() ? + Instant.ofEpochMilli(inspector.field("expiry").asLong()) : + Instant.now().plus(1, ChronoUnit.DAYS); + + controller.serviceRegistry().accessControlService().approveSshAccess(tenant, expiry); + return new MessageResponse("OK"); + } + private HttpResponse tenantInfo(String tenantName, HttpRequest request) { return controller.tenants().get(TenantName.from(tenantName)) .filter(tenant -> tenant.type() == Tenant.Type.cloud) @@ -2738,6 +2782,16 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { .anyMatch(definition -> definition == RoleDefinition.hostedOperator); } + private static boolean isTenantAdmin(TenantName tenant, HttpRequest request) { + return Optional.ofNullable(request.getJDiscRequest().context().get(SecurityContext.ATTRIBUTE_NAME)) + .filter(SecurityContext.class::isInstance) + .map(SecurityContext.class::cast) + .map(SecurityContext::roles) + .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request")) + .stream() + .anyMatch(role -> role.equals(Role.administrator(tenant))); + } + private void ensureApplicationExists(TenantAndApplicationId id, HttpRequest request) { if (controller.applications().getApplication(id).isEmpty()) { log.fine("Application does not exist in public, creating: " + id); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java index 289a63a1128..92d4c0c799c 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java @@ -304,9 +304,9 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient { } @Override - public void approvePendingRoleMembership(AthenzRole athenzRole, AthenzUser athenzUser, Instant expiry, Optional<String> reason) { - URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s/decision", athenzRole.domain().getName(), athenzRole.roleName(), athenzUser.getFullName())); - MembershipEntity membership = new MembershipEntity.RoleMembershipEntity(athenzUser.getFullName(), true, athenzRole.roleName(), Long.toString(expiry.getEpochSecond())); + public void approvePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, Optional<String> reason) { + URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s/decision", athenzRole.domain().getName(), athenzRole.roleName(), athenzIdentity.getFullName())); + MembershipEntity membership = new MembershipEntity.RoleMembershipEntity(athenzIdentity.getFullName(), true, athenzRole.roleName(), Long.toString(expiry.getEpochSecond())); var requestBuilder = RequestBuilder.put() .setUri(uri) diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java index aa038b5bb23..f2fd4fef731 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java @@ -63,7 +63,7 @@ public interface ZmsClient extends AutoCloseable { Map<AthenzUser, String> listPendingRoleApprovals(AthenzRole athenzRole); - void approvePendingRoleMembership(AthenzRole athenzRole, AthenzUser athenzUser, Instant expiry, Optional<String> reason); + void approvePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, Optional<String> reason); List<AthenzIdentity> listMembers(AthenzRole athenzRole); |