Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ServiceAccountSigner interface, enable signing for AE credentials #854

Merged
merged 1 commit into from
Apr 6, 2016
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
104 changes: 94 additions & 10 deletions gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Collection;
import java.util.Objects;

Expand All @@ -35,16 +40,26 @@
*/
public abstract class AuthCredentials implements Restorable<AuthCredentials> {

private static class AppEngineAuthCredentials extends AuthCredentials {
/**
* Represents built-in credentials when running in Google App Engine.
*/
public static class AppEngineAuthCredentials extends AuthCredentials
implements ServiceAccountSigner {

private static final AuthCredentials INSTANCE = new AppEngineAuthCredentials();
private static final AppEngineAuthCredentialsState STATE = new AppEngineAuthCredentialsState();

private static class AppEngineCredentials extends GoogleCredentials {
private AppEngineCredentials credentials;

private static class AppEngineCredentials extends GoogleCredentials
implements ServiceAccountSigner {

private final Object appIdentityService;
private final String account;
private final Method getAccessToken;
private final Method getAccessTokenResult;
private final Method signForApp;
private final Method getSignature;
private final Collection<String> scopes;

AppEngineCredentials() {
Expand All @@ -59,6 +74,12 @@ private static class AppEngineCredentials extends GoogleCredentials {
"com.google.appengine.api.appidentity.AppIdentityService$GetAccessTokenResult");
this.getAccessTokenResult = serviceClass.getMethod("getAccessToken", Iterable.class);
this.getAccessToken = tokenResultClass.getMethod("getAccessToken");
this.account = (String) serviceClass.getMethod("getServiceAccountName")
.invoke(appIdentityService);
this.signForApp = serviceClass.getMethod("signForApp", byte[].class);
Class<?> signingResultClass = Class.forName(
"com.google.appengine.api.appidentity.AppIdentityService$SigningResult");
this.getSignature = signingResultClass.getMethod("getSignature");
this.scopes = null;
} catch (Exception e) {
throw new RuntimeException("Could not create AppEngineCredentials.", e);
Expand All @@ -69,11 +90,14 @@ private static class AppEngineCredentials extends GoogleCredentials {
this.appIdentityService = unscoped.appIdentityService;
this.getAccessToken = unscoped.getAccessToken;
this.getAccessTokenResult = unscoped.getAccessTokenResult;
this.account = unscoped.account;
this.signForApp = unscoped.signForApp;
this.getSignature = unscoped.getSignature;
this.scopes = scopes;
}

/**
* Refresh the access token by getting it from the App Identity service
* Refresh the access token by getting it from the App Identity service.
*/
@Override
public AccessToken refreshAccessToken() throws IOException {
Expand All @@ -98,6 +122,21 @@ public boolean createScopedRequired() {
public GoogleCredentials createScoped(Collection<String> scopes) {
return new AppEngineCredentials(scopes, this);
}

@Override
public String account() {
return account;
}

@Override
public byte[] sign(byte[] toSign) {
try {
Object signingResult = signForApp.invoke(appIdentityService, (Object) toSign);
return (byte[]) getSignature.invoke(signingResult);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new SigningException("Failed to sign the provided bytes", ex);
}
}
}

private static class AppEngineAuthCredentialsState
Expand All @@ -122,14 +161,27 @@ public boolean equals(Object obj) {
}

@Override
public GoogleCredentials credentials() {
return new AppEngineCredentials();
public AppEngineCredentials credentials() {
if (credentials == null) {
credentials = new AppEngineCredentials();
}
return credentials;
}

@Override
public RestorableState<AuthCredentials> capture() {
return STATE;
}

@Override
public String account() {
return credentials().account();
}

@Override
public byte[] sign(byte[] toSign) {
return credentials().sign(toSign);
}
}

/**
Expand All @@ -138,8 +190,10 @@ public RestorableState<AuthCredentials> capture() {
* @see <a href="https://cloud.google.com/docs/authentication#user_accounts_and_service_accounts">
* User accounts and service accounts</a>
*/
public static class ServiceAccountAuthCredentials extends AuthCredentials {
public static class ServiceAccountAuthCredentials extends AuthCredentials
implements ServiceAccountSigner {

private final ServiceAccountCredentials credentials;
private final String account;
private final PrivateKey privateKey;

Expand Down Expand Up @@ -178,23 +232,44 @@ public boolean equals(Object obj) {
}

ServiceAccountAuthCredentials(String account, PrivateKey privateKey) {
this.account = checkNotNull(account);
this.privateKey = checkNotNull(privateKey);
this(new ServiceAccountCredentials(null, account, privateKey, null, null));
}

ServiceAccountAuthCredentials(ServiceAccountCredentials credentials) {
this.credentials = checkNotNull(credentials);
this.account = checkNotNull(credentials.getClientEmail());
this.privateKey = checkNotNull(credentials.getPrivateKey());
}

@Override
public ServiceAccountCredentials credentials() {
return new ServiceAccountCredentials(null, account, privateKey, null, null);
return credentials;
}

@Override
public String account() {
return account;
}

/**
* Returns the private key associated with the service account credentials.
*/
public PrivateKey privateKey() {
return privateKey;
}

@Override
public byte[] sign(byte[] toSign) {
try {
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey());
signer.update(toSign);
return signer.sign();
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) {
throw new SigningException("Failed to sign the provided bytes", ex);
}
}

@Override
public RestorableState<AuthCredentials> capture() {
return new ServiceAccountAuthCredentialsState(account, privateKey);
Expand Down Expand Up @@ -242,6 +317,10 @@ public boolean equals(Object obj) {
}
}

ApplicationDefaultAuthCredentials(GoogleCredentials credentials) {
googleCredentials = credentials;
}

ApplicationDefaultAuthCredentials() throws IOException {
googleCredentials = GoogleCredentials.getApplicationDefault();
}
Expand Down Expand Up @@ -320,7 +399,12 @@ public static AuthCredentials createForAppEngine() {
* @throws IOException if the credentials cannot be created in the current environment
*/
public static AuthCredentials createApplicationDefaults() throws IOException {
return new ApplicationDefaultAuthCredentials();
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
if (credentials instanceof ServiceAccountCredentials) {
ServiceAccountCredentials serviceAccountCredentials = (ServiceAccountCredentials) credentials;
return new ServiceAccountAuthCredentials(serviceAccountCredentials);
}
return new ApplicationDefaultAuthCredentials(credentials);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.google.gcloud;

import java.util.Objects;

/**
* Interface for a service account signer. A signer for a service account is capable of signing
* bytes using the private key associated with its service account.
*/
public interface ServiceAccountSigner {

class SigningException extends RuntimeException {

private static final long serialVersionUID = 8962780757822799255L;

SigningException(String message, Exception cause) {
super(message, cause);
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof SigningException)) {
return false;
}
SigningException other = (SigningException) obj;
return Objects.equals(getCause(), other.getCause())
&& Objects.equals(getMessage(), other.getMessage());
}

@Override
public int hashCode() {
return Objects.hash(getMessage(), getCause());
}
}

/**
* Returns the service account associated with the signer.
*/
String account();

/**
* Signs the provided bytes using the private key associated with the service account.
*
* @param toSign bytes to sign
* @return signed bytes
* @throws SigningException if the attempt to sign the provided bytes failed
*/
byte[] sign(byte[] toSign);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,22 @@
package com.google.gcloud;

import com.google.common.collect.ImmutableList;
import com.google.gcloud.ServiceAccountSigner.SigningException;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;

public class SerializationTest extends BaseSerializationTest {

private static class SomeIamPolicy extends IamPolicy<String> {

private static final long serialVersionUID = 271243551016958285L;

private static class Builder extends IamPolicy.Builder<String, Builder> {

@Override
public SomeIamPolicy build() {
return new SomeIamPolicy(this);
}
}

protected SomeIamPolicy(Builder builder) {
super(builder);
}

@Override
public Builder toBuilder() {
return new Builder();
}
}

private static final BaseServiceException BASE_SERVICE_EXCEPTION =
new BaseServiceException(42, "message", "reason", true);
private static final ExceptionHandler EXCEPTION_HANDLER = ExceptionHandler.defaultInstance();
private static final Identity IDENTITY = Identity.allAuthenticatedUsers();
private static final PageImpl<String> PAGE =
new PageImpl<>(null, "cursor", ImmutableList.of("string1", "string2"));
private static final SigningException SIGNING_EXCEPTION =

This comment was marked as spam.

new SigningException("message", BASE_SERVICE_EXCEPTION);
private static final RetryParams RETRY_PARAMS = RetryParams.defaultInstance();
private static final SomeIamPolicy SOME_IAM_POLICY = new SomeIamPolicy.Builder().build();
private static final String JSON_KEY = "{\n"
Expand Down Expand Up @@ -81,10 +62,32 @@ public Builder toBuilder() {
+ " \"type\": \"service_account\"\n"
+ "}";

private static class SomeIamPolicy extends IamPolicy<String> {

private static final long serialVersionUID = 271243551016958285L;

private static class Builder extends IamPolicy.Builder<String, Builder> {

@Override
public SomeIamPolicy build() {
return new SomeIamPolicy(this);
}
}

protected SomeIamPolicy(Builder builder) {
super(builder);
}

@Override
public Builder toBuilder() {
return new Builder();
}
}

@Override
protected Serializable[] serializableObjects() {
return new Serializable[]{BASE_SERVICE_EXCEPTION, EXCEPTION_HANDLER, IDENTITY, PAGE,
RETRY_PARAMS, SOME_IAM_POLICY};
RETRY_PARAMS, SOME_IAM_POLICY, SIGNING_EXCEPTION};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ public void run(Storage storage, Tuple<ServiceAccountAuthCredentials, BlobInfo>
private void run(Storage storage, ServiceAccountAuthCredentials cred, BlobInfo blobInfo) {
Blob blob = storage.get(blobInfo.blobId());
System.out.println("Signed URL: "
+ blob.signUrl(1, TimeUnit.DAYS, SignUrlOption.serviceAccount(cred)));
+ blob.signUrl(1, TimeUnit.DAYS, SignUrlOption.signWith(cred)));
}

@Override
Expand Down
Loading