/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.source.extractor.extract.google; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URI; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermissions; import java.security.GeneralSecurityException; import java.util.Collection; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.permission.FsAction; import org.apache.hadoop.fs.permission.FsPermission; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.api.client.auth.oauth2.Credential; import com.google.api.client.googleapis.GoogleUtils; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; /** * Utility class that has static methods for Google services. * */ public class GoogleCommon { private static final Logger LOG = LoggerFactory.getLogger(GoogleCommon.class); private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); private static final String JSON_FILE_EXTENSION = ".json"; private static final FsPermission USER_READ_PERMISSION_ONLY = new FsPermission(FsAction.READ, FsAction.NONE, FsAction.NONE); public static class CredentialBuilder { private final String privateKeyPath; private final Collection<String> serviceAccountScopes; private String fileSystemUri; private String serviceAccountId; private String proxyUrl; //Port as String type, so that client can easily pass null instead of checking the existence of it. //( e.g: state.getProp(key) vs state.contains(key) + state.getPropAsInt(key) ) private String portStr; public CredentialBuilder(String privateKeyPath, Collection<String> serviceAccountScopes) { Preconditions.checkArgument(!StringUtils.isEmpty(privateKeyPath), "privateKeyPath is required."); Preconditions.checkArgument(serviceAccountScopes != null && !serviceAccountScopes.isEmpty(), "serviceAccountScopes is required."); this.privateKeyPath = privateKeyPath; this.serviceAccountScopes = ImmutableList.copyOf(serviceAccountScopes); } public CredentialBuilder fileSystemUri(String fileSystemUri) { this.fileSystemUri = fileSystemUri; return this; } public CredentialBuilder serviceAccountId(String serviceAccountId) { this.serviceAccountId = serviceAccountId; return this; } public CredentialBuilder proxyUrl(String proxyUrl) { this.proxyUrl = proxyUrl; return this; } public CredentialBuilder port(int port) { this.portStr = Integer.toString(port); return this; } public CredentialBuilder port(String portStr) { this.portStr = portStr; return this; } public Credential build() { try { HttpTransport transport = newTransport(proxyUrl, portStr); if (privateKeyPath.trim().toLowerCase().endsWith(JSON_FILE_EXTENSION)) { LOG.info("Getting Google service account credential from JSON"); return buildCredentialFromJson(privateKeyPath, Optional.fromNullable(fileSystemUri), transport, serviceAccountScopes); } else { LOG.info("Getting Google service account credential from P12"); return buildCredentialFromP12(privateKeyPath, Optional.fromNullable(fileSystemUri), Optional.fromNullable(serviceAccountId), transport, serviceAccountScopes); } } catch (IOException | GeneralSecurityException e) { throw new RuntimeException("Failed to create credential", e); } } } /** * As Google API only accepts java.io.File for private key, and this method copies private key into local file system. * Once Google credential is instantiated, it deletes copied private key file. * * @param privateKeyPath * @param fsUri * @param id * @param transport * @param serviceAccountScopes * @return Credential * @throws IOException * @throws GeneralSecurityException */ private static Credential buildCredentialFromP12(String privateKeyPath, Optional<String> fsUri, Optional<String> id, HttpTransport transport, Collection<String> serviceAccountScopes) throws IOException, GeneralSecurityException { Preconditions.checkArgument(id.isPresent(), "user id is required."); FileSystem fs = getFileSystem(fsUri); Path keyPath = getPrivateKey(fs, privateKeyPath); final File localCopied = copyToLocal(fs, keyPath); localCopied.deleteOnExit(); try { return new GoogleCredential.Builder() .setTransport(transport) .setJsonFactory(JSON_FACTORY) .setServiceAccountId(id.get()) .setServiceAccountPrivateKeyFromP12File(localCopied) .setServiceAccountScopes(serviceAccountScopes) .build(); } finally { boolean isDeleted = localCopied.delete(); if (!isDeleted) { throw new RuntimeException(localCopied.getAbsolutePath() + " has not been deleted."); } } } /** * Before retrieving private key, it makes sure that original private key's permission is read only on the owner. * This is a way to ensure to keep private key private. * @param fs * @param privateKeyPath * @return * @throws IOException */ private static Path getPrivateKey(FileSystem fs, String privateKeyPath) throws IOException { Path keyPath = new Path(privateKeyPath); FileStatus fileStatus = fs.getFileStatus(keyPath); Preconditions.checkArgument(USER_READ_PERMISSION_ONLY.equals(fileStatus.getPermission()), "Private key file should only have read only permission only on user. " + keyPath); return keyPath; } private static FileSystem getFileSystem(Optional<String> fsUri) throws IOException { if (fsUri.isPresent()) { return FileSystem.get(URI.create(fsUri.get()), new Configuration()); } return FileSystem.get(new Configuration()); } private static Credential buildCredentialFromJson(String privateKeyPath, Optional<String> fsUri, HttpTransport transport, Collection<String> serviceAccountScopes) throws IOException { FileSystem fs = getFileSystem(fsUri); Path keyPath = getPrivateKey(fs, privateKeyPath); return GoogleCredential.fromStream(fs.open(keyPath), transport, JSON_FACTORY) .createScoped(serviceAccountScopes); } /** * Provides HttpTransport. If both proxyUrl and postStr is defined, it provides transport with Proxy. * @param proxyUrl Optional. * @param portStr Optional. String type for port so that user can easily pass null. (e.g: state.getProp(key)) * @return * @throws NumberFormatException * @throws GeneralSecurityException * @throws IOException */ public static HttpTransport newTransport(String proxyUrl, String portStr) throws NumberFormatException, GeneralSecurityException, IOException { if (!StringUtils.isEmpty(proxyUrl) && !StringUtils.isEmpty(portStr)) { return new NetHttpTransport.Builder() .trustCertificates(GoogleUtils.getCertificateTrustStore()) .setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyUrl, Integer.parseInt(portStr)))) .build(); } return GoogleNetHttpTransport.newTrustedTransport(); } private static File copyToLocal(FileSystem fs, Path keyPath) throws IOException { java.nio.file.Path tmpKeyPath = Files.createTempFile(GoogleCommon.class.getSimpleName(), "tmp", PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); File copied = tmpKeyPath.toFile(); copied.deleteOnExit(); fs.copyToLocalFile(keyPath, new Path(copied.getAbsolutePath())); return copied; } public static JsonFactory getJsonFactory() { return JSON_FACTORY; } }