Skip to content

Commit 76170e2

Browse files
committed
api: Application Default Credentials: Fix bug detecting GAE and improve error message.
1 parent a9b3f40 commit 76170e2

File tree

2 files changed

+135
-11
lines changed

2 files changed

+135
-11
lines changed

google-api-client/src/main/java/com/google/api/client/googleapis/auth/oauth2/DefaultCredentialProvider.java

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
import java.io.IOException;
3030
import java.io.InputStream;
3131
import java.lang.reflect.Constructor;
32+
import java.lang.reflect.Field;
3233
import java.lang.reflect.InvocationTargetException;
34+
import java.lang.reflect.Method;
3335
import java.security.AccessControlException;
3436
import java.util.Locale;
3537

@@ -206,27 +208,74 @@ Class<?> forName(String className) throws ClassNotFoundException {
206208
return Class.forName(className);
207209
}
208210

211+
private boolean runningOnAppEngine() {
212+
Class<?> systemPropertyClass = null;
213+
try {
214+
systemPropertyClass = forName("com.google.appengine.api.utils.SystemProperty");
215+
} catch (ClassNotFoundException expected) {
216+
// SystemProperty will always be present on App Engine.
217+
return false;
218+
}
219+
Exception cause = null;
220+
Field environmentField;
221+
try {
222+
environmentField = systemPropertyClass.getField("environment");
223+
Object environmentValue = environmentField.get(null);
224+
Class<?> environmentType = environmentField.getType();
225+
Method valueMethod = environmentType.getMethod("value");
226+
Object environmentValueValue = valueMethod.invoke(environmentValue);
227+
return (environmentValueValue != null);
228+
} catch (NoSuchFieldException exception) {
229+
cause = exception;
230+
} catch (SecurityException exception) {
231+
cause = exception;
232+
} catch (IllegalArgumentException exception) {
233+
cause = exception;
234+
} catch (IllegalAccessException exception) {
235+
cause = exception;
236+
} catch (NoSuchMethodException exception) {
237+
cause = exception;
238+
} catch (InvocationTargetException exception) {
239+
cause = exception;
240+
}
241+
throw OAuth2Utils.exceptionWithCause(new RuntimeException(String.format(
242+
"Unexpcted error trying to determine if runnning on Google App Engine: %s",
243+
cause.getMessage())), cause);
244+
}
245+
209246
private final GoogleCredential tryGetAppEngineCredential(
210-
HttpTransport transport, JsonFactory jsonFactory) {
247+
HttpTransport transport, JsonFactory jsonFactory) throws IOException {
211248
// Checking for App Engine requires a class load, so check only once
212249
if (checkedAppEngine) {
213250
return null;
214251
}
252+
boolean onAppEngine = runningOnAppEngine();
215253
checkedAppEngine = true;
216-
254+
if (!onAppEngine) {
255+
return null;
256+
}
257+
Exception innerException = null;
217258
try {
218259
Class<?> credentialClass = forName(APP_ENGINE_CREDENTIAL_CLASS);
219260
Constructor<?> constructor = credentialClass
220261
.getConstructor(HttpTransport.class, JsonFactory.class);
221262
return (GoogleCredential) constructor.newInstance(transport, jsonFactory);
222-
// Reflection expected to fail when not on App Engine
223-
} catch (ClassNotFoundException expected) {
224-
} catch (NoSuchMethodException expected) {
225-
} catch (InstantiationException expected) {
226-
} catch (IllegalAccessException expected) {
227-
} catch (InvocationTargetException expected) {
263+
} catch (ClassNotFoundException e) {
264+
innerException = e;
265+
} catch (NoSuchMethodException e) {
266+
innerException = e;
267+
} catch (InstantiationException e) {
268+
innerException = e;
269+
} catch (IllegalAccessException e) {
270+
innerException = e;
271+
} catch (InvocationTargetException e) {
272+
innerException = e;
228273
}
229-
return null;
274+
throw OAuth2Utils.exceptionWithCause(new IOException(String.format(
275+
"Application Default Credentials failed to create the Google App Engine service account"
276+
+ " credentials class %s. Check that the component 'google-api-client-appengine' is"
277+
+ " deployed.",
278+
APP_ENGINE_CREDENTIAL_CLASS)), innerException);
230279
}
231280

232281
private final GoogleCredential tryGetComputeCredential(

google-api-client/src/test/java/com/google/api/client/googleapis/auth/oauth2/DefaultCredentialProviderTest.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,18 @@ public class DefaultCredentialProviderTest extends TestCase {
6868
+ "==\n-----END PRIVATE KEY-----\n";
6969
private static final String ACCESS_TOKEN = "1/MkSJoj1xsli0AccessToken_NKPY2";
7070

71+
private static final String GAE_SIGNAL_CLASS = "com.google.appengine.api.utils.SystemProperty";
72+
7173
private static final Lock lock = new ReentrantLock();
7274

7375
private static File tempDirectory = null;
7476

75-
public void testDefaultCredentialAppEngine() throws IOException {
77+
public void testDefaultCredentialAppEngineDeployed() throws IOException {
7678
HttpTransport transport = new MockHttpTransport();
7779
TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider();
7880
testProvider.addType(DefaultCredentialProvider.APP_ENGINE_CREDENTIAL_CLASS,
7981
MockAppEngineCredential.class);
82+
testProvider.addType(GAE_SIGNAL_CLASS, MockAppEngineSystemProperty.class);
8083

8184
Credential defaultCredential = testProvider.getDefaultCredential(transport, JSON_FACTORY);
8285

@@ -86,7 +89,38 @@ public void testDefaultCredentialAppEngine() throws IOException {
8689
assertSame(JSON_FACTORY, defaultCredential.getJsonFactory());
8790
}
8891

89-
public void testDefaultCredentialAppEngineSingleAttempt() {
92+
public void testDefaultCredentialAppEngineComponentOffAppEngineGivesNotFoundError() {
93+
HttpTransport transport = new MockHttpTransport();
94+
TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider();
95+
testProvider.addType(DefaultCredentialProvider.APP_ENGINE_CREDENTIAL_CLASS,
96+
MockAppEngineCredential.class);
97+
testProvider.addType(GAE_SIGNAL_CLASS, MockOffAppEngineSystemProperty.class);
98+
99+
try {
100+
testProvider.getDefaultCredential(transport, JSON_FACTORY);
101+
fail("No credential expected when not on App Engine.");
102+
} catch (IOException e) {
103+
String message = e.getMessage();
104+
assertTrue(message.contains(DefaultCredentialProvider.HELP_PERMALINK));
105+
}
106+
}
107+
108+
public void testDefaultCredentialAppEngineWithoutDependencyThrowsHelpfulLoadError() {
109+
HttpTransport transport = new MockHttpTransport();
110+
TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider();
111+
testProvider.addType(GAE_SIGNAL_CLASS, MockAppEngineSystemProperty.class);
112+
113+
try {
114+
testProvider.getDefaultCredential(transport, JSON_FACTORY);
115+
fail("Credential expected to fail to load if credential class not present.");
116+
} catch (IOException e) {
117+
String message = e.getMessage();
118+
assertFalse(message.contains(DefaultCredentialProvider.HELP_PERMALINK));
119+
assertTrue(message.contains(DefaultCredentialProvider.APP_ENGINE_CREDENTIAL_CLASS));
120+
}
121+
}
122+
123+
public void testDefaultCredentialAppEngineSingleClassLoadAttempt() {
90124
HttpTransport transport = new MockHttpTransport();
91125
TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider();
92126
try {
@@ -109,6 +143,7 @@ public void testDefaultCredentialCaches() throws IOException {
109143
TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider();
110144
testProvider.addType(DefaultCredentialProvider.APP_ENGINE_CREDENTIAL_CLASS,
111145
MockAppEngineCredential.class);
146+
testProvider.addType(GAE_SIGNAL_CLASS, MockAppEngineSystemProperty.class);
112147

113148
Credential firstCall = testProvider.getDefaultCredential(transport, JSON_FACTORY);
114149

@@ -448,6 +483,46 @@ public MockAppEngineCredential(HttpTransport transport, JsonFactory jsonFactory)
448483
}
449484
}
450485

486+
/*
487+
* App Engine is detected by calling SystemProperty.environment.value() via Reflection.
488+
* The following mock types simulate the shape and behavior of that call sequence.
489+
*/
490+
491+
private static class MockAppEngineSystemProperty {
492+
493+
@SuppressWarnings("unused")
494+
public static final MockEnvironment environment =
495+
new MockEnvironment(MockEnvironmentEnum.Production);
496+
}
497+
498+
private static class MockOffAppEngineSystemProperty {
499+
500+
@SuppressWarnings("unused")
501+
public static final MockEnvironment environment = new MockEnvironment(null);
502+
}
503+
504+
private enum MockEnvironmentEnum {
505+
Production,
506+
Development;
507+
}
508+
509+
public static class MockEnvironment {
510+
511+
private MockEnvironmentEnum innerValue;
512+
513+
MockEnvironment(MockEnvironmentEnum value) {
514+
this.innerValue = value;
515+
}
516+
517+
public MockEnvironmentEnum value() {
518+
return innerValue;
519+
}
520+
}
521+
522+
/*
523+
* End of types simulating SystemProperty.environment.value()
524+
*/
525+
451526
private static class MockRequestCountingTransport extends MockHttpTransport {
452527
int requestCount = 0;
453528

0 commit comments

Comments
 (0)