diff options
author | Ola Aunrønning <olaa@verizonmedia.com> | 2022-03-07 09:25:20 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-07 09:25:20 +0100 |
commit | 5ead84bb17aa1ebb39d934d9b74d2d6ef758d566 (patch) | |
tree | a07192f1d6dc8d9ca8102fef5ec79c93f7a560e1 | |
parent | 2e05df2de19c2d5b87befa2ee6c4fd182dcb5630 (diff) | |
parent | 810de0a30b9dc658769deb21c5579f88afdbd528 (diff) |
Merge pull request #21480 from vespa-engine/olaa/athenz-synchronization
Synchronize athenz instances on request approval
11 files changed, 340 insertions, 30 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 a08319055ff..1dd6eb543ef 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 @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.athenz; import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.athenz.api.AthenzRoleInformation; import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.athenz.api.OAuthCredentials; @@ -16,8 +17,9 @@ import java.util.Collection; */ public interface AccessControlService { boolean approveDataPlaneAccess(AthenzUser user, Instant expiry); - boolean approveSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials); + boolean decideSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials, boolean approve); boolean requestSshAccess(TenantName tenantName); - boolean hasPendingAccessRequests(TenantName tenantName); + AthenzRoleInformation getAccessRoleInformation(TenantName tenantName); + void setPreapprovedAccess(TenantName tenantName, boolean preapproved); 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 0568678219e..415a087d990 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.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; @@ -26,11 +27,13 @@ public class AthenzAccessControlService implements AccessControlService { private final AthenzRole dataPlaneAccessRole; private final AthenzGroup vespaTeam; private final ZmsClient vespaZmsClient; //TODO: Merge ZMS clients + private final AthenzInstanceSynchronizer athenzInstanceSynchronizer; - public AthenzAccessControlService(ZmsClient zmsClient, AthenzDomain domain, ZmsClient vespaZmsClient) { + public AthenzAccessControlService(ZmsClient zmsClient, AthenzDomain domain, 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); } @@ -43,7 +46,7 @@ public class AthenzAccessControlService implements AccessControlService { } Map<AthenzIdentity, String> users = zmsClient.listPendingRoleApprovals(dataPlaneAccessRole); if (users.containsKey(user)) { - zmsClient.approvePendingRoleMembership(dataPlaneAccessRole, user, expiry, Optional.empty(), Optional.empty()); + zmsClient.decidePendingRoleMembership(dataPlaneAccessRole, user, expiry, Optional.empty(), Optional.empty(), true); return true; } return false; @@ -62,19 +65,19 @@ public class AthenzAccessControlService implements AccessControlService { * @return Whether the ssh access role has any pending role membership requests */ @Override - public boolean hasPendingAccessRequests(TenantName tenantName) { + public AthenzRoleInformation getAccessRoleInformation(TenantName tenantName) { var role = sshRole(tenantName); if (!vespaZmsClient.listRoles(role.domain()).contains(role)) - return false; - var pendingApprovals = vespaZmsClient.listPendingRoleApprovals(role); - return pendingApprovals.containsKey(vespaTeam); + vespaZmsClient.createRole(role, Map.of()); + + return vespaZmsClient.getFullRoleInformation(role); } /** * @return true if access has been granted - false if already member */ @Override - public boolean approveSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials) { + public boolean decideSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials, boolean approve) { var role = sshRole(tenantName); if (!vespaZmsClient.listRoles(role.domain()).contains(role)) @@ -83,10 +86,13 @@ public class AthenzAccessControlService implements AccessControlService { if (vespaZmsClient.getMembership(role, vespaTeam)) return false; - if (!hasPendingAccessRequests(tenantName)) { - vespaZmsClient.addRoleMember(role, vespaTeam, Optional.empty()); - } - vespaZmsClient.approvePendingRoleMembership(role, vespaTeam, expiry, Optional.empty(), Optional.of(oAuthCredentials)); + var roleInformation = vespaZmsClient.getFullRoleInformation(role); + if (roleInformation.getPendingRequest().isEmpty()) + return false; + var reason = roleInformation.getPendingRequest().get().getReason(); + + vespaZmsClient.decidePendingRoleMembership(role, vespaTeam, expiry, Optional.of(reason), Optional.of(oAuthCredentials), approve); + athenzInstanceSynchronizer.synchronizeInstances(tenantName); return true; } @@ -107,6 +113,16 @@ public class AthenzAccessControlService implements AccessControlService { return true; } + public void setPreapprovedAccess(TenantName tenantName, boolean preapprovedAccess) { + var role = sshRole(tenantName); + + var attributes = Map.<String, Object>of( + "selfServe", !preapprovedAccess, + "reviewEnabled", !preapprovedAccess + ); + vespaZmsClient.createRole(role, attributes); + } + private AthenzRole sshRole(TenantName tenantName) { return new AthenzRole(getOrCreateTenantDomain(tenantName), "ssh_access"); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizer.java new file mode 100644 index 00000000000..3b9166d4363 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizer.java @@ -0,0 +1,15 @@ +// 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; + +/** + * @author olaa + * + * Responsible for synchronizing misc roles and their pending memberships between separate Athenz instances + */ +public interface AthenzInstanceSynchronizer { + + void synchronizeInstances(TenantName tenant); + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizerMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizerMock.java new file mode 100644 index 00000000000..1f0403a0b44 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizerMock.java @@ -0,0 +1,12 @@ +// 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; + +/** + * @author olaa + */ +public class AthenzInstanceSynchronizerMock implements AthenzInstanceSynchronizer { + @Override + public void synchronizeInstances(TenantName tenant) {} +} 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 b8106450705..c14ca2bdc80 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 @@ -3,12 +3,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.AthenzRoleInformation; import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.athenz.api.OAuthCredentials; import java.time.Instant; import java.util.Collection; import java.util.HashSet; +import java.util.List; +import java.util.Optional; import java.util.Set; public class MockAccessControlService implements AccessControlService { @@ -31,7 +35,7 @@ public class MockAccessControlService implements AccessControlService { } @Override - public boolean approveSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials) { + public boolean decideSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials, boolean approve) { return false; } @@ -41,8 +45,13 @@ public class MockAccessControlService implements AccessControlService { } @Override - public boolean hasPendingAccessRequests(TenantName tenantName) { - return false; + public AthenzRoleInformation getAccessRoleInformation(TenantName tenantName) { + return new AthenzRoleInformation(new AthenzDomain("test-domain"), "tenant-role", false, false, Optional.empty(), List.of()); + } + + @Override + public void setPreapprovedAccess(TenantName tenantName, boolean preapproved) { + } public void addPendingMember(AthenzUser 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 38b2a36a348..5f567e8b84a 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 @@ -8,6 +8,7 @@ import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzPolicy; import com.yahoo.vespa.athenz.api.AthenzResourceName; import com.yahoo.vespa.athenz.api.AthenzRole; +import com.yahoo.vespa.athenz.api.AthenzRoleInformation; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.api.OAuthCredentials; import com.yahoo.vespa.athenz.client.zms.RoleAction; @@ -201,7 +202,7 @@ public class ZmsClientMock implements ZmsClient { } @Override - public void approvePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, Optional<String> reason, Optional<OAuthCredentials> oAuthCredentials) { + public void decidePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, Optional<String> reason, Optional<OAuthCredentials> oAuthCredentials, boolean approve) { } @Override @@ -256,6 +257,11 @@ public class ZmsClientMock implements ZmsClient { public void createSubdomain(AthenzDomain parent, String name) {} @Override + public AthenzRoleInformation getFullRoleInformation(AthenzRole role) { + return new AthenzRoleInformation(role.domain(), role.roleName(), true, true, Optional.empty(), List.of()); + } + + @Override public void close() {} private static AthenzDomain getTenantDomain(AthenzResourceName resource) { 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 217bb9e1444..2ece93ba23f 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 @@ -240,7 +240,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/notifications")) return notifications(request, Optional.ofNullable(request.getProperty("tenant")), true); 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/request/ssh")) return accessRequests(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/access/request/operator")) 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(request, Optional.of(path.get("tenant")), false); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}/validate")) return validateSecretStore(path.get("tenant"), path.get("name"), request); @@ -292,8 +292,9 @@ 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/request/ssh")) return requestSshAccess(path.get("tenant"), request); - if (path.matches("/application/v4/tenant/{tenant}/access/approve/ssh")) return approveAccessRequest(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/access/request/operator")) return requestSshAccess(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/access/approve/operator")) return approveAccessRequest(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/access/preapprove/operator")) return addPreapprovedAccess(path.get("tenant")); 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); @@ -341,6 +342,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private HttpResponse handleDELETE(Path path, HttpRequest request) { if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/access/preapprove/operator")) return removePreapprovedAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/key")) return removeDeveloperKey(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return removeArchiveAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return deleteSecretStore(path.get("tenant"), path.get("name"), request); @@ -414,9 +416,27 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { 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 accessControlService = controller.serviceRegistry().accessControlService(); + var accessRoleInformation = accessControlService.getAccessRoleInformation(TenantName.from(tenantName)); + var preapprovedAccess = !accessRoleInformation.isSelfServe() && !accessRoleInformation.isReviewEnabled(); var slime = new Slime(); - slime.setObject().setBool("hasPendingRequests", pendingRequests); + var cursor = slime.setObject(); + cursor.setBool("preapprovedAccess", preapprovedAccess); + accessRoleInformation.getPendingRequest() + .ifPresent(membershipRequest -> { + var requestCursor = cursor.setObject("pendingRequest"); + requestCursor.setString("requestTime", membershipRequest.getCreationTime()); + requestCursor.setString("reason", membershipRequest.getReason()); + }); + var auditLogCursor = cursor.setArray("auditLog"); + accessRoleInformation.getAuditLog() + .forEach(auditLogEntry -> { + var entryCursor = auditLogCursor.addObject(); + entryCursor.setString("created", auditLogEntry.getCreationTime()); + entryCursor.setString("approver", auditLogEntry.getApprover()); + entryCursor.setString("reason", auditLogEntry.getReason()); + entryCursor.setString("status", auditLogEntry.getAction()); + }); return new SlimeJsonResponse(slime); } @@ -443,8 +463,27 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { var expiry = inspector.field("expiry").valid() ? Instant.ofEpochMilli(inspector.field("expiry").asLong()) : Instant.now().plus(1, ChronoUnit.DAYS); + var approve = inspector.field("approve").asBool(); + + controller.serviceRegistry().accessControlService().decideSshAccess(tenant, expiry, OAuthCredentials.fromAuth0RequestContext(request.getJDiscRequest().context()), approve); + return new MessageResponse("OK"); + } + + private HttpResponse addPreapprovedAccess(String tenantName) { + return setPreapprovedAccess(tenantName, true); + } + + private HttpResponse removePreapprovedAccess(String tenantName) { + return setPreapprovedAccess(tenantName, false); + } + + private HttpResponse setPreapprovedAccess(String tenantName, boolean preapprovedAccess) { + var tenant = TenantName.from(tenantName); + + if (controller.tenants().require(tenant).type() != Tenant.Type.cloud) + return ErrorResponse.badRequest("Can only set access privel for cloud tenants"); - controller.serviceRegistry().accessControlService().approveSshAccess(tenant, expiry, OAuthCredentials.fromAuth0RequestContext(request.getJDiscRequest().context())); + controller.serviceRegistry().accessControlService().setPreapprovedAccess(tenant, preapprovedAccess); return new MessageResponse("OK"); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzRoleInformation.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzRoleInformation.java new file mode 100644 index 00000000000..dcc3452a69a --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzRoleInformation.java @@ -0,0 +1,121 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import com.yahoo.vespa.athenz.client.zms.bindings.RoleEntity; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @author olaa + */ +public class AthenzRoleInformation extends AthenzRole { + + private final boolean isSelfServe; + private final boolean reviewEnabled; + private final Optional<MembershipRequest> pendingRequest; + private final List<AuditLogEntry> auditLog; + + public AthenzRoleInformation(AthenzDomain domain, String roleName, boolean isSelfServe, boolean reviewEnabled, Optional<MembershipRequest> pendingRequest, List<AuditLogEntry> auditLog) { + super(domain, roleName); + this.isSelfServe = isSelfServe; + this.reviewEnabled = reviewEnabled; + this.pendingRequest = pendingRequest; + this.auditLog = auditLog; + } + + public boolean isSelfServe() { + return isSelfServe; + } + + public boolean isReviewEnabled() { + return reviewEnabled; + } + + public Optional<MembershipRequest> getPendingRequest() { + return pendingRequest; + } + + public List<AuditLogEntry> getAuditLog() { + return auditLog; + } + + public static AthenzRoleInformation fromRoleEntity(RoleEntity roleEntity) { + var role = fromResourceNameString(roleEntity.roleName()); + var isSelfServe = roleEntity.selfServe() != null && roleEntity.selfServe(); + var reviewEnabled = roleEntity.reviewEnabled() != null && roleEntity.reviewEnabled(); + var pendingRequest = roleEntity.roleMembers() + .stream() + .filter(member -> member.pendingApproval()) + .map(member -> new MembershipRequest(member.memberName(), member.auditRef(), member.requestTime(), member.active())) + .findFirst(); + var auditLog = roleEntity.auditLog() + .stream() + .map(entry -> new AuditLogEntry(entry.getAdmin(), entry.getAction(), entry.getAuditRef(), entry.getCreated())) + .collect(Collectors.toList()); + return new AthenzRoleInformation(role.domain(), role.roleName(), isSelfServe, reviewEnabled, pendingRequest, auditLog); + } + + + public static class MembershipRequest { + private final String memberName; + private final String reason; + private final String creationTime; + private final boolean active; + + public MembershipRequest(String memberName, String reason, String creationTime, boolean active) { + this.memberName = memberName; + this.reason = reason; + this.creationTime = creationTime; + this.active = active; + } + + public String getMemberName() { + return memberName; + } + + public String getReason() { + return reason; + } + + public String getCreationTime() { + return creationTime; + } + + public boolean isActive() { + return active; + } + } + + public static class AuditLogEntry { + private final String approver; + private final String action; + private final String reason; + private final String creationTime; + + public AuditLogEntry(String approver, String action, String reason, String creationTime) { + this.approver = approver; + this.action = action; + this.reason = reason; + this.creationTime = creationTime; + } + + public String getApprover() { + return approver; + } + + public String getAction() { + return action; + } + + public String getReason() { + return reason; + } + + public String getCreationTime() { + return creationTime; + } + } + +} 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 32f54255262..eef833c91a7 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 @@ -8,6 +8,7 @@ import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzPolicy; import com.yahoo.vespa.athenz.api.AthenzResourceName; import com.yahoo.vespa.athenz.api.AthenzRole; +import com.yahoo.vespa.athenz.api.AthenzRoleInformation; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.api.OAuthCredentials; import com.yahoo.vespa.athenz.client.ErrorHandler; @@ -301,10 +302,10 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient { } @Override - public void approvePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, - Optional<String> reason, Optional<OAuthCredentials> oAuthCredentials) { + public void decidePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, + Optional<String> reason, Optional<OAuthCredentials> oAuthCredentials, boolean approve) { 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())); + MembershipEntity membership = new MembershipEntity.RoleMembershipEntity(athenzIdentity.getFullName(), approve, athenzRole.roleName(), Long.toString(expiry.getEpochSecond())); var requestBuilder = RequestBuilder.put() .setUri(uri) @@ -406,6 +407,13 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient { execute(request, response -> readEntity(response, Void.class)); } + public AthenzRoleInformation getFullRoleInformation(AthenzRole role) { + var uri = zmsUrl.resolve(String.format("domain/%s/role/%s?pending=true&auditLog=true", role.domain().getName(), role.roleName())); + var request = RequestBuilder.get(uri).build(); + var roleEntity = execute(request, response -> readEntity(response, RoleEntity.class)); + return AthenzRoleInformation.fromRoleEntity(roleEntity); + } + private static Header createCookieHeader(OAuthCredentials oAuthCredentials) { return new BasicHeader("Cookie", oAuthCredentials.asCookie()); } 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 95b7d9b8976..3ff2ff843a0 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 @@ -7,6 +7,7 @@ import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzPolicy; import com.yahoo.vespa.athenz.api.AthenzResourceName; import com.yahoo.vespa.athenz.api.AthenzRole; +import com.yahoo.vespa.athenz.api.AthenzRoleInformation; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.api.OAuthCredentials; @@ -59,8 +60,8 @@ public interface ZmsClient extends AutoCloseable { Map<AthenzIdentity, String> listPendingRoleApprovals(AthenzRole athenzRole); - void approvePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, - Optional<String> reason, Optional<OAuthCredentials> oAuthCredentials); + void decidePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, + Optional<String> reason, Optional<OAuthCredentials> oAuthCredentials, boolean approve); List<AthenzIdentity> listMembers(AthenzRole athenzRole); @@ -80,5 +81,7 @@ public interface ZmsClient extends AutoCloseable { void createSubdomain(AthenzDomain parent, String name); + AthenzRoleInformation getFullRoleInformation(AthenzRole role); + void close(); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/RoleEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/RoleEntity.java index 28b1f5d3206..3ee0c717f19 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/RoleEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/RoleEntity.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; import java.util.List; /** @@ -15,11 +16,21 @@ import java.util.List; public class RoleEntity { private final String roleName; private final List<Member> roleMembers; + private final Boolean selfServe; + private final Boolean reviewEnabled; + private final List<AuditLogEntry> auditLog; @JsonCreator - public RoleEntity(@JsonProperty("roleName") String roleName, @JsonProperty("roleMembers") List<Member> roleMembers) { + public RoleEntity(@JsonProperty("roleName") String roleName, + @JsonProperty("roleMembers") List<Member> roleMembers, + @JsonProperty("selfServe") Boolean selfServe, + @JsonProperty("reviewEnabled") Boolean reviewEnabled, + @JsonProperty("auditLog") List<AuditLogEntry> auditLog) { this.roleName = roleName; this.roleMembers = roleMembers; + this.selfServe = selfServe; + this.reviewEnabled = reviewEnabled; + this.auditLog = auditLog == null ? new ArrayList<>() : auditLog; } public String roleName() { @@ -30,19 +41,37 @@ public class RoleEntity { return roleMembers; } + public Boolean selfServe() { + return selfServe; + } + + public Boolean reviewEnabled() { + return reviewEnabled; + } + + public List<AuditLogEntry> auditLog() { + return auditLog; + } + @JsonIgnoreProperties(ignoreUnknown = true) public static final class Member { private final String memberName; private final boolean active; private final boolean approved; private final String auditRef; + private final String requestTime; @JsonCreator - public Member(@JsonProperty("memberName") String memberName, @JsonProperty("active") boolean active, @JsonProperty("approved") boolean approved, @JsonProperty("auditRef") String auditRef) { + public Member(@JsonProperty("memberName") String memberName, + @JsonProperty("active") boolean active, + @JsonProperty("approved") boolean approved, + @JsonProperty("auditRef") String auditRef, + @JsonProperty("requestTime") String requestTime) { this.memberName = memberName; this.active = active; this.approved = approved; this.auditRef = auditRef; + this.requestTime = requestTime; } public String memberName() { @@ -56,5 +85,55 @@ public class RoleEntity { public String auditRef() { return auditRef; } + + public String requestTime() { + return requestTime; + } + + public boolean active() { + return active; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class AuditLogEntry { + private final String member; + private final String admin; + private final String action; + private final String auditRef; + private final String created; + + @JsonCreator + public AuditLogEntry(@JsonProperty("member") String member, + @JsonProperty("admin") String admin, + @JsonProperty("created") String created, + @JsonProperty("action") String action, + @JsonProperty("auditRef") String auditRef) { + this.member = member; + this.admin = admin; + this.created = created; + this.action = action; + this.auditRef = auditRef; + } + + public String getMember() { + return member; + } + + public String getAdmin() { + return admin; + } + + public String getAction() { + return action; + } + + public String getAuditRef() { + return auditRef; + } + + public String getCreated() { + return created; + } } } |