Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion servers/zts/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<packaging>war</packaging>

<properties>
<code.coverage.min>0.9934</code.coverage.min>
<code.coverage.min>0.9937</code.coverage.min>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import com.yahoo.athenz.auth.Authorizer;
import com.yahoo.athenz.common.server.external.ExternalCredentialsProvider;
import com.yahoo.athenz.zts.external.azure.AzureAccessTokenProvider;
import com.yahoo.athenz.zts.external.gcp.GcpAccessTokenProvider;
import com.yahoo.athenz.zts.external.gcp.GcpTokenProvider;

import java.util.*;

Expand All @@ -30,7 +30,7 @@ public class ExternalCredentialsManager {
public ExternalCredentialsManager(Authorizer authorizer) {
externalCredentialsProviders = new HashMap<>();

ExternalCredentialsProvider gcpProvider = new GcpAccessTokenProvider();
ExternalCredentialsProvider gcpProvider = new GcpTokenProvider();
gcpProvider.setAuthorizer(authorizer);
externalCredentialsProviders.put(ZTSConsts.ZTS_EXTERNAL_CREDS_PROVIDER_GCP, gcpProvider);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import java.util.List;
import java.util.stream.Stream;

public class GcpAccessTokenRequest {
public class GcpAccessTokenRequest extends GcpTokenRequest {

private List<String> scope;
private String lifetime;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.yahoo.athenz.zts.external.gcp;

public class GcpIdTokenRequest extends GcpTokenRequest {

private String audience;
private boolean includeEmail;
private boolean organizationNumberIncluded;

public String getAudience() {
return audience;
}

public void setAudience(String audience) {
this.audience = audience;
}

public void setIncludeEmail(boolean includeEmail) {
this.includeEmail = includeEmail;
}

public boolean isIncludeEmail() {
return includeEmail;
}

public boolean isOrganizationNumberIncluded() {
return organizationNumberIncluded;
}

public void setOrganizationNumberIncluded(boolean organizationNumberIncluded) {
this.organizationNumberIncluded = organizationNumberIncluded;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.yahoo.athenz.zts.external.gcp;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class GcpIdTokenResponse {

private String token;

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class GcpAccessTokenError {
public class GcpTokenError {

private Error error;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@
import com.yahoo.athenz.common.server.http.HttpDriver;
import com.yahoo.athenz.common.server.http.HttpDriverResponse;
import com.yahoo.athenz.common.server.ServerResourceException;
import com.yahoo.athenz.zts.DomainDetails;
import com.yahoo.athenz.zts.ExternalCredentialsRequest;
import com.yahoo.athenz.zts.ExternalCredentialsResponse;
import com.yahoo.athenz.zts.ZTSConsts;
import com.yahoo.athenz.zts.*;
import com.yahoo.rdl.Timestamp;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.core5.http.ContentType;
Expand All @@ -40,10 +37,11 @@
import java.io.IOException;
import java.util.*;

public class GcpAccessTokenProvider implements ExternalCredentialsProvider {
public class GcpTokenProvider implements ExternalCredentialsProvider {

public static final String GCP_STS_TOKEN_URL = "https://sts.googleapis.com/v1/token";
public static final String GCP_SCOPE_ACTION = "gcp.scope_access";
public static final String GCP_AUDIENCE_ACTION = "gcp.audience_access";

public static final String GCP_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
public static final String GCP_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
Expand All @@ -54,8 +52,15 @@ public class GcpAccessTokenProvider implements ExternalCredentialsProvider {
public static final String GCP_WORKLOAD_POOL_NAME = "gcpWorkloadPoolName";
public static final String GCP_WORKLOAD_PROVIDER_NAME = "gcpWorkloadProviderName";
public static final String GCP_ACCESS_TOKEN = "accessToken";
public static final String GCP_ID_TOKEN = "token";
public static final String GCP_PROJECT_ID = "gcpProjectId";
public static final String GCP_PROJECT_NUMBER = "gcpProjectNumber";
public static final String GCP_FUNCTION_NAME = "gcpFunctionName";
public static final String GCP_GENERATE_ACCESS_TOKEN = "generateAccessToken";
public static final String GCP_GENERATE_ID_TOKEN = "generateIdToken";
public static final String GCP_AUDIENCE = "gcpAudience";
public static final String GCP_INCLUDE_EMAIL = "gcpIncludeEmail";
public static final String GCP_ORG_NUMBER_INCLUDED = "gcpOrganizationNumberIncluded";
private static final String GCP_DEFAULT_TOKEN_SCOPE = "https://www.googleapis.com/auth/cloud-platform";

HttpDriver httpDriver;
Expand All @@ -64,7 +69,7 @@ public class GcpAccessTokenProvider implements ExternalCredentialsProvider {
final String defaultWorkloadPoolName;
final String defaultWorkloadProviderName;

public GcpAccessTokenProvider() {
public GcpTokenProvider() {
this.httpDriver = new HttpDriver.Builder(null)
.clientConnectTimeoutMs(3000)
.clientReadTimeoutMs(10000)
Expand Down Expand Up @@ -132,36 +137,16 @@ GcpExchangeTokenResponse getExchangeToken(DomainDetails domainDetails, final Str
}

/**
* First, we're going to get our exchange token based on our ZTS ID token
* and then request an access token for the given scope as described in the GCP docs:
* Request an access token for the given scope as described in the GCP docs:
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
*/
@Override
public ExternalCredentialsResponse getCredentials(Principal principal, DomainDetails domainDetails, List<String> idTokenGroups,
IdToken idToken, IdTokenSigner idTokenSigner, ExternalCredentialsRequest externalCredentialsRequest)
throws ServerResourceException {

// first make sure that our required components are available and configured

if (authorizer == null) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, "ZTS authorizer not configured");
}
if (StringUtil.isEmpty(domainDetails.getGcpProjectId())) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, "gcp project id not configured for domain");
}
if (StringUtil.isEmpty(domainDetails.getGcpProjectNumber())) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, "gcp project number not configured for domain");
}

Map<String, String> attributes = externalCredentialsRequest.getAttributes();
final String gcpServiceAccount = getRequestAttribute(attributes, GCP_SERVICE_ACCOUNT, null);
if (StringUtil.isEmpty(gcpServiceAccount)) {
throw new ServerResourceException(ServerResourceException.BAD_REQUEST, "missing gcp service account");
}
ExternalCredentialsResponse getAccessToken(Principal principal, DomainDetails domainDetails,
final String signedIdToken, ExternalCredentialsRequest externalCredentialsRequest,
final String gcpServiceAccount, Map<String, String> requestAttributes) throws ServerResourceException {

// verify that the given principal is authorized for all scopes requested

final String gcpTokenScope = getRequestAttribute(attributes, GCP_TOKEN_SCOPE, GCP_DEFAULT_TOKEN_SCOPE);
final String gcpTokenScope = getRequestAttribute(requestAttributes, GCP_TOKEN_SCOPE, GCP_DEFAULT_TOKEN_SCOPE);
String[] gcpTokenScopeList = gcpTokenScope.split(" ");
for (String scopeItem : gcpTokenScopeList) {
final String resource = domainDetails.getName() + ":" + scopeItem;
Expand All @@ -170,13 +155,6 @@ public ExternalCredentialsResponse getCredentials(Principal principal, DomainDet
}
}

// Set the requested groups as the groups claim in the signed id token

idToken.setSubject(principal.getFullName());
idToken.setAudience(externalCredentialsRequest.getClientId());
idToken.setGroups(idTokenGroups);
String signedIdToken = idTokenSigner.sign(idToken, null);

try {
// first we're going to get our exchange token

Expand All @@ -197,23 +175,126 @@ public ExternalCredentialsResponse getCredentials(Principal principal, DomainDet

final HttpDriverResponse httpResponse = httpDriver.doPostHttpResponse(httpPost);
if (httpResponse.getStatusCode() != HttpStatus.SC_OK) {
GcpAccessTokenError error = jsonMapper.readValue(httpResponse.getMessage(), GcpAccessTokenError.class);
GcpTokenError error = jsonMapper.readValue(httpResponse.getMessage(), GcpTokenError.class);
throw new ServerResourceException(httpResponse.getStatusCode(), error.getErrorMessage());
}

GcpAccessTokenResponse gcpAccessTokenResponse = jsonMapper.readValue(httpResponse.getMessage(), GcpAccessTokenResponse.class);

ExternalCredentialsResponse externalCredentialsResponse = new ExternalCredentialsResponse();
attributes = new HashMap<>();
attributes.put(GCP_ACCESS_TOKEN, gcpAccessTokenResponse.getAccessToken());
attributes.put(GCP_PROJECT_ID, domainDetails.getGcpProjectId());
attributes.put(GCP_PROJECT_NUMBER, domainDetails.getGcpProjectNumber());
externalCredentialsResponse.setAttributes(attributes);
Map<String, String> responseAttributes = new HashMap<>();
responseAttributes.put(GCP_ACCESS_TOKEN, gcpAccessTokenResponse.getAccessToken());
responseAttributes.put(GCP_PROJECT_ID, domainDetails.getGcpProjectId());
responseAttributes.put(GCP_PROJECT_NUMBER, domainDetails.getGcpProjectNumber());
externalCredentialsResponse.setAttributes(responseAttributes);
externalCredentialsResponse.setExpiration(Timestamp.fromString(gcpAccessTokenResponse.getExpireTime()));
return externalCredentialsResponse;

} catch (Exception ex) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, ex.getMessage());
}
}

/**
* Request an access token for the given scope as described in the GCP docs:
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken
*/
ExternalCredentialsResponse getIdToken(Principal principal, DomainDetails domainDetails,
final String signedIdToken, ExternalCredentialsRequest externalCredentialsRequest,
final String gcpServiceAccount, Map<String, String> requestAttributes) throws ServerResourceException {

// verify that the given principal is authorized for all scopes requested

final String gcpAudience = getRequestAttribute(requestAttributes, GCP_AUDIENCE, null);
if (StringUtil.isEmpty(gcpAudience)) {
throw new ServerResourceException(ServerResourceException.BAD_REQUEST, "gcp audience not specified");
}
final String resource = domainDetails.getName() + ":" + gcpAudience;
if (!authorizer.access(GCP_AUDIENCE_ACTION, resource, principal, null)) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, "Principal not authorized for configured audience");
}

try {
// first we're going to get our exchange token

GcpExchangeTokenResponse exchangeTokenResponse = getExchangeToken(domainDetails, signedIdToken, externalCredentialsRequest);

GcpIdTokenRequest idTokenRequest = new GcpIdTokenRequest();
idTokenRequest.setAudience(gcpAudience);
idTokenRequest.setIncludeEmail(Boolean.parseBoolean(
getRequestAttribute(requestAttributes, GCP_INCLUDE_EMAIL, "false")));
idTokenRequest.setOrganizationNumberIncluded(Boolean.parseBoolean(
getRequestAttribute(requestAttributes, GCP_ORG_NUMBER_INCLUDED, "false")));

final String serviceUrl = String.format("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s@%s.iam.gserviceaccount.com:generateIdToken",
gcpServiceAccount, domainDetails.getGcpProjectId());
final String authorizationHeader = exchangeTokenResponse.getTokenType() + " " + exchangeTokenResponse.getAccessToken();

HttpPost httpPost = new HttpPost(serviceUrl);
httpPost.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
httpPost.setEntity(new StringEntity(jsonMapper.writeValueAsString(idTokenRequest), ContentType.APPLICATION_JSON));

final HttpDriverResponse httpResponse = httpDriver.doPostHttpResponse(httpPost);
if (httpResponse.getStatusCode() != HttpStatus.SC_OK) {
GcpTokenError error = jsonMapper.readValue(httpResponse.getMessage(), GcpTokenError.class);
throw new ServerResourceException(httpResponse.getStatusCode(), error.getErrorMessage());
}

GcpIdTokenResponse gcpIdTokenResponse = jsonMapper.readValue(httpResponse.getMessage(), GcpIdTokenResponse.class);

ExternalCredentialsResponse externalCredentialsResponse = new ExternalCredentialsResponse();
Map<String, String> responseAttributes = new HashMap<>();
responseAttributes.put(GCP_ID_TOKEN, gcpIdTokenResponse.getToken());
responseAttributes.put(GCP_PROJECT_ID, domainDetails.getGcpProjectId());
responseAttributes.put(GCP_PROJECT_NUMBER, domainDetails.getGcpProjectNumber());
externalCredentialsResponse.setAttributes(responseAttributes);
return externalCredentialsResponse;

} catch (Exception ex) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, ex.getMessage());
}
}

@Override
public ExternalCredentialsResponse getCredentials(Principal principal, DomainDetails domainDetails,
List<String> idTokenGroups, IdToken idToken, IdTokenSigner idTokenSigner,
ExternalCredentialsRequest externalCredentialsRequest) throws ServerResourceException {

// first make sure that our required components are available and configured

if (authorizer == null) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, "ZTS authorizer not configured");
}
if (StringUtil.isEmpty(domainDetails.getGcpProjectId())) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, "gcp project id not configured for domain");
}
if (StringUtil.isEmpty(domainDetails.getGcpProjectNumber())) {
throw new ServerResourceException(ServerResourceException.FORBIDDEN, "gcp project number not configured for domain");
}

Map<String, String> attributes = externalCredentialsRequest.getAttributes();
final String gcpServiceAccount = getRequestAttribute(attributes, GCP_SERVICE_ACCOUNT, null);
if (StringUtil.isEmpty(gcpServiceAccount)) {
throw new ServerResourceException(ServerResourceException.BAD_REQUEST, "missing gcp service account");
}

// Set the requested groups as the groups claim in the signed id token

idToken.setSubject(principal.getFullName());
idToken.setAudience(externalCredentialsRequest.getClientId());
idToken.setGroups(idTokenGroups);
String signedIdToken = idTokenSigner.sign(idToken, null);

final String gcpFunctionName = getRequestAttribute(attributes, GCP_FUNCTION_NAME, GCP_GENERATE_ACCESS_TOKEN);
switch (gcpFunctionName) {
case GCP_GENERATE_ACCESS_TOKEN:
return getAccessToken(principal, domainDetails, signedIdToken, externalCredentialsRequest,
gcpServiceAccount, attributes);
case GCP_GENERATE_ID_TOKEN:
return getIdToken(principal, domainDetails, signedIdToken, externalCredentialsRequest,
gcpServiceAccount, attributes);
default:
throw new ServerResourceException(ServerResourceException.BAD_REQUEST, "invalid gcp function name");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.yahoo.athenz.zts.external.gcp;

import java.util.List;

public class GcpTokenRequest {

private List<String> delegates;

public List<String> getDelegates() {
return delegates;
}

public void setDelegates(List<String> delegates) {
this.delegates = delegates;
}
}
Loading
Loading