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

com.google.appengine.api.appidentity.AppIdentityServiceImpl Maven / Gradle / Ivy

Go to download

API for Google App Engine standard environment with some of the dependencies shaded (repackaged)

There is a newer version: 2.0.27
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * Licensed 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
 *
 *     https://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 com.google.appengine.api.appidentity;

import com.google.appengine.api.appidentity.AppIdentityServicePb.AppIdentityServiceError;
import com.google.appengine.api.appidentity.AppIdentityServicePb.GetAccessTokenRequest;
import com.google.appengine.api.appidentity.AppIdentityServicePb.GetAccessTokenResponse;
import com.google.appengine.api.appidentity.AppIdentityServicePb.GetDefaultGcsBucketNameRequest;
import com.google.appengine.api.appidentity.AppIdentityServicePb.GetDefaultGcsBucketNameResponse;
import com.google.appengine.api.appidentity.AppIdentityServicePb.GetPublicCertificateForAppRequest;
import com.google.appengine.api.appidentity.AppIdentityServicePb.GetPublicCertificateForAppResponse;
import com.google.appengine.api.appidentity.AppIdentityServicePb.GetServiceAccountNameRequest;
import com.google.appengine.api.appidentity.AppIdentityServicePb.GetServiceAccountNameResponse;
import com.google.appengine.api.appidentity.AppIdentityServicePb.SignForAppRequest;
import com.google.appengine.api.appidentity.AppIdentityServicePb.SignForAppResponse;
import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceException;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.utils.SystemProperty;
import com.google.apphosting.api.ApiProxy;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.qual.Nullable;

/** Implementation of the AppIdentityService interface. */
class AppIdentityServiceImpl implements AppIdentityService {

  public static final String PACKAGE_NAME = "app_identity_service";
  public static final String SIGN_FOR_APP_METHOD_NAME = "SignForApp";
  public static final String GET_SERVICE_ACCOUNT_NAME_METHOD_NAME = "GetServiceAccountName";
  public static final String GET_DEFAULT_GCS_BUCKET_NAME = "GetDefaultGcsBucketName";
  public static final String GET_CERTS_METHOD_NAME = "GetPublicCertificatesForApp";
  public static final String GET_ACCESS_TOKEN_METHOD_NAME = "GetAccessToken";
  public static final String MEMCACHE_NAMESPACE = "_ah_";
  public static final String MEMCACHE_KEY_PREFIX = "_ah_app_identity_";

  private static final char APP_PARTITION_SEPARATOR = '~';
  private static final char APP_DOMAIN_SEPARATOR = ':';
  private static final int TOKEN_EXPIRY_SAFETY_MARGIN_MILLIS = 300000;
  private static final int MAX_RANDOM_EXPIRY_DELTA_MILLIS = 60000;
  private static final int MEMCACHE_EXPIRATION_DELTA_MILLIS =
      TOKEN_EXPIRY_SAFETY_MARGIN_MILLIS + MAX_RANDOM_EXPIRY_DELTA_MILLIS;
  private static final int INSTANCE_CACHE_EXPIRATION_DELTA_MILLIS =
      TOKEN_EXPIRY_SAFETY_MARGIN_MILLIS + new Random().nextInt(MAX_RANDOM_EXPIRY_DELTA_MILLIS);
  private static final int MAX_INSTANCE_CACHE_ENTRIES = 100;
  private static final int MAX_CONCURRENT_LOAD_PER_KEY = 3;
  private static LoadingCache cache =
      CacheBuilder.newBuilder()
          .maximumSize(MAX_INSTANCE_CACHE_ENTRIES)
          .build(
              new CacheLoader() {
                @Override
                public CacheItem load(String key) {
                  return new CacheItem();
                }
              });

  private final MemcacheService memcacheService;

  private static class CacheItem {

    private final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_LOAD_PER_KEY);
    private final AtomicReference result = new AtomicReference<>();

    public Semaphore getAccessSemaphore() {
      return semaphore;
    }

    public @Nullable GetAccessTokenResult get() {
      GetAccessTokenResult value = result.get();
      if (value != null) {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.MILLISECOND, INSTANCE_CACHE_EXPIRATION_DELTA_MILLIS);
        if (cal.getTime().before(value.getExpirationTime())) {
          return value;
        }
      }
      return null;
    }

    public void set(GetAccessTokenResult value) {
      result.set(value);
    }
  }

  AppIdentityServiceImpl() {
    memcacheService = MemcacheServiceFactory.getMemcacheService(MEMCACHE_NAMESPACE);
  }

  // Used for testing
  static void clearCache() {
    cache.invalidateAll();
  }

  private void handleApplicationError(ApiProxy.ApplicationException e) {
    AppIdentityServiceError.ErrorCode errorCode =
        AppIdentityServiceError.ErrorCode.forNumber(e.getApplicationError());
    if (errorCode == null) {
      throw new AppIdentityServiceFailureException(
          "The AppIdentity service threw an unexpected error. Details: " + e.getErrorDetail());
    }

    switch (errorCode) {
      case BLOB_TOO_LARGE:
        throw new AppIdentityServiceFailureException("The supplied blob was too long.");
      case NOT_A_VALID_APP:
        throw new AppIdentityServiceFailureException("The application is not valid.");
      case DEADLINE_EXCEEDED:
        throw new AppIdentityServiceFailureException("The deadline for the call was exceeded.");
      case UNKNOWN_ERROR:
        throw new AppIdentityServiceFailureException(
            "There was an unknown error using the AppIdentity service.");
      case UNKNOWN_SCOPE:
        throw new AppIdentityServiceFailureException("An unknown scope was supplied.");
      default:
        throw new AppIdentityServiceFailureException(
            "The AppIdentity service threw an unexpected error. Details: " + e.getErrorDetail());
    }
  }

  @Override
  public List getPublicCertificatesForApp() {
    GetPublicCertificateForAppRequest.Builder requestBuilder =
        GetPublicCertificateForAppRequest.newBuilder();
    GetPublicCertificateForAppResponse.Builder responseBuilder =
        GetPublicCertificateForAppResponse.newBuilder();

    try {
      responseBuilder.mergeFrom(
          ApiProxy.makeSyncCall(
              PACKAGE_NAME, GET_CERTS_METHOD_NAME, requestBuilder.build().toByteArray()));
    } catch (ApiProxy.ApplicationException e) {
      handleApplicationError(e);
    } catch (InvalidProtocolBufferException e) {
      throw new AppIdentityServiceFailureException(e.getMessage());
    }
    GetPublicCertificateForAppResponse response = responseBuilder.build();

    List certs = Lists.newArrayList();
    for (AppIdentityServicePb.PublicCertificate cert : response.getPublicCertificateListList()) {
      certs.add(new PublicCertificate(cert.getKeyName(), cert.getX509CertificatePem()));
    }
    return certs;
  }

  @Override
  public SigningResult signForApp(byte[] signBlob) {
    SignForAppRequest.Builder requestBuilder = SignForAppRequest.newBuilder();
    requestBuilder.setBytesToSign(ByteString.copyFrom(signBlob));
    SignForAppResponse.Builder responseBuilder = SignForAppResponse.newBuilder();
    try {
      responseBuilder.mergeFrom(
          ApiProxy.makeSyncCall(
              PACKAGE_NAME, SIGN_FOR_APP_METHOD_NAME, requestBuilder.build().toByteArray()));
    } catch (ApiProxy.ApplicationException e) {
      handleApplicationError(e);
    } catch (InvalidProtocolBufferException e) {
      throw new AppIdentityServiceFailureException(e.getMessage());
    }

    SignForAppResponse response = responseBuilder.build();
    return new SigningResult(response.getKeyName(), response.getSignatureBytes().toByteArray());
  }

  @Override
  public String getServiceAccountName() {
    GetServiceAccountNameRequest.Builder requestBuilder = GetServiceAccountNameRequest.newBuilder();
    GetServiceAccountNameResponse.Builder responseBuilder =
        GetServiceAccountNameResponse.newBuilder();
    try {
      responseBuilder.mergeFrom(
          ApiProxy.makeSyncCall(
              getAccessTokenPackageName(),
              GET_SERVICE_ACCOUNT_NAME_METHOD_NAME,
              requestBuilder.build().toByteArray()));
    } catch (ApiProxy.ApplicationException e) {
      handleApplicationError(e);
    } catch (InvalidProtocolBufferException e) {
      throw new AppIdentityServiceFailureException(e.getMessage());
    }

    GetServiceAccountNameResponse response = responseBuilder.build();
    return response.getServiceAccountName();
  }

  @Override
  public String getDefaultGcsBucketName() {
    GetDefaultGcsBucketNameRequest.Builder requestBuilder =
        GetDefaultGcsBucketNameRequest.newBuilder();
    GetDefaultGcsBucketNameResponse.Builder responseBuilder =
        GetDefaultGcsBucketNameResponse.newBuilder();
    try {
      responseBuilder.mergeFrom(
          ApiProxy.makeSyncCall(
              PACKAGE_NAME, GET_DEFAULT_GCS_BUCKET_NAME, requestBuilder.build().toByteArray()));
    } catch (ApiProxy.ApplicationException e) {
      handleApplicationError(e);
    } catch (InvalidProtocolBufferException e) {
      throw new AppIdentityServiceFailureException(e.getMessage());
    }

    GetDefaultGcsBucketNameResponse response = responseBuilder.build();
    if (response.hasDefaultGcsBucketName()) {
      return response.getDefaultGcsBucketName();
    } else {
      throw new AppIdentityServiceFailureException(
          "getDefaultGcsBucketNameResponse contained no data");
    }
  }

  @Override
  public GetAccessTokenResult getAccessTokenUncached(Iterable scopes) {
    GetAccessTokenRequest.Builder requestBuilder = GetAccessTokenRequest.newBuilder();
    for (String scope : scopes) {
      requestBuilder.addScope(scope);
    }
    if (requestBuilder.getScopeCount() == 0) {
      // Avoid an RPC which will just fail with UNKNOWN_SCOPE.
      throw new AppIdentityServiceFailureException("No scopes specified.");
    }
    GetAccessTokenResponse.Builder responseBuilder = GetAccessTokenResponse.newBuilder();
    try {
      responseBuilder.mergeFrom(
          ApiProxy.makeSyncCall(
              getAccessTokenPackageName(),
              GET_ACCESS_TOKEN_METHOD_NAME,
              requestBuilder.build().toByteArray()));
    } catch (ApiProxy.ApplicationException e) {
      handleApplicationError(e);
    } catch (InvalidProtocolBufferException e) {
      throw new AppIdentityServiceFailureException(e.getMessage());
    }

    GetAccessTokenResponse response = responseBuilder.build();
    return new GetAccessTokenResult(
        response.getAccessToken(), new Date(response.getExpirationTime() * 1000));
  }

  private String getAccessTokenPackageName() {
    return Boolean.getBoolean("appengine.app_identity.use_robot")
            && SystemProperty.environment.value() != SystemProperty.Environment.Value.Production
        ? "robot_enabled_app_identity_service"
        : "app_identity_service";
  }

  String memcacheKeyForScopes(Iterable scopes) {
    // Same format as Python MEMCACHE_KEY_PREFIX + scopes
    // We don't allow a single scope in Java (perhaps we should?) so a cross-
    // platform app may use both key_['scope1'] and key_scope1. No big deal.
    // TODO: Even the case of multi-scope is not going to match
    // the Python keys because the latter scope's delimiter is ', '.
    // This is actually good as we cross-platform memcache matching
    // is not desirable (values are serialized differently).
    StringBuilder builder = new StringBuilder();
    builder.append(MEMCACHE_KEY_PREFIX);
    builder.append('[');
    if (!Iterables.isEmpty(scopes)) {
      for (String scope : scopes) {
        builder.append('\'');
        builder.append(scope);
        builder.append("',");
      }
      builder.setLength(builder.length() - 1);
    }
    builder.append(']');
    return builder.toString();
  }

  @Override
  public GetAccessTokenResult getAccessToken(Iterable scopes) {
    String cacheKey = memcacheKeyForScopes(scopes);
    CacheItem cacheItem = cache.getUnchecked(cacheKey);

    try {
      cacheItem.getAccessSemaphore().acquire();
    } catch (InterruptedException e) {
      // reset the interrupt flag and bail out
      Thread.currentThread().interrupt();
      throw new AppIdentityServiceFailureException(e.getMessage());
    }

    try {
      // First, Check locally
      GetAccessTokenResult result = cacheItem.get();
      if (result != null) {
        return result;
      }
      // Second, check at memcache. Though memcache should expire the value before
      // the local cache the latter may not have it upon startup.
      try {
        result = (GetAccessTokenResult) memcacheService.get(cacheKey);
      } catch (MemcacheServiceException e) {
        // Silently ignore this error, since storing the data in memcache
        // is purely an optimization.
      }
      if (result != null) {
        cacheItem.set(result);
        return result;
      }
      // Not found in both caches, get from service and populate caches.
      result = getAccessTokenUncached(scopes);
      Calendar cal = Calendar.getInstance();
      cal.setTime(result.getExpirationTime());
      cal.add(Calendar.MILLISECOND, -1 * MEMCACHE_EXPIRATION_DELTA_MILLIS);
      try {
        memcacheService.put(cacheKey, result, Expiration.onDate(cal.getTime()));
      } catch (MemcacheServiceException e) {
        // Silently ignore this error, since storing the data in memcache
        // is purely an optimization.
      }
      cacheItem.set(result);
      return result;
    } finally {
      cacheItem.getAccessSemaphore().release();
    }
  }

  @Override
  public ParsedAppId parseFullAppId(String fullAppId) {
    int partitionIdx = fullAppId.indexOf(APP_PARTITION_SEPARATOR);
    String partition;
    // ~x should not denote the empty partition to avoid having different
    // app ids mapping to the same ParsedAppId (same for domains below)
    if (partitionIdx > 0) {
      partition = fullAppId.substring(0, partitionIdx);
      fullAppId = fullAppId.substring(partitionIdx + 1);
    } else {
      partition = "";
    }

    int domainIdx = fullAppId.indexOf(APP_DOMAIN_SEPARATOR);
    String domain;
    if (domainIdx > 0) {
      domain = fullAppId.substring(0, domainIdx);
      fullAppId = fullAppId.substring(domainIdx + 1);
    } else {
      domain = "";
    }

    return new ParsedAppId(partition, domain, fullAppId);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy