Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / appidentity / AppIdentityServiceImpl.java
blob4bcc2ec63a6e8954a7b4dcdc5d4f118742d9657b
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;
35 /**
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();
66 });
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() {
76 return semaphore;
79 public GetAccessTokenResult get() {
80 GetAccessTokenResult value = result.get();
81 if (value != null) {
82 Calendar cal = Calendar.getInstance();
83 cal.add(Calendar.MILLISECOND, INSTANCE_CACHE_EXPIRATION_DELTA_MILLIS);
84 if (cal.getTime().before(value.getExpirationTime())) {
85 return value;
88 return null;
91 public void set(GetAccessTokenResult value) {
92 result.set(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());
112 switch (errorCode) {
113 case BLOB_TOO_LARGE:
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.");
119 case UNKNOWN_ERROR:
120 throw new AppIdentityServiceFailureException(
121 "There was an unknown error using the AppIdentity service.");
122 case UNKNOWN_SCOPE:
123 throw new AppIdentityServiceFailureException("An unknown scope was supplied.");
124 default:
125 throw new AppIdentityServiceFailureException(
126 "The AppIdentity service threw an unexpected error. Details: " + e.getErrorDetail());
130 @Override
131 public List<PublicCertificate> getPublicCertificatesForApp() {
132 GetPublicCertificateForAppRequest.Builder requestBuilder =
133 GetPublicCertificateForAppRequest.newBuilder();
134 GetPublicCertificateForAppResponse.Builder responseBuilder =
135 GetPublicCertificateForAppResponse.newBuilder();
137 try {
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()));
152 return certs;
155 @Override
156 public SigningResult signForApp(byte[] signBlob) {
157 SignForAppRequest.Builder requestBuilder = SignForAppRequest.newBuilder();
158 requestBuilder.setBytesToSign(ByteString.copyFrom(signBlob));
159 SignForAppResponse.Builder responseBuilder = SignForAppResponse.newBuilder();
160 try {
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());
174 @Override
175 public String getServiceAccountName() {
176 GetServiceAccountNameRequest.Builder requestBuilder = GetServiceAccountNameRequest.newBuilder();
177 GetServiceAccountNameResponse.Builder responseBuilder =
178 GetServiceAccountNameResponse.newBuilder();
179 try {
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();
194 @Override
195 public String getDefaultGcsBucketName() {
196 GetDefaultGcsBucketNameRequest.Builder requestBuilder =
197 GetDefaultGcsBucketNameRequest.newBuilder();
198 GetDefaultGcsBucketNameResponse.Builder responseBuilder =
199 GetDefaultGcsBucketNameResponse.newBuilder();
200 try {
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();
214 } else {
215 throw new AppIdentityServiceFailureException(
216 "getDefaultGcsBucketNameResponse contained no data");
220 @Override
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();
230 try {
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);
256 builder.append('[');
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);
265 builder.append(']');
266 return builder.toString();
269 @Override
270 public GetAccessTokenResult getAccessToken(Iterable<String> scopes) {
271 String cacheKey = memcacheKeyForScopes(scopes);
272 CacheItem cacheItem = cache.getUnchecked(cacheKey);
274 try {
275 cacheItem.getAccessSemaphore().acquire();
276 } catch (InterruptedException e) {
277 Thread.currentThread().interrupt();
278 throw new AppIdentityServiceFailureException(e.getMessage());
281 try {
282 GetAccessTokenResult result = cacheItem.get();
283 if (result != null) {
284 return result;
286 result = (GetAccessTokenResult) memcacheService.get(cacheKey);
287 if (result != null) {
288 cacheItem.set(result);
289 return 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);
297 return result;
298 } finally {
299 cacheItem.getAccessSemaphore().release();
303 @Override
304 public ParsedAppId parseFullAppId(String fullAppId) {
305 int partitionIdx = fullAppId.indexOf(APP_PARTITION_SEPARATOR);
306 String partition;
307 if (partitionIdx > 0) {
308 partition = fullAppId.substring(0, partitionIdx);
309 fullAppId = fullAppId.substring(partitionIdx + 1);
310 } else {
311 partition = "";
314 int domainIdx = fullAppId.indexOf(APP_DOMAIN_SEPARATOR);
315 String domain;
316 if (domainIdx > 0) {
317 domain = fullAppId.substring(0, domainIdx);
318 fullAppId = fullAppId.substring(domainIdx + 1);
319 } else {
320 domain = "";
323 return new ParsedAppId(partition, domain, fullAppId);