Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Commit

Permalink
Adding beginnings of a system tests URL.
Browse files Browse the repository at this point in the history
It seems `google-auth` is a little under-powered on
the dev-appserver:

```
>>> import google.auth
>>> credentials, project = google.auth.default()
>>> credentials
<google.auth.app_engine.Credentials object at 0x7fc9b51105d0>
>>> project
'None'
```

As compared with (also in the dev appserver):

```
>>> from google.appengine.api import app_identity
>>> app_identity.get_application_id()
'None'
>>> app_identity.get_service_account_name()
'...@developer.gserviceaccount.com'
>>> key_name, signature = app_identity.sign_blob(b'abc')
>>> key_name
'5167...'
>>> signature
'6)\x0c\xe0\xad\x1a\x05\xfa\xfd\xcd\xd9_3L{\xad...'
```

References:

- https://cloud.google.com/appengine/docs/standard/python/refdocs/google.appengine.api.app_identity.app_identity
- googleapis/google-cloud-python#574
  • Loading branch information
dhermes committed Oct 11, 2017
1 parent a18d047 commit 37d3201
Show file tree
Hide file tree
Showing 3 changed files with 306 additions and 4 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 && \
Expand Down
255 changes: 255 additions & 0 deletions convert_key.py
Original file line number Diff line number Diff line change
@@ -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()
49 changes: 46 additions & 3 deletions language-app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import boltons.tbutils
import flask
import google.auth
import google.protobuf
try:
import grpc
Expand All @@ -34,6 +35,8 @@
import setuptools
import six

from google.appengine.api import app_identity


app = flask.Flask(__name__)

Expand All @@ -42,7 +45,8 @@
<ul>
<li><a href="/info">Environment Info</a></li>
<li><a href="/import">Package Import Check</a></li>
<li><a href="/tests">Unit Test Output</a></li>
<li><a href="/unit-tests">Unit Test Output</a></li>
<li><a href="/system-tests">System Test Output</a></li>
</ul>
</html>
"""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'...'),
)

0 comments on commit 37d3201

Please sign in to comment.