summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOla Aunrønning <olaa@verizonmedia.com>2022-02-09 20:06:42 +0100
committerOla Aunrønning <olaa@verizonmedia.com>2022-02-09 20:06:42 +0100
commit8789e0b449fc7b32f3924da7c8a3f86e734f2289 (patch)
tree220300d80d6993407ba882bda1c04ab109e31496
parentf99a9a1005f35117e90f0df0d6f9c2bb36357d07 (diff)
Request, list, and approve ssh access
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java4
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java51
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java16
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java54
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java6
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java2
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);