diff --git a/Makefile b/Makefile index e0ed88d..63cbd59 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,8 @@ PY27?=python2.7 DEV_APPSERVER?=$(shell which dev_appserver.py) GCLOUD?=gcloud +GAE_EMAIL=$(shell $(PY27) convert_key.py --email) +GAE_KEY=$(shell $(PY27) convert_key.py --pkcs1) help: @echo 'Makefile for a google-cloud-python-on-gae' @@ -47,7 +49,9 @@ language-app/clean-env: language-app-run: language-app/lib language-app/clean-env language-app/app.yaml # $(GCLOUD) components update cd language-app && \ - clean-env/bin/python2.7 $(DEV_APPSERVER) app.yaml + clean-env/bin/python2.7 $(DEV_APPSERVER) app.yaml \ + --appidentity_email_address $(GAE_EMAIL) \ + --appidentity_private_key_path $(GAE_KEY) language-app-deploy: language-app/lib language-app/app.yaml cd language-app && \ diff --git a/convert_key.py b/convert_key.py new file mode 100644 index 0000000..4d54c7a --- /dev/null +++ b/convert_key.py @@ -0,0 +1,255 @@ +# Copyright 2017 Google Inc. All rights reserved. +# +# 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 +# +# http://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. + +"""Helper to convert a JSON key file into a PEM PKCS#1 key.""" + +from __future__ import print_function + +import argparse +import os +import json +import subprocess +import sys + +try: + import py +except ImportError: + py = None + + +ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS' + + +def _require_env(): + json_filename = os.environ.get(ENV_VAR) + if json_filename is None: + msg = '{} is unset'.format(ENV_VAR) + print(msg, file=sys.stderr) + sys.exit(1) + + return json_filename + + +def _require_file(json_filename): + if not os.path.isfile(json_filename): + msg = '{}={} is not a file.'.format(ENV_VAR, json_filename) + print(msg, file=sys.stderr) + sys.exit(1) + + +def _require_json(json_filename): + with open(json_filename, 'r') as file_obj: + try: + return json.load(file_obj) + except: + msg = '{}={} does not contain valid JSON.'.format( + ENV_VAR, json_filename) + print(msg, file=sys.stderr) + sys.exit(1) + + +def _require_private_key(key_json): + pkcs8_pem = key_json.get('private_key') + if pkcs8_pem is None: + msg = '``private_key`` missing in JSON key file' + print(msg, file=sys.stderr) + sys.exit(1) + + return pkcs8_pem + + +def _require_email(key_json): + client_email = key_json.get('client_email') + if client_email is None: + msg = '``client_email`` missing in JSON key file' + print(msg, file=sys.stderr) + sys.exit(1) + + return client_email + + +def get_key_json(): + json_filename = _require_env() + _require_file(json_filename) + key_json = _require_json(json_filename) + return key_json, json_filename + + +def _require_py(): + if py is None: + msg = 'py (https://pypi.org/project/py/) must be installed.' + print(msg, file=sys.stderr) + sys.exit(1) + + +def _require_openssl(): + """Check that ``openssl`` is on the PATH. + + Assumes :func:`_require_py` has been checked. + """ + if py.path.local.sysfind('openssl') is None: + msg = '``openssl`` command line tool must be installed.' + print(msg, file=sys.stderr) + sys.exit(1) + + +def _pkcs8_filename(pkcs8_pem, base): + """Create / check a PKCS#8 file. + + Exits with 1 if the file already exists and differs from + ``pkcs8_pem``. If the file does not exists, creates it with + ``pkcs8_pem`` as contents and sets permissions to 0400. + + Args: + pkcs8_pem (str): The contents to be stored (or checked). + base (str): The base file path (without extension). + + Returns: + str: The filename that was checked / created. + """ + pkcs8_filename = '{}-PKCS8.pem'.format(base) + if os.path.exists(pkcs8_filename): + with open(pkcs8_filename, 'r') as file_obj: + contents = file_obj.read() + + if contents != pkcs8_pem: + msg = 'PKCS#8 file {} already exists.'.format(pkcs8_filename) + print(msg, file=sys.stderr) + sys.exit(1) + else: + with open(pkcs8_filename, 'w') as file_obj: + file_obj.write(pkcs8_pem) + # Protect the file from being read by other users.. + os.chmod(pkcs8_filename, 0o400) + + return pkcs8_filename + + +def _pkcs1_verify(pkcs8_filename, pkcs1_filename): + """Verify the contents of an existing PKCS#1 file. + + Does so by using ``openssl rsa`` to print to stdout and + then checking against contents. + + Exits with 1 if: + + * The ``openssl`` command fails + * The ``pkcs1_filename`` contents differ from what was produced + by ``openssl`` + + Args: + pkcs8_filename (str): The PKCS#8 file to be converted. + pkcs1_filename (str): The PKCS#1 file to check against. + """ + cmd = ( + 'openssl', + 'rsa', + '-in', + pkcs8_filename, + ) + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return_code = process.wait() + + if return_code != 0: + msg = 'Failed checking contents of {} against openssl.'.format( + pkcs1_filename) + print(msg, file=sys.stderr) + sys.exit(1) + + cmd_output = process.stdout.read().decode('utf-8') + with open(pkcs1_filename, 'r') as file_obj: + expected_contents = file_obj.read() + + if cmd_output != expected_contents: + msg = 'PKCS#1 file {} already exists.'.format(pkcs1_filename) + print(msg, file=sys.stderr) + sys.exit(1) + + +def _pkcs1_create(pkcs8_filename, pkcs1_filename): + """Create a existing PKCS#1 file from a PKCS#8 file. + + Does so by using ``openssl rsa -in * -out *``. + + Exits with 1 if the ``openssl`` command fails. + + Args: + pkcs8_filename (str): The PKCS#8 file to be converted. + pkcs1_filename (str): The PKCS#1 file to be created. + """ + cmd = ( + 'openssl', + 'rsa', + '-in', + pkcs8_filename, + '-out', + pkcs1_filename, + ) + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return_code = process.wait() + if return_code != 0: + msg = 'Failed to convert {} to {} with openssl.'.format( + pkcs8_filename, pkcs1_filename) + print(msg, file=sys.stderr) + sys.exit(1) + + +def convert_key(pkcs8_pem, json_filename): + _require_py() + _require_openssl() + + base, _ = os.path.splitext(json_filename) + pkcs8_filename = _pkcs8_filename(pkcs8_pem, base) + + pkcs1_filename = '{}-PKCS1.pem'.format(base) + if os.path.exists(pkcs1_filename): + _pkcs1_verify(pkcs8_filename, pkcs1_filename) + else: + _pkcs1_create(pkcs8_filename, pkcs1_filename) + + return pkcs1_filename + + +def get_args(): + parser = argparse.ArgumentParser( + description='Convert a JSON keyfile to dev_appserver values.') + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + '--email', action='store_true', + help='Requests that the email address be returned.') + pkcs1_help = ( + 'Requests that a filename for the converted PKCS#1 file be returned.') + group.add_argument( + '--pkcs1', action='store_true', help=pkcs1_help) + + return parser.parse_args() + + +def main(): + args = get_args() + + key_json, json_filename = get_key_json() + if args.email: + print(_require_email(key_json)) + else: + pkcs8_pem = _require_private_key(key_json) + pkcs1_filename = convert_key(pkcs8_pem, json_filename) + print(pkcs1_filename) + + +if __name__ == '__main__': + main() diff --git a/language-app/main.py b/language-app/main.py index 97416bc..34e8977 100644 --- a/language-app/main.py +++ b/language-app/main.py @@ -23,6 +23,7 @@ import boltons.tbutils import flask +import google.auth import google.protobuf try: import grpc @@ -34,6 +35,8 @@ import setuptools import six +from google.appengine.api import app_identity + app = flask.Flask(__name__) @@ -42,7 +45,8 @@ """ @@ -154,9 +158,9 @@ def load_module(path): mod_name, file_obj, filename, details) -@app.route('/tests') +@app.route('/unit-tests') @PrettyErrors -def tests(): +def unit_tests(): test_mods = [] for dirpath, _, filenames in os.walk('unit-tests'): for filename in filenames: @@ -226,3 +230,42 @@ def import_(): '>>> language', repr(language), ) + + +@app.route('/system-tests') +@PrettyErrors +def system_tests(): + credentials, project = google.auth.default() + key_name, signature = app_identity.sign_blob(b'abc') + return code_block( + '>>> import google.auth', + '>>> credentials, project = google.auth.default()', + '>>> credentials', + repr(credentials), + '>>> project', + repr(project), + '>>> credentials.__dict__', + repr(credentials.__dict__), + '>>> from google.appengine.api import app_identity', + '>>> app_identity', + repr(app_identity), + # ALSO: get_access_token_uncached + # (scopes, service_account_id=None) + # '>>> app_identity.get_access_token()', + # repr(app_identity.get_access_token()), + '>>> app_identity.get_application_id()', + repr(app_identity.get_application_id()), + '>>> app_identity.get_default_gcs_bucket_name()', + repr(app_identity.get_default_gcs_bucket_name()), + '>>> app_identity.get_default_version_hostname()', + repr(app_identity.get_default_version_hostname()), + '>>> app_identity.get_public_certificates()', + repr(app_identity.get_public_certificates()), + '>>> app_identity.get_service_account_name()', + repr(app_identity.get_service_account_name()), + '>>> key_name, signature = app_identity.sign_blob(b\'abc\')', + '>>> key_name', + repr(key_name), + '>>> signature', + repr(signature[:16] + b'...'), + )