All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ctoolkit.restapi.client.adapter.GoogleApiProxyFactory Maven / Gradle / Ivy

/*
 * Copyright (c) 2019 Comvai, s.r.o. All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.ctoolkit.restapi.client.adapter;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpExecuteInterceptor;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponseInterceptor;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.PemReader;
import com.google.api.client.util.SecurityUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.eventbus.EventBus;
import org.ctoolkit.restapi.client.ApiCredential;
import org.ctoolkit.restapi.client.provider.AuthKeyProvider;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.ctoolkit.restapi.client.ApiCredential.CREDENTIAL_ATTR;
import static org.ctoolkit.restapi.client.ApiCredential.DEFAULT_CREDENTIAL_PREFIX;
import static org.ctoolkit.restapi.client.ApiCredential.DEFAULT_NUMBER_OF_RETRIES;
import static org.ctoolkit.restapi.client.ApiCredential.DEFAULT_READ_TIMEOUT;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_API_KEY;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_APPLICATION_NAME;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_CLIENT_ID;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_CREDENTIAL_ON;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_DISABLE_GZIP_CONTENT;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_ENDPOINT_URL;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_FILE_NAME;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_FILE_NAME_JSON;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_NUMBER_OF_RETRIES;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_PROJECT_ID;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_READ_TIMEOUT;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_SCOPES;
import static org.ctoolkit.restapi.client.ApiCredential.PROPERTY_SERVICE_ACCOUNT_EMAIL;

/**
 * The factory to build proxy instance to allow authenticated calls to Google APIs on behalf of the application
 * instead of an end-user by OAuth 2.0.
 * 

* OAuth 2.0 allows users to share specific data with application (for example, contact lists) * while keeping their usernames, passwords, and other information private. * * @author Aurel Medvegy */ public abstract class GoogleApiProxyFactory { protected final EventBus eventBus; private final Map credential; private HttpTransport httpTransport; private JsonFactory jsonFactory; /** * Optional authentication key provider (client responsibility). */ private AuthKeyProvider keyProvider; /** * Create factory instance. */ protected GoogleApiProxyFactory( @Nonnull Map credential, @Nonnull EventBus eventBus ) { this.credential = credential; this.eventBus = eventBus; } protected void setKeyProvider( AuthKeyProvider keyProvider ) { this.keyProvider = keyProvider; } /** * Returns singleton instance of the HTTP transport. * * @return the reusable {@link HttpTransport} instance */ public final HttpTransport getHttpTransport() throws GeneralSecurityException, IOException { if ( httpTransport == null ) { httpTransport = GoogleNetHttpTransport.newTrustedTransport(); } return httpTransport; } /** * Returns singleton instance of the JSON factory. * * @return the reusable {@link JsonFactory} instance */ public final JsonFactory getJsonFactory() { if ( jsonFactory == null ) { jsonFactory = JacksonFactory.getDefaultInstance(); } return jsonFactory; } /** * Returns value set by {@link ApiCredential#setProjectId(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the project ID * @throws MissingResourceException if default credential was requested and haven't been found */ public String getProjectId( @Nullable String prefix ) { return getStringValue( prefix, PROPERTY_PROJECT_ID ); } /** * Returns value set by {@link ApiCredential#setClientId(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the client ID * @throws MissingResourceException if default credential was requested and haven't been found */ public String getClientId( @Nullable String prefix ) { return getStringValue( prefix, PROPERTY_CLIENT_ID ); } /** * Returns value set by {@link ApiCredential#setDisableGZipContent(boolean)} (boolean)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return true to disable GZip compression. Otherwise HTTP content will be compressed. */ public final boolean isDisableGZipContent( @Nullable String prefix ) { return getBoolean( PROPERTY_DISABLE_GZIP_CONTENT, prefix ); } /** * Returns value set by {@link ApiCredential#setScopes(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the unmodifiable list of API scopes * @throws MissingResourceException if default credential was requested and haven't been found */ public List getScopes( @Nullable String prefix ) { String values = getStringValue( prefix, PROPERTY_SCOPES ); if ( values == null ) { return Collections.emptyList(); } List list = Splitter.on( "," ).trimResults().omitEmptyStrings().splitToList( values ); return Collections.unmodifiableList( list ); } /** * Returns value set by {@link ApiCredential#setServiceAccountEmail(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the service email * @throws MissingResourceException if default credential was requested and haven't been found */ public final String getServiceAccountEmail( @Nullable String prefix ) { return getStringValue( prefix, PROPERTY_SERVICE_ACCOUNT_EMAIL ); } /** * Returns value set by {@link ApiCredential#setApplicationName(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the application name * @throws MissingResourceException if default credential was requested and haven't been found */ public final String getApplicationName( @Nullable String prefix ) { return getStringValue( prefix, PROPERTY_APPLICATION_NAME ); } /** * Returns value set by {@link ApiCredential#setFileName(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the file name path * @throws MissingResourceException if default credential was requested and haven't been found */ public final String getFileName( @Nullable String prefix ) { return getStringValue( prefix, PROPERTY_FILE_NAME ); } /** * Returns value set by {@link ApiCredential#setFileNameJson(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the file name path * @throws MissingResourceException if default credential was requested and haven't been found */ public final String getFileNameJson( @Nullable String prefix ) { return getStringValue( prefix, PROPERTY_FILE_NAME_JSON ); } /** * Returns value set by {@link ApiCredential#setApiKey(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the API key * @throws MissingResourceException if default credential was requested and haven't been found */ public final String getApiKey( @Nullable String prefix ) { return getStringValue( prefix, PROPERTY_API_KEY ); } /** * Returns value set by {@link ApiCredential#setEndpointUrl(String)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the endpoint URL * @throws MissingResourceException if default credential was requested and haven't been found */ public final String getEndpointUrl( @Nullable String prefix ) { return getStringValue( prefix, PROPERTY_ENDPOINT_URL ); } /** * Returns value set by {@link ApiCredential#setNumberOfRetries(int)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the number of configured retries */ public int getNumberOfRetries( @Nullable String prefix ) { return getInteger( PROPERTY_NUMBER_OF_RETRIES, DEFAULT_NUMBER_OF_RETRIES, prefix ); } /** * Returns value set by {@link ApiCredential#setRequestReadTimeout(int)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned 20000 (20 seconds). * * @param prefix the prefix used to identify specific credential or null for default * @return the request read timeout in milliseconds */ public int getReadTimeout( @Nullable String prefix ) { return getInteger( PROPERTY_READ_TIMEOUT, DEFAULT_READ_TIMEOUT, prefix ); } /** * Returns the integer value for given property. * * @param property the name of the property to retrieve * @param defaultValue the default value if no value will be found * @param prefix the prefix used to identify specific credential or null for default * @return the integer value */ private int getInteger( @Nonnull String property, @Nonnull String defaultValue, @Nullable String prefix ) { if ( Strings.isNullOrEmpty( prefix ) ) { prefix = DEFAULT_CREDENTIAL_PREFIX; } String errorMessage = "Property name is expected"; String value = credential.get( CREDENTIAL_ATTR + prefix + "." + checkNotNull( property, errorMessage ) ); if ( value == null ) { value = credential.get( CREDENTIAL_ATTR + DEFAULT_CREDENTIAL_PREFIX + "." + checkNotNull( property ) ); } if ( value == null ) { value = checkNotNull( defaultValue, "Default value is expected" ); } return Integer.parseInt( value ); } /** * Returns value set by {@link ApiCredential#setCredentialOn(boolean)} * or defined by property file. * If specific credential wouldn't not be found, default will be returned. * * @param prefix the prefix used to identify specific credential or null for default * @return the true if credential will be used to authenticate */ public final boolean isCredentialOn( @Nullable String prefix ) { return getBoolean( PROPERTY_CREDENTIAL_ON, prefix ); } /** * Returns the boolean value for given property. * * @param property the name of the property to retrieve * @param prefix the prefix used to identify specific credential or null for default * @return the boolean value */ private boolean getBoolean( @Nonnull String property, @Nullable String prefix ) { if ( Strings.isNullOrEmpty( prefix ) ) { prefix = DEFAULT_CREDENTIAL_PREFIX; } String fullProperty = CREDENTIAL_ATTR + prefix + "." + property; String value = credential.get( fullProperty ); if ( value == null ) { fullProperty = CREDENTIAL_ATTR + DEFAULT_CREDENTIAL_PREFIX + "." + property; value = credential.get( fullProperty ); } if ( value == null ) { return false; } return Boolean.parseBoolean( value ); } private String getStringValue( String prefix, String property ) { checkArgument( !Strings.isNullOrEmpty( property ) ); if ( Strings.isNullOrEmpty( prefix ) ) { prefix = DEFAULT_CREDENTIAL_PREFIX; } String value = credential.get( CREDENTIAL_ATTR + prefix + "." + property ); if ( value == null ) { value = credential.get( CREDENTIAL_ATTR + DEFAULT_CREDENTIAL_PREFIX + "." + property ); } defaultPropertyValueCheck( prefix, property, value ); return value; } private void defaultPropertyValueCheck( String prefix, String property, String value ) { if ( value != null || !DEFAULT_CREDENTIAL_PREFIX.equals( prefix ) ) { return; } String fullProperty = CREDENTIAL_ATTR + DEFAULT_CREDENTIAL_PREFIX + "." + property; String className = ApiCredential.class.getName(); String message = "No value configured for default credential: '" + fullProperty + "'"; throw new MissingResourceException( message, className, fullProperty ); } /** * Creates the thread-safe Google-specific implementation of the OAuth 2.0. * * @param scopes the space-separated OAuth scopes to use with the service account flow * or {@code null} for none. * @param userAccount the email address. If you want to impersonate a user account, specify the email address. * Useful for domain-wide delegation. * @return the thread-safe credential instance */ public HttpRequestInitializer authorize( @Nonnull Collection scopes, @Nullable String userAccount, @Nonnull String prefix ) throws GeneralSecurityException, IOException { GoogleCredential googleCredential; Collection checkedScopes = checkNotNull( scopes, "Scopes is mandatory" ); if ( isJsonConfiguration( checkNotNull( prefix, "API short name is mandatory" ) ) ) { // json file load right before usage InputStream json = getServiceAccountJson( prefix ); googleCredential = new ConfiguredByJsonGoogleCredential( json, prefix ) .setTransport( getHttpTransport() ) .setJsonFactory( getJsonFactory() ) .setServiceAccountScopes( checkedScopes ) .setServiceAccountUser( userAccount ) .build(); } else { String serviceAccountEmail = getServiceAccountEmail( prefix ); if ( serviceAccountEmail == null ) { throw new NullPointerException( "Missing service account email." ); } // p12 file load right before usage URL resource = getServiceAccountPrivateKeyP12Resource( prefix ); googleCredential = new ConfiguredGoogleCredential( prefix ) .setTransport( getHttpTransport() ) .setJsonFactory( getJsonFactory() ) .setServiceAccountId( serviceAccountEmail ) .setServiceAccountScopes( checkedScopes ) .setServiceAccountPrivateKeyFromP12File( new File( resource.getPath() ) ) .setServiceAccountUser( userAccount ) .build(); } return googleCredential; } public boolean isJsonConfiguration( String prefix ) { if ( keyProvider != null && keyProvider.isConfigured( prefix ) ) { // if defined authentication key takes precedence return true; } try { return getFileNameJson( prefix ) != null; } catch ( MissingResourceException e ) { return false; } } public URL getServiceAccountPrivateKeyP12Resource( String prefix ) { String fileName = getFileName( prefix ); return GoogleApiProxyFactory.class.getResource( fileName ); } /** * Returns the Google APIs service account key as JSON. * * @param prefix the prefix used to identify specific credential * @return the service account key as JSON */ public InputStream getServiceAccountJson( String prefix ) { InputStream stream; if ( keyProvider != null && keyProvider.isConfigured( prefix ) ) { stream = keyProvider.get( prefix ); } else { String fileName = getFileNameJson( prefix ); stream = GoogleApiProxyFactory.class.getResourceAsStream( fileName ); if ( stream == null ) { throw new IllegalArgumentException( "No file has been found with name '" + fileName + "'" ); } } return stream; } public HttpRequestInitializer newRequestConfig( @Nullable String prefix, @Nullable HttpResponseInterceptor interceptor ) { return new RequestConfig( prefix, interceptor ); } private PrivateKey privateKeyFromPkcs8( String privateKeyPem ) throws IOException { Reader reader = new StringReader( privateKeyPem ); PemReader.Section section = PemReader.readFirstSectionAndClose( reader, "PRIVATE KEY" ); if ( section == null ) { throw new IOException( "Invalid PKCS8 data." ); } byte[] bytes = section.getBase64DecodedBytes(); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec( bytes ); try { KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory(); return keyFactory.generatePrivate( keySpec ); } catch ( NoSuchAlgorithmException | InvalidKeySpecException e ) { throw new IOException( "Unexpected exception reading PKCS data", e ); } } /** * Configure HTTP request right before execution. * * @param request the HTTP request * @param numberOfRetries the number of configured retries * @param readTimeout the request read timeout in milliseconds */ protected final void configureHttpRequest( @Nonnull HttpRequest request, int numberOfRetries, int readTimeout ) { request.setNumberOfRetries( numberOfRetries ); request.setReadTimeout( readTimeout ); } private class RequestConfig implements HttpRequestInitializer { private final int numberOfRetries; private final int readTimeout; private final HttpResponseInterceptor responseInterceptor; HttpExecuteInterceptor interceptor = new HttpExecuteInterceptor() { @Override public void intercept( HttpRequest request ) { eventBus.post( new BeforeRequestEvent( request ) ); } }; private RequestConfig( String prefix, HttpResponseInterceptor responseInterceptor ) { this.numberOfRetries = getNumberOfRetries( prefix ); this.readTimeout = getReadTimeout( prefix ); this.responseInterceptor = responseInterceptor; } public void initialize( HttpRequest request ) { configureHttpRequest( request, numberOfRetries, readTimeout ); request.setInterceptor( interceptor ); if ( responseInterceptor != null ) { request.setResponseInterceptor( responseInterceptor ); } } } /** * Custom GoogleCredential implementation */ private class ConfiguredGoogleCredential extends GoogleCredential.Builder { private final int numberOfRetries; private final int readTimeout; private ConfiguredGoogleCredential( String prefix ) { this.numberOfRetries = getNumberOfRetries( prefix ); this.readTimeout = getReadTimeout( prefix ); } @Override public GoogleCredential build() { return new GoogleCredential( this ) { @Override public void intercept( HttpRequest request ) throws IOException { String authorization = request.getHeaders().getAuthorization(); // the authorization header set by facade client has a preference, see Request#authBy(String) if ( authorization == null ) { super.intercept( request ); } eventBus.post( new BeforeRequestEvent( request ) ); } @Override public void initialize( HttpRequest request ) throws IOException { super.initialize( request ); configureHttpRequest( request, numberOfRetries, readTimeout ); } }; } } private class ConfiguredByJsonGoogleCredential extends ConfiguredGoogleCredential { ConfiguredByJsonGoogleCredential( InputStream jsonStream, String prefix ) throws IOException { super( prefix ); JsonObjectParser parser = new JsonObjectParser( GoogleApiProxyFactory.this.getJsonFactory() ); GenericJson fileContents = parser.parseAndClose( jsonStream, Charsets.UTF_8, GenericJson.class ); String clientId = ( String ) fileContents.get( "client_id" ); String clientEmail = ( String ) fileContents.get( "client_email" ); String privateKeyPem = ( String ) fileContents.get( "private_key" ); String privateKeyId = ( String ) fileContents.get( "private_key_id" ); if ( clientId == null || clientEmail == null || privateKeyPem == null || privateKeyId == null ) { throw new IOException( "Error reading service account credential from stream, " + "expecting 'client_id', 'client_email', 'private_key' and 'private_key_id'." ); } PrivateKey privateKey = privateKeyFromPkcs8( privateKeyPem ); // setup credential from json setServiceAccountId( clientEmail ); setServiceAccountPrivateKey( privateKey ); setServiceAccountPrivateKeyId( privateKeyId ); String tokenUri = ( String ) fileContents.get( "token_uri" ); if ( tokenUri != null ) { setTokenServerEncodedUrl( tokenUri ); } String projectId = ( String ) fileContents.get( "project_id" ); if ( projectId != null ) { setServiceAccountProjectId( projectId ); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy