diff options
10 files changed, 69 insertions, 13 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 765312b40a3..14adc29468e 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 @@ -4,9 +4,10 @@ package com.yahoo.vespa.hosted.controller.api.integration.athenz; import com.yahoo.vespa.athenz.api.AthenzUser; +import java.time.Instant; import java.util.Collection; public interface AccessControlService { - public boolean approveDataPlaneAccess(AthenzUser user); - public Collection<AthenzUser> listMembers(); + boolean approveDataPlaneAccess(AthenzUser user, Instant expiry); + 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 2882fb1483c..f6d2b333cc3 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 @@ -7,6 +7,7 @@ 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; @@ -23,11 +24,10 @@ public class AthenzAccessControlService implements AccessControlService { } @Override - public boolean approveDataPlaneAccess(AthenzUser user) { + public boolean approveDataPlaneAccess(AthenzUser user, Instant expiry) { List<AthenzUser> users = zmsClient.listPendingRoleApprovals(dataPlaneAccessRole); if (users.contains(user)) { - // TODO (mortent): Handle expiry - zmsClient.approvePendingRoleMembership(dataPlaneAccessRole, user, null); + zmsClient.approvePendingRoleMembership(dataPlaneAccessRole, user, expiry); return true; } return false; 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 9a6027317c5..81bc7725c7a 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 @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.athenz; import com.yahoo.vespa.athenz.api.AthenzUser; +import java.time.Instant; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -14,7 +15,7 @@ public class MockAccessControlService implements AccessControlService { private final Set<AthenzUser> members = new HashSet<>(); @Override - public boolean approveDataPlaneAccess(AthenzUser user) { + public boolean approveDataPlaneAccess(AthenzUser user, Instant expiry) { if (pendingMembers.remove(user)) { return members.add(user); } else { 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 5e5dfcd6aed..5a1fcb32113 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 @@ -36,7 +36,8 @@ enum PathGroup { "/routing/v1/status/environment/{*}", "/routing/v1/inactive/environment/{*}", "/state/v1/{*}", - "/changemanagement/v1/{*}"), + "/changemanagement/v1/{*}", + "/application/v4/tenant/{*}/application/{*}/instance/{*}/environment/{*}/region/{*}/access/support/grant"), /** Paths used for creating and reading user resources. */ user("/application/v4/user", 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 b72a6d2f820..30f5f6462ae 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 @@ -31,6 +31,7 @@ import com.yahoo.restapi.Path; import com.yahoo.restapi.ResourceResponse; import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.security.KeyUtils; +import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.JsonParseException; @@ -122,6 +123,7 @@ import java.net.URISyntaxException; import java.security.DigestInputStream; import java.security.Principal; import java.security.PublicKey; +import java.security.cert.X509Certificate; import java.time.DayOfWeek; import java.time.Duration; import java.time.Instant; @@ -307,6 +309,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/suspend")) return suspend(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), true); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/access/support")) return allowSupportAccess(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/access/support/grant")) return grantAccess(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/deploy")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); // legacy synonym of the above if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); @@ -992,6 +995,26 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { return new SlimeJsonResponse(SupportAccessSerializer.toSlime(allowed, false, Optional.of(now))); } + private HttpResponse grantAccess(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) { + DeploymentId deployment = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region)); + Principal principal = requireUserPrincipal(request); + Instant now = controller.clock().instant(); + + Inspector requestObject = toSlime(request.getData()).get(); + X509Certificate certificate = X509CertificateUtils.fromPem(requestObject.field("certificate").asString()); + + // Register grant + SupportAccess supportAccess = controller.supportAccess().registerGrant(deployment, principal.getName(), certificate); + + // Trigger deployment to include operator cert + JobType jobType = JobType.from(controller.system(), deployment.zoneId()) + .orElseThrow(() -> new IllegalStateException("No job found to trigger for " + deployment.toUserFriendlyString())); + + String jobName = controller.applications().deploymentTrigger() + .reTrigger(deployment.applicationId(), jobType).type().jobName(); + return new MessageResponse(String.format("Operator %s granted access and job %s triggered", principal.getName(), jobName)); + } + private HttpResponse disallowSupportAccess(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) { DeploymentId deployment = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region)); Principal principal = requireUserPrincipal(request); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java index 1a1118a127f..2b2087fccf4 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java @@ -2,6 +2,9 @@ package com.yahoo.vespa.hosted.controller.restapi.controller; import com.yahoo.component.Version; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Zone; +import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; @@ -12,8 +15,8 @@ import com.yahoo.restapi.ResourceResponse; import com.yahoo.slime.Inspector; import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.athenz.api.AthenzUser; -import com.yahoo.vespa.athenz.utils.AthenzIdentities; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler; import com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance; import com.yahoo.vespa.hosted.controller.maintenance.Upgrader; @@ -85,7 +88,11 @@ public class ControllerApiHandler extends AuditLoggingRequestHandler { private HttpResponse approveMembership(HttpRequest request, String user) { AthenzUser athenzUser = AthenzUser.fromUserId(user); - boolean approved = controller.serviceRegistry().accessControlService().approveDataPlaneAccess(athenzUser); + byte[] jsonBytes = toJsonBytes(request.getData()); + Inspector inspector = SlimeUtils.jsonToSlime(jsonBytes).get(); + ApplicationId applicationId = ApplicationId.fromSerializedForm(inspector.field("applicationId").asString()); + ZoneId zone = ZoneId.from(inspector.field("zone").asString()); + controller.supportAccess().allowDataplaneMembership(athenzUser, new DeploymentId(applicationId, zone)); return new AccessRequestResponse(controller.serviceRegistry().accessControlService().listMembers()); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java index 4a550ad3379..585cd609f53 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java @@ -1,16 +1,20 @@ // Copyright 2021 Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.support.access; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; +import java.security.Principal; import java.security.cert.X509Certificate; import java.time.Instant; import java.time.Period; import java.util.List; import java.util.stream.Collectors; +import static com.yahoo.vespa.hosted.controller.support.access.SupportAccess.State.ALLOWED; import static com.yahoo.vespa.hosted.controller.support.access.SupportAccess.State.NOT_ALLOWED; /** @@ -87,4 +91,15 @@ public class SupportAccessControl { .filter(grant -> !grant.certificate().getNotAfter().toInstant().isAfter(now)) .collect(Collectors.toUnmodifiableList()); } + + public void allowDataplaneMembership(AthenzUser identity, DeploymentId deploymentId) { + Instant instant = controller.clock().instant(); + SupportAccess supportAccess = forDeployment(deploymentId); + SupportAccess.CurrentStatus currentStatus = supportAccess.currentStatus(instant); + if(currentStatus.state() == ALLOWED) { + controller.serviceRegistry().accessControlService().approveDataPlaneAccess(identity, currentStatus.allowedUntil().orElse(instant.plus(MAX_SUPPORT_ACCESS_TIME))); + } else { + throw new IllegalStateException("Not allowed"); + } + } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 47aa3e6b9d4..f1e54033792 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -18,6 +18,7 @@ import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; import com.yahoo.security.SignatureAlgorithm; import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.X509CertificateUtils; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzPrincipal; @@ -82,6 +83,7 @@ import java.io.File; import java.math.BigInteger; import java.net.URI; import java.security.cert.X509Certificate; +import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; @@ -1501,6 +1503,7 @@ public class ApplicationApiTest extends ControllerContainerTest { var zone = ZoneId.from(Environment.prod, RegionName.from("us-west-1")); deploymentTester.controllerTester().zoneRegistry().setRoutingMethod(ZoneApiMock.from(zone), List.of(RoutingMethod.exclusive, RoutingMethod.shared)); + addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain"), AthenzService.from("service")) .compileVersion(RoutingController.DIRECT_ROUTING_MIN_VERSION) @@ -1528,11 +1531,16 @@ public class ApplicationApiTest extends ControllerContainerTest { // Grant access to support user X509Certificate support_cert = grantCertificate(now, now.plusSeconds(3600)); - tester.controller().supportAccess().registerGrant(app.deploymentIdIn(zone), "user.andreer", support_cert); + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/access/support/grant", POST) + .data("{\"certificate\":\""+X509CertificateUtils.toPem(support_cert)+"\"}") + .userIdentity(HOSTED_VESPA_OPERATOR), + "{\"message\":\"Operator user.johnoperator granted access and job production-us-west-1 triggered\"}"); + + //tester.controller().supportAccess().registerGrant(app.deploymentIdIn(zone), "user.andreer", support_cert); // GET shows grant String grantResponse = allowedResponse.replaceAll("\"grants\":\\[]", - "\"grants\":[{\"requestor\":\"user.andreer\",\"notBefore\":\"" + serializeInstant(now) + "\",\"notAfter\":\"" + serializeInstant(now.plusSeconds(3600)) + "\"}]"); + "\"grants\":[{\"requestor\":\"user.johnoperator\",\"notBefore\":\"" + serializeInstant(now) + "\",\"notAfter\":\"" + serializeInstant(now.plusSeconds(3600)) + "\"}]"); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/access/support", GET) .userIdentity(USER_ID), grantResponse, 200 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 d352e48283a..9ee599b22eb 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 @@ -222,8 +222,7 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient { @Override public void approvePendingRoleMembership(AthenzRole athenzRole, AthenzUser athenzUser, Instant expiry) { URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s/decision", athenzRole.domain().getName(), athenzRole.roleName(), athenzUser.getFullName())); - // TODO provide the shortest expiry of what is provided and requested (or verify this is done by ZMS) - MembershipEntity membership = new MembershipEntity(athenzUser.getFullName(), true, athenzRole.roleName(), null); + MembershipEntity membership = new MembershipEntity(athenzUser.getFullName(), true, athenzRole.roleName(), Long.toString(expiry.getEpochSecond())); HttpUriRequest request = RequestBuilder.put() .setUri(uri) .setEntity(toJsonStringEntity(membership)) diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/MembershipEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/MembershipEntity.java index b1e7df11638..d0672473776 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/MembershipEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/MembershipEntity.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; /** * @author bjorncs + * @author mortent */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) |