1 // Copyright 2011 Google Inc. All Rights Reserved.
2 package com
.google
.appengine
.api
.appidentity
;
4 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.AppIdentityServiceError
;
5 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.GetAccessTokenRequest
;
6 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.GetAccessTokenResponse
;
7 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.GetDefaultGcsBucketNameRequest
;
8 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.GetDefaultGcsBucketNameResponse
;
9 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.GetPublicCertificateForAppRequest
;
10 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.GetPublicCertificateForAppResponse
;
11 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.GetServiceAccountNameRequest
;
12 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.GetServiceAccountNameResponse
;
13 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.SignForAppRequest
;
14 import com
.google
.appengine
.api
.appidentity
.AppIdentityServicePb
.SignForAppResponse
;
15 import com
.google
.appengine
.api
.memcache
.Expiration
;
16 import com
.google
.appengine
.api
.memcache
.MemcacheService
;
17 import com
.google
.appengine
.api
.memcache
.MemcacheServiceFactory
;
18 import com
.google
.appengine
.api
.utils
.SystemProperty
;
19 import com
.google
.apphosting
.api
.ApiProxy
;
20 import com
.google
.common
.cache
.CacheBuilder
;
21 import com
.google
.common
.cache
.CacheLoader
;
22 import com
.google
.common
.cache
.LoadingCache
;
23 import com
.google
.common
.collect
.Iterables
;
24 import com
.google
.common
.collect
.Lists
;
25 import com
.google
.protobuf
.ByteString
;
26 import com
.google
.protobuf
.InvalidProtocolBufferException
;
28 import java
.util
.Calendar
;
29 import java
.util
.Date
;
30 import java
.util
.List
;
31 import java
.util
.Random
;
32 import java
.util
.concurrent
.Semaphore
;
33 import java
.util
.concurrent
.atomic
.AtomicReference
;
36 * Implementation of the AppIdentityService interface.
39 class AppIdentityServiceImpl
implements AppIdentityService
{
41 public static final String PACKAGE_NAME
= "app_identity_service";
42 public static final String SIGN_FOR_APP_METHOD_NAME
= "SignForApp";
43 public static final String GET_SERVICE_ACCOUNT_NAME_METHOD_NAME
= "GetServiceAccountName";
44 public static final String GET_DEFAULT_GCS_BUCKET_NAME
= "GetDefaultGcsBucketName";
45 public static final String GET_CERTS_METHOD_NAME
= "GetPublicCertificatesForApp";
46 public static final String GET_ACCESS_TOKEN_METHOD_NAME
= "GetAccessToken";
47 public static final String MEMCACHE_NAMESPACE
= "_ah_";
48 public static final String MEMCACHE_KEY_PREFIX
= "_ah_app_identity_";
50 private static final char APP_PARTITION_SEPARATOR
= '~';
51 private static final char APP_DOMAIN_SEPARATOR
= ':';
52 private static final int TOKEN_EXPIRY_SAFETY_MARGIN_MILLIS
= 300000;
53 private static final int MAX_RANDOM_EXPIRY_DELTA_MILLIS
= 60000;
54 private static final int MEMCACHE_EXPIRATION_DELTA_MILLIS
=
55 TOKEN_EXPIRY_SAFETY_MARGIN_MILLIS
+ MAX_RANDOM_EXPIRY_DELTA_MILLIS
;
56 private static final int INSTANCE_CACHE_EXPIRATION_DELTA_MILLIS
=
57 TOKEN_EXPIRY_SAFETY_MARGIN_MILLIS
+ new Random().nextInt(MAX_RANDOM_EXPIRY_DELTA_MILLIS
);
58 private static final int MAX_INSTANCE_CACHE_ENTRIES
= 100;
59 private static final int MAX_CONCURRENT_LOAD_PER_KEY
= 3;
60 private static LoadingCache
<String
, CacheItem
> cache
= CacheBuilder
.newBuilder()
61 .maximumSize(MAX_INSTANCE_CACHE_ENTRIES
)
62 .build(new CacheLoader
<String
, CacheItem
>() {
63 @Override public CacheItem
load(String key
) {
64 return new CacheItem();
68 private final MemcacheService memcacheService
;
70 private static class CacheItem
{
72 private final Semaphore semaphore
= new Semaphore(MAX_CONCURRENT_LOAD_PER_KEY
);
73 private final AtomicReference
<GetAccessTokenResult
> result
= new AtomicReference
<>();
75 public Semaphore
getAccessSemaphore() {
79 public GetAccessTokenResult
get() {
80 GetAccessTokenResult value
= result
.get();
82 Calendar cal
= Calendar
.getInstance();
83 cal
.add(Calendar
.MILLISECOND
, INSTANCE_CACHE_EXPIRATION_DELTA_MILLIS
);
84 if (cal
.getTime().before(value
.getExpirationTime())) {
91 public void set(GetAccessTokenResult value
) {
96 AppIdentityServiceImpl() {
97 memcacheService
= MemcacheServiceFactory
.getMemcacheService(MEMCACHE_NAMESPACE
);
100 static void clearCache() {
101 cache
.invalidateAll();
104 private void handleApplicationError(ApiProxy
.ApplicationException e
) {
105 AppIdentityServiceError
.ErrorCode errorCode
=
106 AppIdentityServiceError
.ErrorCode
.valueOf(e
.getApplicationError());
107 if (errorCode
== null) {
108 throw new AppIdentityServiceFailureException(
109 "The AppIdentity service threw an unexpected error. Details: " + e
.getErrorDetail());
114 throw new AppIdentityServiceFailureException("The supplied blob was too long.");
115 case NOT_A_VALID_APP
:
116 throw new AppIdentityServiceFailureException("The application is not valid.");
117 case DEADLINE_EXCEEDED
:
118 throw new AppIdentityServiceFailureException("The deadline for the call was exceeded.");
120 throw new AppIdentityServiceFailureException(
121 "There was an unknown error using the AppIdentity service.");
123 throw new AppIdentityServiceFailureException("An unknown scope was supplied.");
125 throw new AppIdentityServiceFailureException(
126 "The AppIdentity service threw an unexpected error. Details: " + e
.getErrorDetail());
131 public List
<PublicCertificate
> getPublicCertificatesForApp() {
132 GetPublicCertificateForAppRequest
.Builder requestBuilder
=
133 GetPublicCertificateForAppRequest
.newBuilder();
134 GetPublicCertificateForAppResponse
.Builder responseBuilder
=
135 GetPublicCertificateForAppResponse
.newBuilder();
138 responseBuilder
.mergeFrom(
139 ApiProxy
.makeSyncCall(
140 PACKAGE_NAME
, GET_CERTS_METHOD_NAME
, requestBuilder
.build().toByteArray()));
141 } catch (ApiProxy
.ApplicationException e
) {
142 handleApplicationError(e
);
143 } catch (InvalidProtocolBufferException e
) {
144 throw new AppIdentityServiceFailureException(e
.getMessage());
146 GetPublicCertificateForAppResponse response
= responseBuilder
.build();
148 List
<PublicCertificate
> certs
= Lists
.newArrayList();
149 for (AppIdentityServicePb
.PublicCertificate cert
: response
.getPublicCertificateListList()) {
150 certs
.add(new PublicCertificate(cert
.getKeyName(), cert
.getX509CertificatePem()));
156 public SigningResult
signForApp(byte[] signBlob
) {
157 SignForAppRequest
.Builder requestBuilder
= SignForAppRequest
.newBuilder();
158 requestBuilder
.setBytesToSign(ByteString
.copyFrom(signBlob
));
159 SignForAppResponse
.Builder responseBuilder
= SignForAppResponse
.newBuilder();
161 responseBuilder
.mergeFrom(
162 ApiProxy
.makeSyncCall(
163 PACKAGE_NAME
, SIGN_FOR_APP_METHOD_NAME
, requestBuilder
.build().toByteArray()));
164 } catch (ApiProxy
.ApplicationException e
) {
165 handleApplicationError(e
);
166 } catch (InvalidProtocolBufferException e
) {
167 throw new AppIdentityServiceFailureException(e
.getMessage());
170 SignForAppResponse response
= responseBuilder
.build();
171 return new SigningResult(response
.getKeyName(), response
.getSignatureBytes().toByteArray());
175 public String
getServiceAccountName() {
176 GetServiceAccountNameRequest
.Builder requestBuilder
= GetServiceAccountNameRequest
.newBuilder();
177 GetServiceAccountNameResponse
.Builder responseBuilder
=
178 GetServiceAccountNameResponse
.newBuilder();
180 responseBuilder
.mergeFrom(
181 ApiProxy
.makeSyncCall(
182 getAccessTokenPackageName(), GET_SERVICE_ACCOUNT_NAME_METHOD_NAME
,
183 requestBuilder
.build().toByteArray()));
184 } catch (ApiProxy
.ApplicationException e
) {
185 handleApplicationError(e
);
186 } catch (InvalidProtocolBufferException e
) {
187 throw new AppIdentityServiceFailureException(e
.getMessage());
190 GetServiceAccountNameResponse response
= responseBuilder
.build();
191 return response
.getServiceAccountName();
195 public String
getDefaultGcsBucketName() {
196 GetDefaultGcsBucketNameRequest
.Builder requestBuilder
=
197 GetDefaultGcsBucketNameRequest
.newBuilder();
198 GetDefaultGcsBucketNameResponse
.Builder responseBuilder
=
199 GetDefaultGcsBucketNameResponse
.newBuilder();
201 responseBuilder
.mergeFrom(
202 ApiProxy
.makeSyncCall(
203 PACKAGE_NAME
, GET_DEFAULT_GCS_BUCKET_NAME
,
204 requestBuilder
.build().toByteArray()));
205 } catch (ApiProxy
.ApplicationException e
) {
206 handleApplicationError(e
);
207 } catch (InvalidProtocolBufferException e
) {
208 throw new AppIdentityServiceFailureException(e
.getMessage());
211 GetDefaultGcsBucketNameResponse response
= responseBuilder
.build();
212 if (response
.hasDefaultGcsBucketName()) {
213 return response
.getDefaultGcsBucketName();
215 throw new AppIdentityServiceFailureException(
216 "getDefaultGcsBucketNameResponse contained no data");
221 public GetAccessTokenResult
getAccessTokenUncached(Iterable
<String
> scopes
) {
222 GetAccessTokenRequest
.Builder requestBuilder
= GetAccessTokenRequest
.newBuilder();
223 for (String scope
: scopes
) {
224 requestBuilder
.addScope(scope
);
226 if (requestBuilder
.getScopeCount() == 0) {
227 throw new AppIdentityServiceFailureException("No scopes specified.");
229 GetAccessTokenResponse
.Builder responseBuilder
= GetAccessTokenResponse
.newBuilder();
231 responseBuilder
.mergeFrom(
232 ApiProxy
.makeSyncCall(
233 getAccessTokenPackageName(), GET_ACCESS_TOKEN_METHOD_NAME
,
234 requestBuilder
.build().toByteArray()));
235 } catch (ApiProxy
.ApplicationException e
) {
236 handleApplicationError(e
);
237 } catch (InvalidProtocolBufferException e
) {
238 throw new AppIdentityServiceFailureException(e
.getMessage());
241 GetAccessTokenResponse response
= responseBuilder
.build();
242 return new GetAccessTokenResult(response
.getAccessToken(),
243 new Date(response
.getExpirationTime() * 1000));
246 private String
getAccessTokenPackageName() {
247 return Boolean
.getBoolean("appengine.app_identity.use_robot")
248 && SystemProperty
.environment
.value() != SystemProperty
.Environment
.Value
.Production
249 ?
"robot_enabled_app_identity_service"
250 : "app_identity_service";
253 String
memcacheKeyForScopes(Iterable
<String
> scopes
) {
254 StringBuilder builder
= new StringBuilder();
255 builder
.append(MEMCACHE_KEY_PREFIX
);
257 if (!Iterables
.isEmpty(scopes
)) {
258 for (String scope
: scopes
) {
259 builder
.append('\'');
260 builder
.append(scope
);
261 builder
.append("',");
263 builder
.setLength(builder
.length() - 1);
266 return builder
.toString();
270 public GetAccessTokenResult
getAccessToken(Iterable
<String
> scopes
) {
271 String cacheKey
= memcacheKeyForScopes(scopes
);
272 CacheItem cacheItem
= cache
.getUnchecked(cacheKey
);
275 cacheItem
.getAccessSemaphore().acquire();
276 } catch (InterruptedException e
) {
277 Thread
.currentThread().interrupt();
278 throw new AppIdentityServiceFailureException(e
.getMessage());
282 GetAccessTokenResult result
= cacheItem
.get();
283 if (result
!= null) {
286 result
= (GetAccessTokenResult
) memcacheService
.get(cacheKey
);
287 if (result
!= null) {
288 cacheItem
.set(result
);
291 result
= getAccessTokenUncached(scopes
);
292 Calendar cal
= Calendar
.getInstance();
293 cal
.setTime(result
.getExpirationTime());
294 cal
.add(Calendar
.MILLISECOND
, -1 * MEMCACHE_EXPIRATION_DELTA_MILLIS
);
295 memcacheService
.put(cacheKey
, result
, Expiration
.onDate(cal
.getTime()));
296 cacheItem
.set(result
);
299 cacheItem
.getAccessSemaphore().release();
304 public ParsedAppId
parseFullAppId(String fullAppId
) {
305 int partitionIdx
= fullAppId
.indexOf(APP_PARTITION_SEPARATOR
);
307 if (partitionIdx
> 0) {
308 partition
= fullAppId
.substring(0, partitionIdx
);
309 fullAppId
= fullAppId
.substring(partitionIdx
+ 1);
314 int domainIdx
= fullAppId
.indexOf(APP_DOMAIN_SEPARATOR
);
317 domain
= fullAppId
.substring(0, domainIdx
);
318 fullAppId
= fullAppId
.substring(domainIdx
+ 1);
323 return new ParsedAppId(partition
, domain
, fullAppId
);