|
34 | 34 | import static com.google.common.base.MoreObjects.firstNonNull;
|
35 | 35 |
|
36 | 36 | import java.io.IOException;
|
| 37 | +import java.io.InputStream; |
37 | 38 | import java.text.DateFormat;
|
38 | 39 | import java.text.ParseException;
|
39 | 40 | import java.text.SimpleDateFormat;
|
|
46 | 47 |
|
47 | 48 | import com.google.api.client.http.GenericUrl;
|
48 | 49 | import com.google.api.client.http.HttpContent;
|
| 50 | +import com.google.api.client.http.HttpHeaders; |
49 | 51 | import com.google.api.client.http.HttpRequest;
|
50 | 52 | import com.google.api.client.http.HttpRequestFactory;
|
51 | 53 | import com.google.api.client.http.HttpResponse;
|
| 54 | +import com.google.api.client.http.HttpStatusCodes; |
52 | 55 | import com.google.api.client.http.HttpTransport;
|
53 | 56 | import com.google.api.client.http.json.JsonHttpContent;
|
54 | 57 | import com.google.api.client.json.JsonObjectParser;
|
|
57 | 60 | import com.google.auth.http.HttpTransportFactory;
|
58 | 61 | import com.google.common.base.MoreObjects;
|
59 | 62 | import com.google.common.collect.ImmutableMap;
|
| 63 | +import com.google.common.io.BaseEncoding; |
| 64 | + |
| 65 | +import com.google.auth.ServiceAccountSigner; |
60 | 66 |
|
61 | 67 | /**
|
62 | 68 | * ImpersonatedCredentials allowing credentials issued to a user or service account to impersonate
|
|
81 | 87 | * System.out.println(b);
|
82 | 88 | * </pre>
|
83 | 89 | */
|
84 |
| -public class ImpersonatedCredentials extends GoogleCredentials { |
| 90 | +public class ImpersonatedCredentials extends GoogleCredentials implements ServiceAccountSigner { |
85 | 91 |
|
86 | 92 | private static final long serialVersionUID = -2133257318957488431L;
|
87 | 93 | private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
88 | 94 | private static final int ONE_HOUR_IN_SECONDS = 3600;
|
89 | 95 | private static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
|
90 |
| - private static final String IAM_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken"; |
| 96 | + private static final String IAM_ACCESS_TOKEN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken"; |
| 97 | + private static final String IAM_ID_TOKEN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken"; |
| 98 | + private static final String IAM_SIGN_ENDPOINT = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; |
91 | 99 |
|
92 | 100 | private static final String SCOPE_EMPTY_ERROR = "Scopes cannot be null";
|
93 | 101 | private static final String LIFETIME_EXCEEDED_ERROR = "lifetime must be less than or equal to 3600";
|
| 102 | + private static final String PARSE_ERROR_SIGNATURE = "Error parsing signature response. "; |
| 103 | + private static final String PARSE_ERROR_MESSAGE = "Error parsing error message response. "; |
94 | 104 |
|
95 | 105 | private GoogleCredentials sourceCredentials;
|
96 | 106 | private String targetPrincipal;
|
@@ -153,6 +163,85 @@ public static ImpersonatedCredentials create(GoogleCredentials sourceCredentials
|
153 | 163 | .build();
|
154 | 164 | }
|
155 | 165 |
|
| 166 | + /** |
| 167 | + * Returns the email field of the serviceAccount that is being impersonated. |
| 168 | + * |
| 169 | + * @return email address of the impesonated service account. |
| 170 | + */ |
| 171 | + @Override |
| 172 | + public String getAccount() { |
| 173 | + return this.targetPrincipal; |
| 174 | + } |
| 175 | + |
| 176 | + /** |
| 177 | + * Signs the provided bytes using the private key associated with the impersonated |
| 178 | + * service account |
| 179 | + * |
| 180 | + * @param toSign bytes to sign |
| 181 | + * @return signed bytes |
| 182 | + * @throws SigningException if the attempt to sign the provided bytes failed |
| 183 | + * @see <a href="https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/signBlob">Blob Signing</a> |
| 184 | + */ |
| 185 | + @Override |
| 186 | + public byte[] sign(byte[] toSign) { |
| 187 | + BaseEncoding base64 = BaseEncoding.base64(); |
| 188 | + String signature; |
| 189 | + try { |
| 190 | + signature = getSignature(base64.encode(toSign)); |
| 191 | + } catch (IOException ex) { |
| 192 | + throw new SigningException("Failed to sign the provided bytes", ex); |
| 193 | + } |
| 194 | + return base64.decode(signature); |
| 195 | + } |
| 196 | + |
| 197 | + private String getSignature(String bytes) throws IOException { |
| 198 | + String signBlobUrl = String.format(IAM_SIGN_ENDPOINT, getAccount()); |
| 199 | + GenericUrl genericUrl = new GenericUrl(signBlobUrl); |
| 200 | + |
| 201 | + GenericData signRequest = new GenericData(); |
| 202 | + signRequest.set("delegates", this.delegates); |
| 203 | + signRequest.set("payload", bytes); |
| 204 | + JsonHttpContent signContent = new JsonHttpContent(OAuth2Utils.JSON_FACTORY, signRequest); |
| 205 | + HttpTransport httpTransport = this.transportFactory.create(); |
| 206 | + HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(sourceCredentials); |
| 207 | + HttpRequestFactory requestFactory = httpTransport.createRequestFactory(); |
| 208 | + |
| 209 | + HttpRequest request = requestFactory.buildPostRequest(genericUrl, signContent); |
| 210 | + Map<String, List<String>> headers = getRequestMetadata(); |
| 211 | + HttpHeaders requestHeaders = request.getHeaders(); |
| 212 | + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { |
| 213 | + requestHeaders.put(entry.getKey(), entry.getValue()); |
| 214 | + } |
| 215 | + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); |
| 216 | + adapter.initialize(request); |
| 217 | + request.setParser(parser); |
| 218 | + request.setThrowExceptionOnExecuteError(false); |
| 219 | + |
| 220 | + HttpResponse response = request.execute(); |
| 221 | + int statusCode = response.getStatusCode(); |
| 222 | + if (statusCode >= 400 && statusCode < HttpStatusCodes.STATUS_CODE_SERVER_ERROR) { |
| 223 | + GenericData responseError = response.parseAs(GenericData.class); |
| 224 | + Map<String, Object> error = OAuth2Utils.validateMap(responseError, "error", PARSE_ERROR_MESSAGE); |
| 225 | + String errorMessage = OAuth2Utils.validateString(error, "message", PARSE_ERROR_MESSAGE); |
| 226 | + throw new IOException(String.format("Error code %s trying to sign provided bytes: %s", |
| 227 | + statusCode, errorMessage)); |
| 228 | + } |
| 229 | + if (statusCode != HttpStatusCodes.STATUS_CODE_OK) { |
| 230 | + throw new IOException(String.format("Unexpected Error code %s trying to sign provided bytes: %s", statusCode, |
| 231 | + response.parseAsString())); |
| 232 | + } |
| 233 | + InputStream content = response.getContent(); |
| 234 | + if (content == null) { |
| 235 | + // Throw explicitly here on empty content to avoid NullPointerException from parseAs call. |
| 236 | + // Mock transports will have success code with empty content by default. |
| 237 | + throw new IOException("Empty content from sign blob server request."); |
| 238 | + } |
| 239 | + |
| 240 | + GenericData responseData = response.parseAs(GenericData.class); |
| 241 | + return OAuth2Utils.validateString(responseData, "signedBlob", PARSE_ERROR_SIGNATURE); |
| 242 | + } |
| 243 | + |
| 244 | + |
156 | 245 | private ImpersonatedCredentials(Builder builder) {
|
157 | 246 | this.sourceCredentials = builder.getSourceCredentials();
|
158 | 247 | this.targetPrincipal = builder.getTargetPrincipal();
|
@@ -192,7 +281,7 @@ public AccessToken refreshAccessToken() throws IOException {
|
192 | 281 | HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(sourceCredentials);
|
193 | 282 | HttpRequestFactory requestFactory = httpTransport.createRequestFactory();
|
194 | 283 |
|
195 |
| - String endpointUrl = String.format(IAM_ENDPOINT, this.targetPrincipal); |
| 284 | + String endpointUrl = String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal); |
196 | 285 | GenericUrl url = new GenericUrl(endpointUrl);
|
197 | 286 |
|
198 | 287 | Map<String, Object> body = ImmutableMap.<String, Object>of("delegates", this.delegates, "scope",
|
|
0 commit comments