From 1d24832d61672ae3a302a7552068abc35461854b Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Fri, 19 Oct 2018 17:34:14 +0200 Subject: Add ZmsClient and DefaultZmsClient --- .../vespa/athenz/api/AthenzResourceGroup.java | 40 ++++++ .../yahoo/vespa/athenz/api/OktaAccessToken.java | 42 ++++++ .../vespa/athenz/client/zms/DefaultZmsClient.java | 141 +++++++++++++++++++++ .../yahoo/vespa/athenz/client/zms/RoleAction.java | 49 +++++++ .../yahoo/vespa/athenz/client/zms/ZmsClient.java | 35 +++++ .../athenz/client/zms/ZmsClientException.java | 32 +++++ .../client/zms/bindings/AccessResponseEntity.java | 19 +++ .../zms/bindings/DomainListResponseEntity.java | 21 +++ .../zms/bindings/MembershipResponseEntity.java | 28 ++++ .../ProviderResourceGroupRolesRequestEntity.java | 56 ++++++++ .../client/zms/bindings/TenancyRequestEntity.java | 31 +++++ 11 files changed, 494 insertions(+) create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzResourceGroup.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/OktaAccessToken.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/RoleAction.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClientException.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AccessResponseEntity.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/DomainListResponseEntity.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/MembershipResponseEntity.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/ProviderResourceGroupRolesRequestEntity.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/TenancyRequestEntity.java (limited to 'vespa-athenz') diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzResourceGroup.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzResourceGroup.java new file mode 100644 index 00000000000..2825cf57c7b --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzResourceGroup.java @@ -0,0 +1,40 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.util.Objects; + +/** + * @author bjorncs + */ +public class AthenzResourceGroup { + private final String name; + + public AthenzResourceGroup(String name) { + this.name = name; + } + + public String name() { + return name; + } + + @Override + public String toString() { + return "AthenzResourceGroup{" + + "name='" + name + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AthenzResourceGroup that = (AthenzResourceGroup) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/OktaAccessToken.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/OktaAccessToken.java new file mode 100644 index 00000000000..8b19f7abdd5 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/OktaAccessToken.java @@ -0,0 +1,42 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.util.Objects; + +/** + * @author bjorncs + */ +public class OktaAccessToken { + + public static final String HTTP_HEADER_NAME = "Okta-Access-Token"; + + private final String token; + + public OktaAccessToken(String token) { + this.token = token; + } + + public String token() { + return token; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OktaAccessToken that = (OktaAccessToken) o; + return Objects.equals(token, that.token); + } + + @Override + public int hashCode() { + return Objects.hash(token); + } + + @Override + public String toString() { + return "OktaAccessToken{" + + "token='" + token + '\'' + + '}'; + } +} 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 new file mode 100644 index 00000000000..ed9c1da56c2 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java @@ -0,0 +1,141 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms; + +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzResourceName; +import com.yahoo.vespa.athenz.api.AthenzRole; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.api.OktaAccessToken; +import com.yahoo.vespa.athenz.client.common.ClientBase; +import com.yahoo.vespa.athenz.client.zms.bindings.AccessResponseEntity; +import com.yahoo.vespa.athenz.client.zms.bindings.DomainListResponseEntity; +import com.yahoo.vespa.athenz.client.zms.bindings.MembershipResponseEntity; +import com.yahoo.vespa.athenz.client.zms.bindings.ProviderResourceGroupRolesRequestEntity; +import com.yahoo.vespa.athenz.client.zms.bindings.TenancyRequestEntity; +import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; +import org.apache.http.Header; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.message.BasicHeader; + +import javax.net.ssl.SSLContext; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import static java.util.stream.Collectors.toList; + +/** + * @author bjorncs + */ +public class DefaultZmsClient extends ClientBase implements ZmsClient { + + private final URI zmsUrl; + private final AthenzIdentity identity; + + public DefaultZmsClient(URI zmsUrl, AthenzIdentity identity, SSLContext sslContext) { + this(zmsUrl, identity, () -> sslContext); + } + + public DefaultZmsClient(URI zmsUrl, ServiceIdentityProvider identityProvider) { + this(zmsUrl, identityProvider.identity(), identityProvider::getIdentitySslContext); + } + + private DefaultZmsClient(URI zmsUrl, AthenzIdentity identity, Supplier sslContextSupplier) { + super("vespa-zms-client", sslContextSupplier, ZmsClientException::new); + this.zmsUrl = addTrailingSlash(zmsUrl); + this.identity = identity; + } + + private static URI addTrailingSlash(URI zmsUrl) { + return zmsUrl.getPath().endsWith("/") ? zmsUrl : URI.create(zmsUrl.toString() + '/'); + } + + @Override + public void createTenancy(AthenzDomain tenantDomain, AthenzService providerService, OktaAccessToken token) { + URI uri = zmsUrl.resolve(String.format("domain/%s/tenancy/%s", tenantDomain.getName(), providerService.getFullName())); + HttpUriRequest request = RequestBuilder.put() + .setUri(uri) + .addHeader(creatOktaAccessTokenHeader(token)) + .setEntity(toJsonStringEntity(new TenancyRequestEntity(tenantDomain, providerService, Collections.emptyList()))) + .build(); + execute(request, response -> { + readEntity(response, String.class); + return response.getStatusLine().getStatusCode(); + }); + } + + @Override + public void deleteTenancy(AthenzDomain tenantDomain, AthenzService providerService, OktaAccessToken token) { + URI uri = zmsUrl.resolve(String.format("domain/%s/tenancy/%s", tenantDomain.getName(), providerService.getFullName())); + HttpUriRequest request = RequestBuilder.delete() + .setUri(uri) + .addHeader(creatOktaAccessTokenHeader(token)) + .build(); + execute(request, response -> readEntity(response, String.class)); + } + + @Override + public void createProviderResourceGroup(AthenzDomain tenantDomain, AthenzService providerService, String resourceGroup, Set roleActions, OktaAccessToken token) { + URI uri = zmsUrl.resolve(String.format("domain/%s/provDomain/%s/provService/%s/resourceGroup/%s", tenantDomain.getName(), providerService.getDomainName(), providerService.getName(), resourceGroup)); + HttpUriRequest request = RequestBuilder.put() + .setUri(uri) + .addHeader(creatOktaAccessTokenHeader(token)) + .setEntity(toJsonStringEntity(new ProviderResourceGroupRolesRequestEntity(providerService, tenantDomain, roleActions, resourceGroup))) + .build(); + execute(request, response -> readEntity(response, String.class)); // The ZMS API will return a json object that is similar to ProviderResourceGroupRolesRequestEntity + } + + @Override + public void deleteProviderResourceGroup(AthenzDomain tenantDomain, AthenzService providerService, String resourceGroup, OktaAccessToken token) { + URI uri = zmsUrl.resolve(String.format("domain/%s/provDomain/%s/provService/%s/resourceGroup/%s", tenantDomain.getName(), providerService.getDomainName(), providerService.getName(), resourceGroup)); + HttpUriRequest request = RequestBuilder.delete() + .setUri(uri) + .addHeader(creatOktaAccessTokenHeader(token)) + .build(); + execute(request, response -> readEntity(response, String.class)); + } + + @Override + public boolean getMembership(AthenzRole role, AthenzIdentity identity) { + URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s", role.domain().getName(), role.roleName(), identity.getFullName())); + HttpUriRequest request = RequestBuilder.get() + .setUri(uri) + .build(); + return execute(request, response -> { + MembershipResponseEntity membership = readEntity(response, MembershipResponseEntity.class); + return membership.isMember; + }); + } + + @Override + public List getDomainList(String prefix) { + HttpUriRequest request = RequestBuilder.get() + .setUri(zmsUrl.resolve("domain")) + .addParameter("prefix", prefix) + .build(); + return execute(request, response -> { + DomainListResponseEntity result = readEntity(response, DomainListResponseEntity.class); + return result.domains.stream().map(AthenzDomain::new).collect(toList()); + }); + } + + @Override + public boolean hasAccess(AthenzResourceName resource, String action, AthenzIdentity identity) { + URI uri = zmsUrl.resolve(String.format("access/%s/%s", action, resource.toResourceNameString())); + HttpUriRequest request = RequestBuilder.get() + .setUri(uri) + .build(); + return execute(request, response -> { + AccessResponseEntity result = readEntity(response, AccessResponseEntity.class); + return result.granted; + }); + } + + private static Header creatOktaAccessTokenHeader(OktaAccessToken token) { + return new BasicHeader("Cookie", String.format("okta_at=%s", token.token())); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/RoleAction.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/RoleAction.java new file mode 100644 index 00000000000..405dd1aa56a --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/RoleAction.java @@ -0,0 +1,49 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms; + +import com.yahoo.vespa.athenz.api.AthenzRole; + +import java.util.Objects; + +/** + * @author bjorncs + */ +public class RoleAction { + private final String roleName; + private final String action; + + public RoleAction(String roleName, String action) { + this.roleName = roleName; + this.action = action; + } + + public String getRoleName() { + return roleName; + } + + public String getAction() { + return action; + } + + @Override + public String toString() { + return "RoleAction{" + + "roleName=" + roleName + + ", action='" + action + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleAction that = (RoleAction) o; + return Objects.equals(roleName, that.roleName) && + Objects.equals(action, that.action); + } + + @Override + public int hashCode() { + return Objects.hash(roleName, action); + } +} 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 new file mode 100644 index 00000000000..cf044edeac0 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java @@ -0,0 +1,35 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms; + +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzResourceName; +import com.yahoo.vespa.athenz.api.AthenzRole; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.api.OktaAccessToken; + +import java.time.Instant; +import java.util.List; +import java.util.Set; + +/** + * @author bjorncs + */ +public interface ZmsClient extends AutoCloseable { + + void createTenancy(AthenzDomain tenantDomain, AthenzService providerService, OktaAccessToken token); + + void deleteTenancy(AthenzDomain tenantDomain, AthenzService providerService, OktaAccessToken token); + + void createProviderResourceGroup(AthenzDomain tenantDomain, AthenzService providerService, String resourceGroup, Set roleActions, OktaAccessToken token); + + void deleteProviderResourceGroup(AthenzDomain tenantDomain, AthenzService providerService, String resourceGroup, OktaAccessToken token); + + boolean getMembership(AthenzRole role, AthenzIdentity identity); + + List getDomainList(String prefix); + + boolean hasAccess(AthenzResourceName resource, String action, AthenzIdentity identity); + + void close(); +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClientException.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClientException.java new file mode 100644 index 00000000000..f1b3ab8e7da --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClientException.java @@ -0,0 +1,32 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms; + +/** + * An exception that can be thrown by {@link ZmsClient} implementations. + * + * @author bjorncs + */ +public class ZmsClientException extends RuntimeException { + + private final int errorCode; + private final String description; + + public ZmsClientException(int errorCode, String description) { + super(createMessage(errorCode, description)); + this.errorCode = errorCode; + this.description = description; + } + + public int getErrorCode() { + return errorCode; + } + + public String getDescription() { + return description; + } + + private static String createMessage(int code, String description) { + return String.format("Received error from ZMS: code=%d, message=\"%s\"", code, description); + } + +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AccessResponseEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AccessResponseEntity.java new file mode 100644 index 00000000000..dcc17bc807a --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AccessResponseEntity.java @@ -0,0 +1,19 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms.bindings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author bjorncs + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AccessResponseEntity { + public final boolean granted; + + @JsonCreator + public AccessResponseEntity(@JsonProperty("granted") boolean granted) { + this.granted = granted; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/DomainListResponseEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/DomainListResponseEntity.java new file mode 100644 index 00000000000..938d85dd74f --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/DomainListResponseEntity.java @@ -0,0 +1,21 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms.bindings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * @author bjorncs + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class DomainListResponseEntity { + public final List domains; + + @JsonCreator + public DomainListResponseEntity(@JsonProperty("names") List domains) { + this.domains = domains; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/MembershipResponseEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/MembershipResponseEntity.java new file mode 100644 index 00000000000..499afb48f25 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/MembershipResponseEntity.java @@ -0,0 +1,28 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms.bindings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author bjorncs + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class MembershipResponseEntity { + public final String memberName; + public final boolean isMember; + public final String roleName; + public final String expiration; + + @JsonCreator + public MembershipResponseEntity(@JsonProperty("memberName") String memberName, + @JsonProperty("isMember") boolean isMember, + @JsonProperty("roleName") String roleName, + @JsonProperty("expiration") String expiration) { + this.memberName = memberName; + this.isMember = isMember; + this.roleName = roleName; + this.expiration = expiration; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/ProviderResourceGroupRolesRequestEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/ProviderResourceGroupRolesRequestEntity.java new file mode 100644 index 00000000000..dccd18fed61 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/ProviderResourceGroupRolesRequestEntity.java @@ -0,0 +1,56 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms.bindings; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.client.zms.RoleAction; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +/** + * @author bjorncs + */ +public class ProviderResourceGroupRolesRequestEntity { + + @JsonProperty("domain") + private final String domain; + + @JsonProperty("service") + private final String service; + + @JsonProperty("tenant") + private final String tenant; + + @JsonProperty("roles") + private final List roles; + + @JsonProperty("resourceGroup") + private final String resourceGroup; + + public ProviderResourceGroupRolesRequestEntity(AthenzService providerService, AthenzDomain tenantDomain, Set rolesActions, String resourceGroup) { + this.domain = providerService.getDomainName(); + this.service = providerService.getName(); + this.tenant = tenantDomain.getName(); + this.roles = rolesActions.stream().map(roleAction -> new TenantRoleAction(roleAction.getRoleName(), roleAction.getAction())).collect(toList()); + this.resourceGroup = resourceGroup; + } + + public static class TenantRoleAction { + @JsonProperty("role") + private final String role; + + @JsonProperty("action") + private final String action; + + public TenantRoleAction(String role, String action) { + this.role = role; + this.action = action; + } + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/TenancyRequestEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/TenancyRequestEntity.java new file mode 100644 index 00000000000..7883a505c71 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/TenancyRequestEntity.java @@ -0,0 +1,31 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.client.zms.bindings; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzService; + +import java.util.List; + +/** + * @author bjorncs + */ +public class TenancyRequestEntity { + + @JsonProperty("domain") + private final String tenantDomain; + + @JsonProperty("service") + private final String providerService; + + @JsonProperty("resourceGroups") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private final List resourceGroups; + + public TenancyRequestEntity(AthenzDomain tenantDomain, AthenzService providerService, List resourceGroups) { + this.tenantDomain = tenantDomain.getName(); + this.providerService = providerService.getFullName(); + this.resourceGroups = resourceGroups; + } +} -- cgit v1.2.3