Skip to content

Instantly share code, notes, and snippets.

@EliasRanz
Last active July 13, 2019 16:08
Show Gist options
  • Save EliasRanz/0b3f95e3b786df69413c7120d800f0ac to your computer and use it in GitHub Desktop.
Save EliasRanz/0b3f95e3b786df69413c7120d800f0ac to your computer and use it in GitHub Desktop.
A helper class that allows you to store Google Credential files in Amazon S3, and loads it into context for Google's SDKs.
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Map;
public class SystemEnvironmentUtil {
public static void setEnv(Map<String, String> newenv) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException{
try {
Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
theEnvironmentField.setAccessible(true);
Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
env.putAll(newenv);
Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
theCaseInsensitiveEnvironmentField.setAccessible(true);
Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
cienv.putAll(newenv);
} catch (NoSuchFieldException e) {
Class[] classes = Collections.class.getDeclaredClasses();
Map<String, String> env = System.getenv();
for(Class cl : classes) {
if("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
Field field = cl.getDeclaredField("m");
field.setAccessible(true);
Object obj = field.get(env);
Map<String, String> map = (Map<String, String>) obj;
map.clear();
map.putAll(newenv);
}
}
}
}
}
@EliasRanz
Copy link
Author

EliasRanz commented Jul 13, 2019

Goal

I have a file stored in Amazon S3 that stores my credentials for my Google Application that's utilized by Dialogflow for Natural Language Processing. I wanted to configure a Spring Bean that allowed me to just @Autowired the credential provider from the Dialogflow SDK. The application is running on spring-boot.

Problem

Locally when I run the application I get the following exception thrown from the Google SDK.

java.io.IOException: The Application Default Credentials are not available. They are available if running in Google Compute Engine. 
Otherwise, the environment variable GOOGLE_APPLICATION_CREDENTIALS must be defined pointing to a file defining the credentials. See https://developers.google.com/accounts/docs/application-default-credentials for more information.

Since the file isn't stored on disc, I have to download the file and then load it into the system's environment variables. This is the important part, because at Runtime the file doesn't exist, and Google is expecting it to be loaded at the system level, not the Java process level so it fails saying it can't find the file.

Solution

Courtesy of this stackoverflow post I was able to use their Reflection method to grab the System that is running the Java process and then adding an additional property to the environment, which then allowed Google's SDK to receive the credentials correctly. When I load the credentials into the DIalogflow SDK I simply just do it with the following...

@Autowired
private ServiceAccountCredentials serviceAccountCredentials;

Then I can make authenticated calls no problem.

Additional Code

AWSConfig.java

Receives it's credentials from ~/.aws/credentials on OSX.

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AWSConfig {
    @Value("${AWS_DEFAULT_REGION}")
    private String region;

    @Bean
    public AWSCredentialsProvider awsCredentialsProvider() {
        return new DefaultAWSCredentialsProviderChain();
    }

    @Bean
    public AmazonS3 amazonS3() {
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(awsCredentialsProvider())
                .build();
    }
}

GoogleConfig.java

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.eliasranz.bot.utils.SystemEnvironmentUtil;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.lang.String.format;

@Configuration
@Slf4j
public class GoogleConfig {
    private final AmazonS3 amazonS3;
    private static final String DIRECTORY_NAME = "<APP_DIRECTORY>";
    private static final String GOOGLE_CREDENTIALS_ENV_PROPERTY = "GOOGLE_APPLICATION_CREDENTIALS";
    private static final String GOOGLE_SERVICE_ACCOUNT_CREDENTIAL_FILE = "<GOOGLE_CREDENTIALS_JSON_FILE>";

    @Autowired
    public GoogleConfig(AmazonS3 amazonS3) {
        this.amazonS3 = amazonS3;
    }

    @Bean
    @DependsOn("googleCredentials")
    public ServiceAccountCredentials serviceAccountCredentials() throws IOException {
        return (ServiceAccountCredentials) googleCredentials();
    }

    @Bean
    public GoogleCredentials googleCredentials() throws IOException {
        List<String> scopes = new ArrayList<>();
        scopes.add("https://www.googleapis.com/auth/dialogflow");
        scopes.add("https://www.googleapis.com/auth/cloud-platform");

        // Setting GOOGLE_APPLICATION_CREDENTIALS environment variable because Google needs the credentials defined
        // in their environment, but first we need to make sure we have a path to download the file from S3.
        File homeDir = new File(System.getProperty("user.home"));
        log.debug("Home Directory: " + homeDir.getAbsoluteFile());

        File configDirectory = new File(homeDir.getAbsoluteFile() + "/" + DIRECTORY_NAME);

        if(!configDirectory.exists()) {
            boolean created = configDirectory.mkdirs();
            log.debug("Didn't find the directory at " + homeDir);
            log.debug("Directory created: " + created);
        }

        File credentialsFile = new File(configDirectory + "/" + GOOGLE_SERVICE_ACCOUNT_CREDENTIAL_FILE);
        ObjectMetadata s3Credentials = amazonS3.getObject(new GetObjectRequest(DIRECTORY_NAME, GOOGLE_SERVICE_ACCOUNT_CREDENTIAL_FILE), credentialsFile);

        Map<String, String> google = new HashMap<>();
        google.put(GOOGLE_CREDENTIALS_ENV_PROPERTY, credentialsFile.getPath());
        try {
            SystemEnvironmentUtil.setEnv(google);
        } catch (Exception e) {
            log.error(format("Failed to set %s environment variable", GOOGLE_CREDENTIALS_ENV_PROPERTY), e);
        }
        GoogleCredentials credentials = GoogleCredentials.getApplicationDefault().createScoped(scopes);
        credentials.refreshIfExpired();
        log.debug(credentials.toString());

        return credentials;
    }
}

Closing

If you have comments/questions let me know, or if you can think of a better method then let me know in the comments below as I'd love to be able to improve the approach that I am doing.

Note that since we're modifying things at the system level, you need to be extra careful with security so that you don't add a vulnerability onto the host system. E.g. you'll want to make sure that it's private, and you're explicitly telling it what to do rather than dynamically allowing it to make system level changes.

A solution for that would be to restrict access to the class through a private method in your config class instead of an external utility class. That would mean moving SystemEnvironmentUtil#setEnv() to the class that you're invoking it from, alternatively making the utility class package private so that only your configuration classes can access it. In reality most the time the only thing that might need it is a configuration class, which is specific to your application, so it can be private. You can expose things like a String field that contains your path to assets, which your configuration utilizes to something like a Builder class, if you really need to add customizations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment