From 1fc818726ef54d77bdc0c00990304a69db15b39f Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 17 Mar 2017 13:27:51 -0700 Subject: [PATCH 01/12] Initial code contribution with auth API implementation and unit tests --- .gitignore | 2 + firebase/__init__.py | 156 +++++++++++++++ firebase/auth.py | 345 ++++++++++++++++++++++++++++++++ firebase/jwt.py | 149 ++++++++++++++ tests/__init__.py | 1 + tests/app_test.py | 72 +++++++ tests/auth_test.py | 344 +++++++++++++++++++++++++++++++ tests/data/private_key.pem | 28 +++ tests/data/public_certs.json | 5 + tests/data/service_account.json | 12 ++ tests/testutils.py | 36 ++++ 11 files changed, 1150 insertions(+) create mode 100644 .gitignore create mode 100644 firebase/__init__.py create mode 100644 firebase/auth.py create mode 100644 firebase/jwt.py create mode 100644 tests/__init__.py create mode 100644 tests/app_test.py create mode 100644 tests/auth_test.py create mode 100644 tests/data/private_key.pem create mode 100644 tests/data/public_certs.json create mode 100644 tests/data/service_account.json create mode 100644 tests/testutils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..07f8a98e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +.cache/ diff --git a/firebase/__init__.py b/firebase/__init__.py new file mode 100644 index 000000000..1681a6565 --- /dev/null +++ b/firebase/__init__.py @@ -0,0 +1,156 @@ +"""Firebase Admin SDK for Python.""" +import threading + + +_apps = {} +_apps_lock = threading.RLock() + +_DEFAULT_APP_NAME = '[DEFAULT]' + + +def initialize_app(options, name=_DEFAULT_APP_NAME): + """Initializes and returns a new App instance. + + Creates a new App intance using the specified options + and the app name. If an instance already exists by the same + app name a ValueError is raised. Use this function whenever + a new App instance is required. Do not directly invoke the + App constructor. + + Args: + options: A dictionary of configuration options. + name: Name of the app (optional). + + Returns: + A newly initialized instance of App. + + Raises: + ValueError: If the app name is already in use, or any of the + provided arguments are invalid. + """ + app = App(name, options) + with _apps_lock: + if app.name not in _apps: + _apps[app.name] = app + return app + + if name == _DEFAULT_APP_NAME: + raise ValueError(( + 'The default Firebase app already exists. This means you called ' + 'initialize_app() more than once without providing an app name as the ' + 'second argument. In most cases you only need to call initialize_app()' + ' once. But if you do want to initialize multiple apps, pass a second ' + 'argument to initialize_app() to give each app a unique name.')) + else: + raise ValueError(( + 'Firebase app named "{0}" already exists. This means you called ' + 'initialize_app() more than once with the same app name as the second ' + 'argument. Make sure you provide a unique name every time you call ' + 'initialize_app().').format(name)) + + +def delete_app(name): + """Gracefully deletes an App instance. + + Args: + name: Name of the app instance to be deleted. + + Raises: + ValueError: If the name is not a string. + """ + if not isinstance(name, basestring): + raise ValueError('Illegal app name argument type: "{}". App name ' + 'must be a string.'.format(type(name))) + with _apps_lock: + if name in _apps: + del _apps[name] + return + if name == _DEFAULT_APP_NAME: + raise ValueError( + ('The default Firebase app does not exist. Make sure to initialize the' + ' SDK by calling initialize_app().')) + else: + raise ValueError( + ('Firebase app named "{0}" does not exist. Make sure to initialize the' + ' SDK by calling initialize_app() with your app name as the second' + ' argument.').format(name)) + + +def get_app(name=_DEFAULT_APP_NAME): + """Retrieves an App instance by name. + + Args: + name: Name of the App instance to retrieve (optional). + + Returns: + An App instance. + + Raises: + ValueError: If the specified name is not a string, or if the specified + app does not exist. + """ + if not isinstance(name, basestring): + raise ValueError('Illegal app name argument type: "{}". App name ' + 'must be a string.'.format(type(name))) + with _apps_lock: + if name in _apps: + return _apps[name] + + if name == _DEFAULT_APP_NAME: + raise ValueError( + 'The default Firebase app does not exist. Make sure to initialize the' + ' SDK by calling initialize_app().') + else: + raise ValueError( + ('Firebase app named "{0}" does not exist. Make sure to initialize the' + ' SDK by calling initialize_app() with your app name as the second' + ' argument.').format(name)) + + +class _AppOptions(object): + """A collection of configuration options for an App.""" + + def __init__(self, options): + if not isinstance(options, dict): + raise ValueError('Illegal Firebase app options type: {0}. Options ' + 'must be a dictionary.'.format(type(options))) + self._credential = options.get('credential', None) + if not self._credential: + raise ValueError('Options must be a dict containing at least a' + ' "credential" key.') + + @property + def credential(self): + return self._credential + + +class App(object): + """The entry point for Firebase Python SDK. + + Represents a Firebase app, while holding the configuration and state + common to all Firebase APIs. + """ + + def __init__(self, name, options): + """Constructs a new App using the provided name and options. + + Args: + name: Name of the application. + options: A dictionary of configuration options. + + Raises: + ValueError: If an argument is None or invalid. + """ + if not name or not isinstance(name, basestring): + raise ValueError('Illegal Firebase app name "{0}" provided. ' + 'App name must be a non-empty string.'.format(name)) + self._name = name + self._options = _AppOptions(options) + + @property + def name(self): + return self._name + + @property + def options(self): + return self._options diff --git a/firebase/auth.py b/firebase/auth.py new file mode 100644 index 000000000..fcbb102b9 --- /dev/null +++ b/firebase/auth.py @@ -0,0 +1,345 @@ +"""Firebase Authentication Library. + +This library contains helper methods and utilities for minting and verifying +JWTs used for authenticating against Firebase services. +""" + +import json +import os +import sys +import threading +import time + +import httplib2 +from oauth2client import client +from oauth2client import crypt +from OpenSSL import crypto + +import firebase +from firebase import jwt + +_auth_lock = threading.Lock() +_http = None + +_AUTH_ATTRIBUTE = '_auth' +GCLOUD_PROJECT_ENV_VAR = 'GCLOUD_PROJECT' + + +def _get_initialized_app(app): + if app is None: + return firebase.get_app() + elif isinstance(app, firebase.App): + initialized_app = firebase.get_app(app.name) + if app is not initialized_app: + raise ValueError('Illegal app argument. App instance not initialized' + ' via the firebase module.') + return app + else: + raise ValueError('Illegal app argument. Argument must be of type ' + ' firebase.App, but given "{0}".'.format(type(app))) + + +def _get_token_generator(app): + """Returns a _TokenGenerator instance for an App. + + If the App already has a _TokenGenerator associated with it, simply returns + it. Otherwise creates a new _TokenGenerator, and adds it to the App before + returning it. + + Args: + app: A Firebase App instance (or None to use the default App). + + Returns: + A _TokenGenerator instance. + + Raises: + ValueError: If the app argument is invalid. + """ + app = _get_initialized_app(app) + with _auth_lock: + if not hasattr(app, _AUTH_ATTRIBUTE): + setattr(app, _AUTH_ATTRIBUTE, _TokenGenerator(app)) + return getattr(app, _AUTH_ATTRIBUTE) + + +def create_custom_token(uid, developer_claims=None, app=None): + """Builds and signs a Firebase custom auth token. + + Args: + uid: ID of the user for whom the token is created. + developer_claims: A dictionary of claims to be included in the token + (optional). + app: An App instance (optional). + + Returns: + A token string minted from the input parameters. + + Raises: + ValueError: If input parameters are invalid. + """ + token_generator = _get_token_generator(app) + return token_generator.create_custom_token(uid, developer_claims) + + +def verify_id_token(id_token, app=None): + """Verifies the signature and data for the provided JWT. + + Accepts a signed token string, verifies that it is current, and issued + to this project, and that it was correctly signed by Google. + + Args: + id_token: A string of the encoded JWT. + app: An App instance (optional). + + Returns: + A dict consisting of the key-value pairs parsed from the decoded JWT. + + Raises: + ValueError: If the input parameters are invalid, or if the App was not + initialized with a CertificateCredential. + AppIdenityError: The JWT was found to be invalid, the message will contain + details. + """ + token_generator = _get_token_generator(app) + return token_generator.verify_id_token(id_token) + + +class _TokenGenerator(object): + """Generates custom tokens, and validates ID tokens.""" + + FIREBASE_CERT_URI = ('https://www.googleapis.com/robot/v1/metadata/x509/' + 'securetoken@system.gserviceaccount.com') + + ISSUER_PREFIX = 'https://securetoken.google.com/' + + MAX_TOKEN_LIFETIME_SECONDS = 3600 # One Hour, in Seconds + FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis.com/google.identity.' + 'identitytoolkit.v1.IdentityToolkit') + + # Key names we don't allow to appear in the developer_claims. + _RESERVED_CLAIMS_ = set([ + 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', + 'exp', 'firebase', 'iat', 'iss', 'jti', 'nbf', 'nonce', 'sub' + ]) + """Provided for overriding during tests. (OAuth2 client uses a caching-enabled + HTTP client internally if none provided) + """ + + def __init__(self, app): + """Initializes FirebaseAuth from a FirebaseApp instance. + + Args: + app: A FirebaseApp instance. + """ + self._app = app + + def create_custom_token(self, uid, developer_claims=None): + """Builds and signs a FirebaseCustomAuthToken. + + Args: + uid: ID of the user for whom the token is created. + developer_claims: A dictionary of claims to be included in the token. + + Returns: + A token string minted from the input parameters. + + Raises: + ValueError: If input parameters are invalid. + """ + credential = self._app.options.credential + if not isinstance(credential, CertificateCredential): + raise ValueError( + 'Must initialize Firebase App with a certificate credential' + 'to call create_custom_token().') + + if developer_claims is not None: + if not isinstance(developer_claims, dict): + raise ValueError('developer_claims must be a dictionary') + + disallowed_keys = set(developer_claims.keys()) & self._RESERVED_CLAIMS_ + if disallowed_keys: + if len(disallowed_keys) > 1: + error_message = ('Developer claims {0} are reserved and cannot be ' + 'specified.'.format(', '.join(disallowed_keys))) + else: + error_message = ('Developer claim {0} is reserved and cannot be ' + 'specified.'.format(', '.join(disallowed_keys))) + raise ValueError(error_message) + + if not uid or not isinstance(uid, basestring) or len(uid) > 128: + raise ValueError('uid must be a string between 1 and 128 characters.') + + now = int(time.time()) + payload = { + 'iss': credential.service_account_email, + 'sub': credential.service_account_email, + 'aud': self.FIREBASE_AUDIENCE, + 'uid': uid, + 'iat': now, + 'exp': now + self.MAX_TOKEN_LIFETIME_SECONDS, + } + + if developer_claims is not None: + payload['claims'] = developer_claims + + return jwt.encode(payload, credential.signer) + + def verify_id_token(self, id_token): + """Verifies the signature and data for the provided JWT. + + Accepts a signed token string, verifies that is the current, and issued + to this project, and that it was correctly signed by Google. + + Args: + id_token: A string of the encoded JWT. + + Returns: + A dict consisting of the key-value pairs parsed from the decoded JWT. + + Raises: + ValueError: The app was not initialized with a CertificateCredential + AppIdenityError: The JWT was found to be invalid, the message will contain + details. + """ + if not id_token or not isinstance(id_token, basestring): + raise ValueError('Illegal ID token provided: {0}. ID token ' + 'must be a non-empty string.'.format(id_token)) + + credential = self._app.options.credential + try: + project_id = credential.project_id + except AttributeError: + project_id = os.environ.get(GCLOUD_PROJECT_ENV_VAR) + + if not project_id: + raise ValueError('Must initialize app with a CertificateCredential or ' + 'set your Firebase project ID as the GCLOUD_PROJECT ' + 'environment variable to call verify_id_token().') + + header, payload = jwt.decode(id_token) + issuer = payload.get('iss') + audience = payload.get('aud') + subject = payload.get('sub') + expected_issuer = self.ISSUER_PREFIX + project_id + + project_id_match_msg = ('Make sure the ID token comes from the same' + ' Firebase project as the service account used' + ' to authenticate this SDK.') + verify_id_token_msg = ( + 'See https://firebase.google.com/docs/auth/admin/verify-id-tokens' + ' for details on how to retrieve an ID token.') + error_message = None + if not header.get('kid'): + if audience == self.FIREBASE_AUDIENCE: + error_message = ('verify_id_token() expects an ID token, but was' + ' given a custom token.') + elif header.get('alg') == 'HS256' and payload.get( + 'v') is 0 and 'uid' in payload.get('d', {}): + error_message = ('verify_id_token() expects an ID token, but was' + ' given a legacy custom token.') + else: + error_message = 'Firebase ID token has no "kid" claim.' + elif header.get('alg') != 'RS256': + error_message = ('Firebase ID token has incorrect algorithm. ' + 'Expected "RS256" but got "{0}". {1}'.format( + header.get('alg'), verify_id_token_msg)) + elif audience != project_id: + error_message = ( + 'Firebase ID token has incorrect "aud" (audience) claim. Expected ' + '"{0}" but got "{1}". {2} {3}'.format( + project_id, audience, project_id_match_msg, verify_id_token_msg)) + elif issuer != expected_issuer: + error_message = ('Firebase ID token has incorrect "iss" (issuer) claim.' + ' Expected "{0}" but got "{1}". {2} {3}'.format( + expected_issuer, issuer, project_id_match_msg, + verify_id_token_msg)) + elif subject is None or not isinstance(subject, basestring): + error_message = ('Firebase ID token has no "sub" (subject) ' + 'claim. ') + verify_id_token_msg + elif not subject: + error_message = ('Firebase ID token has an empty string "sub" (subject) ' + 'claim. ') + verify_id_token_msg + elif len(subject) > 128: + error_message = ('Firebase ID token has "sub" (subject) claim longer than' + ' 128 characters. ') + verify_id_token_msg + + if error_message: + raise crypt.AppIdentityError(error_message) + + return jwt.verify_id_token( + id_token, + self.FIREBASE_CERT_URI, + audience=project_id, + kid=header.get('kid'), + http=_http) + + +class Credential(object): + """Provides OAuth2 access tokens for accessing Firebase services. + """ + + def get_access_token(self, force_refresh=False): + """Fetches a Google OAuth2 access token using this credential instance. + + Args: + force_refresh: A boolean value indicating whether to fetch a new token + or use a cached one if available. + """ + raise NotImplementedError + + def get_credential(self): + """Returns the underlying credential instance used for authentication.""" + raise NotImplementedError + + +class CertificateCredential(Credential): + """A Credential initialized from a JSON keyfile.""" + + def __init__(self, file_path): + """Initializes a credential from a certificate file. + + Parses the specified certificate file (service account file), and + creates a credential instance from it. + + Args: + file_path: Path to a service account certificate file. + + Raises: + IOError: If the specified file doesn't exist or cannot be read. + ValueError: If an error occurs while parsing the file content. + """ + super(CertificateCredential, self).__init__() + # TODO(hkj): Clean this up once we are able to take a dependency + # TODO(hkj): on latest oauth2client. + with open(file_path) as json_keyfile: + json_data = json.load(json_keyfile) + self._project_id = json_data.get('project_id') + try: + self._signer = crypt.Signer.from_string(json_data.get('private_key')) + except crypto.Error as error: + err_type, err_value, err_traceback = sys.exc_info() + err_message = 'Failed to parse the private key string: {0}'.format(error) + raise ValueError, (err_message, err_type, err_value), err_traceback + self._service_account_email = json_data.get('client_email') + self._g_credential = client.GoogleCredentials.from_stream(file_path) + + @property + def project_id(self): + return self._project_id + + @property + def signer(self): + return self._signer + + @property + def service_account_email(self): + return self._service_account_email + + def get_access_token(self, force_refresh=False): + if force_refresh: + self._g_credential.refresh(httplib2.Http()) + token_info = self._g_credential.get_access_token() + return token_info.access_token + + def get_credential(self): + return self._g_credential diff --git a/firebase/jwt.py b/firebase/jwt.py new file mode 100644 index 000000000..e71723f55 --- /dev/null +++ b/firebase/jwt.py @@ -0,0 +1,149 @@ +"""Utility functions for encoding/decoding JWT tokens. + +This module implements the basic JWT token encoding and +decoding functionality. Most function implementations +were inspired by the oauth2client library. It also uses the +crypto capabilities of the oauth2client library for +signing and verifying JWTs. However, unlike oauth2client +this implementation provides more control over JWT headers. +""" +import base64 +import json + +import httplib2 +from oauth2client import client +from oauth2client import crypt +import six + +try: + # Newer versions of oauth2client (> v1.4) + # pylint: disable=g-import-not-at-top + from oauth2client import transport + _cached_http = httplib2.Http(transport.MemoryCache()) +except ImportError: + # Older versions of oauth2client (<= v1.4) + _cached_http = httplib2.Http(client.MemoryCache()) + + +def _to_bytes(value, encoding='ascii'): + result = (value.encode(encoding) + if isinstance(value, six.text_type) else value) + if isinstance(result, six.binary_type): + return result + else: + raise ValueError('{0!r} could not be converted to bytes'.format(value)) + + +def _urlsafe_b64encode(raw_bytes): + raw_bytes = _to_bytes(raw_bytes, encoding='utf-8') + return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=') + + +def _urlsafe_b64decode(b64string): + b64string = _to_bytes(b64string) + padded = b64string + b'=' * (4 - len(b64string) % 4) + return base64.urlsafe_b64decode(padded) + + +def encode(payload, signer, headers=None): + """Encodes the provided payload into a signed JWT. + + Creates a signed JWT from the given dictionary payload of claims. + By default this function only adds the 'typ' and 'alg' headers to + the encoded JWT. The 'headers' argument can be used to set additional + JWT headers, and override the defaults. This function provides the + bare minimal token encoding and signing functionality. Any validations + on individual claims should be performed by the caller. + + Args: + payload: A dictionary of claims. + signer: An oauth2client.crypt.Signer instance for signing tokens. + headers: An dictionary of headers (optional). + + Returns: + A signed JWT token as a string + """ + header = {'typ': 'JWT', 'alg': 'RS256'} + if headers: + header.update(headers) + segments = [ + _urlsafe_b64encode(json.dumps(header, separators=(',', ':'))), + _urlsafe_b64encode(json.dumps(payload, separators=(',', ':'))), + ] + signing_input = b'.'.join(segments) + signature = signer.sign(signing_input) + segments.append(_urlsafe_b64encode(signature)) + return b'.'.join(segments) + + +def decode(token): + """Decodes the provided JWT into dictionaries. + + Parses the provided token and extracts its header values and claims. + Note that this function does not perform any verification on the + token content. Nor does it attempt to verify the token signature. + Th only validation it performs is for the proper formatting/encoding + of the JWT token, which is necessary to parse it. Simply use this + function to unpack, and inspect the contents of a JWT. + + Args: + token: A signed JWT token as a string. + + Returns: + A 2-tuple where the first element is a dictionary of JWT headers, + and the second element is a dictionary of payload claims. + + Raises: + AppIdentityError: If the token is malformed or badly formatted + """ + if token.count(b'.') != 2: + raise crypt.AppIdentityError(('Wrong number of segments' + ' in token: {0}').format(token)) + header, payload, _ = token.split(b'.') + header_dict = json.loads(_urlsafe_b64decode(header).decode('utf-8')) + payload_dict = json.loads(_urlsafe_b64decode(payload).decode('utf-8')) + return (header_dict, payload_dict) + + +def verify_id_token(id_token, cert_uri, audience=None, kid=None, http=None): + """Verifies the provided ID token. + + Checks for token integrity by verifying its signature against + a set of public key certificates. Certificates are downloaded + from cert_uri, and cached according to the HTTP cache control + requirements. If provided, the audience and kid fields of the + ID token are also validated. + + Args: + id_token: JWT ID token to be validated. + cert_uri: A URI string pointing to public key certificates. + audience: Audience string that should be present in the token. + kid: JWT key ID header to locate the public key certificate. + http: An httplib2 HTTP client instance. + + Returns: + A dictionary of claims extracted from the ID token. + + Raises: + ValueError: Certificate URI is None or empty. + AppIdentityError: Token integrity check failed. + VerifyJwtTokenError: Failed to load public keys or invalid kid header. + """ + if not cert_uri: + raise ValueError('Certificate URI is required') + if not http: + http = _cached_http + resp, content = http.request(cert_uri) + if resp.status != 200: + raise client.VerifyJwtTokenError( + ('Failed to load public key' + ' certificates from URL "{0}". Received HTTP status code {1}.').format( + cert_uri, resp.status)) + certs = json.loads(content.decode('utf-8')) + if kid and not certs.has_key(kid): + raise client.VerifyJwtTokenError( + ('Firebase ID token has "kid" claim which does' + ' not correspond to a known public key. Most' + ' likely the ID token is expired, so get a' + ' fresh token from your client app and try again.')) + return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..bd8a7091e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Enables exclusion of the tests module from the distribution. diff --git a/tests/app_test.py b/tests/app_test.py new file mode 100644 index 000000000..0e08b1e5d --- /dev/null +++ b/tests/app_test.py @@ -0,0 +1,72 @@ +"""Tests for firebase.App.""" +import unittest + +import firebase +from firebase import auth +import testutils + + +class FirebaseAppTest(unittest.TestCase): + + SERVICE_ACCOUNT_PATH = 'service_account.json' + CREDENTIAL = auth.CertificateCredential( + testutils.resource_filename(SERVICE_ACCOUNT_PATH)) + OPTIONS = {'credential': CREDENTIAL} + + def tearDown(self): + testutils.cleanup_apps() + + def testDefaultAppInit(self): + app = firebase.initialize_app(self.OPTIONS) + self.assertEquals(firebase._DEFAULT_APP_NAME, app.name) + self.assertIs(self.CREDENTIAL, app.options.credential) + with self.assertRaises(ValueError): + firebase.initialize_app(self.OPTIONS) + + def testNonDefaultAppInit(self): + app = firebase.initialize_app(self.OPTIONS, 'myApp') + self.assertEquals('myApp', app.name) + self.assertIs(self.CREDENTIAL, app.options.credential) + with self.assertRaises(ValueError): + firebase.initialize_app(self.OPTIONS, 'myApp') + + def testAppInitWithEmptyOptions(self): + with self.assertRaises(ValueError): + firebase.initialize_app({}) + + def testAppInitWithNoCredential(self): + options = {'key': 'value'} + with self.assertRaises(ValueError): + firebase.initialize_app(options) + + def testAppInitWithInvalidOptions(self): + for options in [None, 0, 1, 'foo', list(), tuple(), True, False]: + with self.assertRaises(ValueError): + firebase.initialize_app(options) + + def testAppInitWithInvalidName(self): + for name in [None, '', 0, 1, dict(), list(), tuple(), True, False]: + with self.assertRaises(ValueError): + firebase.initialize_app(self.OPTIONS, name) + + def testDefaultAppGet(self): + app = firebase.initialize_app(self.OPTIONS) + self.assertIs(app, firebase.get_app()) + + def testNonDefaultAppGet(self): + app = firebase.initialize_app(self.OPTIONS, 'myApp') + self.assertIs(app, firebase.get_app('myApp')) + + def testNonExistingDefaultAppGet(self): + with self.assertRaises(ValueError): + self.assertIsNone(firebase.get_app()) + + def testNonExistingAppGet(self): + with self.assertRaises(ValueError): + self.assertIsNone(firebase.get_app('myApp')) + + def testAppGetWithInvalidName(self): + for name in [None, '', 0, 1, dict(), list(), tuple(), True, False]: + with self.assertRaises(ValueError): + firebase.initialize_app(self.OPTIONS, name) + diff --git a/tests/auth_test.py b/tests/auth_test.py new file mode 100644 index 000000000..56594d089 --- /dev/null +++ b/tests/auth_test.py @@ -0,0 +1,344 @@ +"""Tests for firebase.auth.""" +import os +import time +import unittest + +from oauth2client import client +from oauth2client import crypt + +import firebase +from firebase import auth +from firebase import jwt +import testutils + + +class _AbstractAuthTest(unittest.TestCase): + """Super class for auth-related tests. + + Defines constants used in auth-related tests, and provides a method for + asserting the validity of custom tokens. + """ + SERVICE_ACCOUNT_EMAIL = 'test-484@mg-test-1210.iam.gserviceaccount.com' + PROJECT_ID = 'test-484' + CLIENT_CERT_URL = ('https://www.googleapis.com/robot/v1/metadata/x509/' + 'test-484%40mg-test-1210.iam.gserviceaccount.com') + + FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis' + '.com/google.identity.identitytoolkit.' + 'v1.IdentityToolkit') + USER = 'user1' + ISSUER = 'test-484@mg-test-1210.iam.gserviceaccount.com' + CLAIMS = {'one': 2, 'three': 'four'} + + CREDENTIAL = auth.CertificateCredential( + testutils.resource_filename('service_account.json')) + PUBLIC_CERTS = testutils.resource('public_certs.json') + PRIVATE_KEY = testutils.resource('private_key.pem') + + def verify_custom_token(self, custom_token, verify_claims=True): + token = client.verify_id_token( + custom_token, + self.FIREBASE_AUDIENCE, + http=testutils.HttpMock(200, self.PUBLIC_CERTS), + cert_uri=self.CLIENT_CERT_URL) + self.assertEquals(token['uid'], self.USER) + self.assertEquals(token['iss'], self.SERVICE_ACCOUNT_EMAIL) + self.assertEquals(token['sub'], self.SERVICE_ACCOUNT_EMAIL) + if verify_claims: + self.assertEquals(token['claims']['one'], self.CLAIMS['one']) + self.assertEquals(token['claims']['three'], self.CLAIMS['three']) + + def _merge_jwt_claims(self, defaults, overrides): + defaults.update(overrides) + for k, v in overrides.items(): + if v is None: + del defaults[k] + return defaults + + def get_id_token(self, payload_overrides=None, header_overrides=None): + signer = crypt.Signer.from_string(self.PRIVATE_KEY) + headers = { + 'kid': 'd98d290613ae1468f7e5f5cf604ead38ca9c8358' + } + payload = { + 'aud': 'mg-test-1210', + 'iss': 'https://securetoken.google.com/mg-test-1210', + 'iat': int(time.time()) - 100, + 'exp': int(time.time()) + 3600, + 'sub': '1234567890', + 'uid': self.USER, + 'admin': True, + } + if header_overrides: + headers = self._merge_jwt_claims(headers, header_overrides) + if payload_overrides: + payload = self._merge_jwt_claims(payload, payload_overrides) + return jwt.encode(payload, signer, headers=headers) + + +class TokenGeneratorTest(_AbstractAuthTest): + + APP = firebase.App('test-app', {'credential': _AbstractAuthTest.CREDENTIAL}) + TOKEN_GEN = auth._TokenGenerator(APP) + + def testCustomTokenCreation(self): + token_string = self.TOKEN_GEN.create_custom_token(self.USER, self.CLAIMS) + self.assertIsInstance(token_string, basestring) + self.verify_custom_token(token_string) + + def testCustomTokenCreationWithCorrectHeader(self): + token_string = self.TOKEN_GEN.create_custom_token(self.USER, self.CLAIMS) + header, _ = jwt.decode(token_string) + self.assertEquals('JWT', header.get('typ')) + self.assertEquals('RS256', header.get('alg')) + + def testCustomTokenCreationWithoutDevClaims(self): + token_string = self.TOKEN_GEN.create_custom_token(self.USER) + self.verify_custom_token(token_string, False) + + def testCustomTokenCreationWithEmptyDevClaims(self): + token_string = self.TOKEN_GEN.create_custom_token(self.USER, {}) + self.verify_custom_token(token_string, False) + + def testCustomTokenCreationWithNoUid(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(None) + + def testCustomTokenCreationWithEmptyUid(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token('') + + def testCustomTokenCreationWithLongUid(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token('x' * 129) + + def testCustomTokenCreationWithNonStringUid(self): + for item in [True, False, 0, 1, [], {}, {'a': 1}]: + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(item) + + def testCustomTokenCreationWithBadClaims(self): + for item in [True, False, 0, 1, 'foo', [], (1, 2)]: + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token('user1', item) + + def testCustomTokenCreationWithNonCertCredential(self): + app = firebase.initialize_app({'credential': auth.Credential()}, 'test-app') + token_generator = auth._TokenGenerator(app) + with self.assertRaises(ValueError): + token_generator.create_custom_token(self.USER) + + def testCustomTokenCreationFailsWithReservedClaim(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(self.USER, {'sub': '1234'}) + + def testCustomTokenCreationWithMalformedDeveloperClaims(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(self.USER, 'bad_value') + + def testVerifyValidToken(self): + id_token = self.get_id_token() + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + claims = self.TOKEN_GEN.verify_id_token(id_token) + self.assertEquals(claims['admin'], True) + self.assertEquals(claims['uid'], self.USER) + + def testVerifyValidTokenWithProjectIdEnvVariable(self): + id_token = self.get_id_token() + gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) + try: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = 'mg-test-1210' + app = firebase.App('test-app', {'credential': auth.Credential()}) + token_generator = auth._TokenGenerator(app) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + claims = token_generator.verify_id_token(id_token) + self.assertEquals(claims['admin'], True) + self.assertEquals(claims['uid'], self.USER) + finally: + if gcloud_project: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project + else: + del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] + + def testVerifyTokenWithoutProjectId(self): + id_token = self.get_id_token() + if os.environ.has_key(auth.GCLOUD_PROJECT_ENV_VAR): + del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] + + app = firebase.App('test-app', {'credential': auth.Credential()}) + token_generator = auth._TokenGenerator(app) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(ValueError): + token_generator.verify_id_token(id_token) + + def testVerifyTokenWithNoKeyId(self): + id_token = self.get_id_token(header_overrides={'kid': None}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyTokenWithWrongKeyId(self): + id_token = self.get_id_token(header_overrides={'kid': 'foo'}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(client.VerifyJwtTokenError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyTokenWithWrongAlgorithm(self): + id_token = self.get_id_token(header_overrides={'alg': 'HS256'}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyInvalidTokenWithCustomToken(self): + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + id_token = self.TOKEN_GEN.create_custom_token(self.USER) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyInvalidTokenWithBadAudience(self): + id_token = self.get_id_token({'aud': 'bad-audience'}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyInvalidTokenWithBadIssuer(self): + id_token = self.get_id_token({ + 'iss': 'https://securetoken.google.com/wrong-issuer' + }) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyInvalidTokenWithEmptySubject(self): + id_token = self.get_id_token({'sub': ''}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyInvalidTokenWithNonStringSubject(self): + id_token = self.get_id_token({'sub': 10}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyInvalidTokenWithLongSubject(self): + id_token = self.get_id_token({'sub': 'a' * 129}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyInvalidTokenWithFutureToken(self): + id_token = self.get_id_token({'iat': int(time.time()) + 1000}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyInvalidTokenWithExpiredToken(self): + id_token = self.get_id_token({ + 'iat': int(time.time()) - 10000, + 'exp': int(time.time()) - 3600 + }) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyTokenWithCertificateRequestFailure(self): + id_token = self.get_id_token() + auth._http = testutils.HttpMock(404, 'not found') + with self.assertRaises(client.VerifyJwtTokenError): + self.TOKEN_GEN.verify_id_token(id_token) + + def testVerifyNoneToken(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.verify_id_token(None) + + def testVerifyEmptyToken(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.verify_id_token('') + + def testVerifyNonStringToken(self): + for item in [True, False, 0, 1, [], {}, {'a': 1}]: + with self.assertRaises(ValueError): + self.TOKEN_GEN.verify_id_token(item) + + def testVerifyBadFormatToken(self): + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token('foobar') + + def testMalformedDeveloperClaims(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(self.USER, 'bad_value') + + +class AuthApiTest(_AbstractAuthTest): + + def setUp(self): + super(AuthApiTest, self).setUp() + firebase.initialize_app({'credential': self.CREDENTIAL}) + + def tearDown(self): + testutils.cleanup_apps() + super(AuthApiTest, self).tearDown() + + def testCustomTokenCreation(self): + token_string = auth.create_custom_token(self.USER, self.CLAIMS) + self.assertIsInstance(token_string, basestring) + self.verify_custom_token(token_string) + + def testCustomTokenCreationForNonDefaultApp(self): + app = firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') + token_string = auth.create_custom_token(self.USER, self.CLAIMS, app) + self.assertIsInstance(token_string, basestring) + self.verify_custom_token(token_string) + + def testCustomTokenCreationForUninitializedApp(self): + app = firebase.App('test-app', {'credential': self.CREDENTIAL}) + with self.assertRaises(ValueError): + auth.create_custom_token(self.USER, self.CLAIMS, app) + + def testCustomTokenCreationForUninitializedDuplicateApp(self): + firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') + app = firebase.App('test-app', {'credential': self.CREDENTIAL}) + with self.assertRaises(ValueError): + auth.create_custom_token(self.USER, self.CLAIMS, app) + + def testCustomTokenCreationForInvalidApp(self): + for app in ['foo', 1, 0, True, False, dict(), list(), tuple()]: + with self.assertRaises(ValueError): + auth.create_custom_token(self.USER, self.CLAIMS, app) + + def testVerifyIdToken(self): + id_token = self.get_id_token() + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + claims = auth.verify_id_token(id_token) + self.assertEquals(claims['admin'], True) + self.assertEquals(claims['uid'], self.USER) + + def testVerifyIdTokenForNonDefaultApp(self): + app = firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') + id_token = self.get_id_token() + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + claims = auth.verify_id_token(id_token, app) + self.assertEquals(claims['admin'], True) + self.assertEquals(claims['uid'], self.USER) + + def testVerifyIdTokenForUninitializedApp(self): + app = firebase.App('test-app', {'credential': self.CREDENTIAL}) + id_token = self.get_id_token() + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(ValueError): + auth.verify_id_token(id_token, app) + + def testVerifyIdTokenForUninitializedDuplicateApp(self): + firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') + app = firebase.App('test-app', {'credential': self.CREDENTIAL}) + id_token = self.get_id_token() + with self.assertRaises(ValueError): + auth.verify_id_token(id_token, app) + + def testVerifyIdTokenForInvalidApp(self): + id_token = self.get_id_token() + for app in ['foo', 1, 0, True, False, dict(), list(), tuple()]: + with self.assertRaises(ValueError): + auth.verify_id_token(id_token, app) + diff --git a/tests/data/private_key.pem b/tests/data/private_key.pem new file mode 100644 index 000000000..e55eb262e --- /dev/null +++ b/tests/data/private_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCQiozUlHeHrUsN +Q34VpMMWfBCStUMb2oPLnhA1LESXGAQ9HIaFzgoDPtxFMxJZ+gJZiRhpb+dmSpN9 +aWGxrBlBTWgrymPEmFRAdilMhIDgNMubPlHieD2ZUYyplZoKziiI+92LPKvG6zpE +h1DltFtnKGXinOfbcTy2zMLGcVfgpid7K8u1txegS9J7OGbqfrJPhs3NJmC4rE5J +lTdc0U+ZwFWNg82MDZpbMTni67S974msFWRGr/dcH86+8nKMii93GfMX3Vgnkq6Q +ckyFmqdBEhzdVw2PHn5bBJAoTaSVHvAHQCwAizSXEDjDEf/4RQIczZLmV+syJ3Pt +w5eYyZ4nAgMBAAECggEAaeBzhIw0enggD9kulKAaH3BFm5GXVOHdxNtFuA1zONJo +2HL6vyzL/NCg/TeJ68rMydU4wpMsB6v9GdKFs2YDIeiXs+wO5MVIgeeMHPa6iIFj +25Xs2V2GkbZAuUBSlDOrUZxdDk8k7RMFnYkZYwmYIppe935EAGSUOrsGueHwoycz +58otdgE/f08Jtwwtlmg1eAdB8YTpG+8g5GgyqPX9Pmsl/1QPkkoya4yHuu6ytkMA +nPqdqpNxczFSD9BcckmmvDPl/gMbvrX2TwQ29LCm0uwpzAVLODlF6z/IglDFJ12a +qCloWR+0GT32rJbJMKAzSm8j0iM8TKZK7tr5aDElwQKBgQDn53tc4GPekaBXoqU4 +0zTqdZGSArzFcspRFFf+F77pQheskFSvQDVuynGT0RNTxepRqvJM95tyxOUqpeR2 +kL58BJWT1EjJbGiPJWD4ZRiCX2xFPuyBa0V2IFw08ZA3k/MyRhRGgDvzro+QX2pM +wXmpqB/ByDiFQN9quO0EB7O3cQKBgQCfj0M8Df80O818dLzACEMHuY2Dajei10K6 +3ITjmYQ601/mJDgHnrGnZCxSVstY/ygyRvpQZOKEyjy8QmxW0RdYP7Q8srfGmNsQ +2tZsk+jM7Txbt/ml8+TRCz28eywtbYQVAkhx4ttjtv5nCjJ3x36Ak0v+Oaen+5zp +tJ67OZLTFwKBgCOVWVB+/dQA9GF+C2wUvGHdeGC3GtARNQoL3RSYACs6gPdxjgz2 +BTziw1qzEgwgqjutx1AYDjomDCPnII8w1omhCnKMeD6v67tLOP3kRUZ77dkSNqgF +Fbtya7OT/VUJ1p84MZQ/yPMzLcQxX9Y3ObvWmEjbuBB6S83MYlHj/KeBAoGACBSi +TA1NamDI9E+ZK4R/mImOICSl8qpCJ+J5HGmu56fCyI33BHPF/Xs2P2lD3Rr29yzf +CmlBi4YOc15NzEvEieSYBSbr5bPiDEV47IDFHnO5Rc/YZc4nPWr7UmtOfnJ4aPP3 +pUTe5XrkAWXjzmsc/ff3tkVHN1unw7IxA7xTsjsCgYAma2fYRuBGnOu3BP7AQ2xa +BdbTyLndP1N3e6BfVufqPM7yWrDCLn97xxscShQg4TYjz0BcATnP1SSsjKtUkQqk +vAHiB4Q3zWSUFFkaW6iAOJuYZI1jbI+J59C/NRWM+kCRYLLwcsJMP0++QdmKZq3E +8lhVy3sgqT14vVKsiy+LdQ== +-----END PRIVATE KEY----- diff --git a/tests/data/public_certs.json b/tests/data/public_certs.json new file mode 100644 index 000000000..76c08f074 --- /dev/null +++ b/tests/data/public_certs.json @@ -0,0 +1,5 @@ +{ + "d98d290613ae1468f7e5f5cf604ead38ca9c8358": "-----BEGIN CERTIFICATE-----\nMIIDKjCCAhKgAwIBAgIIMOpmoq9fpOIwDQYJKoZIhvcNAQEFBQAwODE2MDQGA1UE\nAxMtdGVzdC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29t\nMB4XDTE2MDMxODE4MDY1NFoXDTE2MDMyMDA3MDY1NFowODE2MDQGA1UEAxMtdGVz\ndC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwc1MDUEpn4riMUvY1eJ4kjGifzHX\neNKNuwph0bAvUJqGAcRkXl4YWdRZ0mhj9AnBzC5FdynUYgaMphZmfIrd9cXvbXNT\nljkpY77+h5TKGUbTVKnEP8Yzqm+BfGnzQ+vg5wAlD7Xz/k0yr+k6rw2vBtIBYSZw\nMCXvuQp/zwHLcm2RQgihjhUmDVaouqWRWKo8y4iXNEZUqwYi+iEUjzG2aNvb/DIC\nISbIMgpH8V0vh2BdZNY6x2UML0mC4ZGUdNCl39RFhngDVa3WIAObYD+EoxJRXlXc\nMOpyeQdjykovG2jarsRmIT/ef07fUU/U1IP2cA62+VM57z4XYENtyEbqTQIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEASRz/2FRGOqvjKjKE9l1zhiwT\nkNOm2ioYYgVrG/LFa+Jy2GcnaOW+7Al520NHGYwBCtsX6NmJrunHrVNRabPmUUVJ\nkWH1x+iD63kFJrv3AJb7+FxZBLNVuu0IjsmGlsHgnmUcRe3bHRbTmjpRfq38mbGn\nbBdAM9gOfZteAXrkfA078oDNeUdMlT0sfY8YiiZDASV/h9nfh7KSZQ+LmyKoVAs2\nP/YlvQlNfYbw9yOguxhunEnbixwprra8TYMFmXxh2nLNBqGEGzeF2bijWUGbCWRS\nUH5NSnW0LtZdkbUpZoxfMXAW5kuPebi0zpAbYLx/OhJ/i4XNKGfxOpv6xh747w==\n-----END CERTIFICATE-----\n", + "525a87bdd5d50522922e6ed2c0216fc442e83e54": "-----BEGIN CERTIFICATE-----\nMIIDKjCCAhKgAwIBAgIIBIUnv7pTIx8wDQYJKoZIhvcNAQEFBQAwODE2MDQGA1UE\nAxMtdGVzdC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29t\nMB4XDTE2MDMxOTE3NTE1NFoXDTE2MDMyMTA2NTE1NFowODE2MDQGA1UEAxMtdGVz\ndC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7beJFmTrA/T4AeMWk/IjxUlGpaxH\n6D1CYbfxEBJUqzuIe7ujaxh76ik/FPQV5WxlL1GOjW0/f5CsmrNaFmTmQbsK4BY3\n3cCd3gM8LcEtmF1I9NxxpXxrZihlfuwbEpb5NpjGPkCC+fG3gTY7qtjuO6e8pGb2\nVQQguOGXKw/YZLZRZXZ41xkQRYrs+tFw48+4YkjMsYJIxyBMiL5Q/HNAQ2IUyZwr\nuc+CMcWyPLNcnsRNXgnPXQD/GKZQnjjJ5KzQAU1vnDcufL9V5KRhb0kRxTTUjE7D\nJl3x4+J6+hbAheZFu9Fntrxie9TvQuQbEBm/437QFYZphfQli0fDjlPHSwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAQzlUGQiWiHgeBZyUsetuoTiQ\nsxzU7B1qw3la/FQrG+jRFr9GE3yjOOxi9JvX16U/ebwSHLUip8UFf/Ir6AJlt/tt\nIjBA6TOd8DysAtr4PCZrAP/m43H9w4lBWdWl1XJE2YfYQgZnorveAMUZqTo0P0pd\nFo3IsYBSTMflKv2Vqz91PPiHgyu2fk+8TYwJT57rnnkS6VzdORTIf+9ZB+J1ye9i\nQN5IgdZ/eqFiJPD8qT5jOcXelWSWqHHdGrNjQNp+z8jgMusY5/ZAlZUe55eo3I0m\nuDSPImLNkDwqY0+bBW6Fp5xi/4O+gJg3cQ+/PeIHzoFqKAlSpxQZSCziPpGfAA==\n-----END CERTIFICATE-----\n", + "d2d687bf7d14cb8a54fbd0f36bcc9c4f32fa84cf": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAeKgAwIBAgIIRKlYUHIlbRkwDQYJKoZIhvcNAQEFBQAwIDEeMBwGA1UE\nAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMB4XDTE2MDIxMDAxMzI1N1oXDTI2MDIw\nNzAxMzI1N1owIDEeMBwGA1UEAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkIqM1JR3h61LDUN+FaTDFnwQkrVD\nG9qDy54QNSxElxgEPRyGhc4KAz7cRTMSWfoCWYkYaW/nZkqTfWlhsawZQU1oK8pj\nxJhUQHYpTISA4DTLmz5R4ng9mVGMqZWaCs4oiPvdizyrxus6RIdQ5bRbZyhl4pzn\n23E8tszCxnFX4KYneyvLtbcXoEvSezhm6n6yT4bNzSZguKxOSZU3XNFPmcBVjYPN\njA2aWzE54uu0ve+JrBVkRq/3XB/OvvJyjIovdxnzF91YJ5KukHJMhZqnQRIc3VcN\njx5+WwSQKE2klR7wB0AsAIs0lxA4wxH/+EUCHM2S5lfrMidz7cOXmMmeJwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAF+V8kmeJQnvpPKlFT74BROi0\n1Eple2mSsyQbtm1kL7FJpl1AXZ4sLXXTVj3ql0LsqVawDCVtUSvDXBSHtejnh0bi\nZ0WUyEEJ38XPfXRilIaTrYP408ezowDaXxrfLhho1EjoMOPgXjksu1FyhBFoHmif\ndLJoxyA4f+8DZ8jj7ew6ZIVEmvONYgctpU72uUh36Vyl84oc9D2GByq/zYDXvVvl\nSKWYZ5+86/eGocO4sosB5QrsEdVGT2Im6mz2DUIewSyIvrDgZ5r3XyL4RXpdi8+8\n9re/meIh5pnhimU4pX9weQia8bqSPf0oZhh0uAWxO5ES7k1GwblnJfxeCZ0xDQ==\n-----END CERTIFICATE-----\n" +} diff --git a/tests/data/service_account.json b/tests/data/service_account.json new file mode 100644 index 000000000..99d89ace2 --- /dev/null +++ b/tests/data/service_account.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "mg-test-1210", + "private_key_id": "d5dedce38b8a8d20679c33a0838d954ae9c2553c", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCQiozUlHeHrUsN\nQ34VpMMWfBCStUMb2oPLnhA1LESXGAQ9HIaFzgoDPtxFMxJZ+gJZiRhpb+dmSpN9\naWGxrBlBTWgrymPEmFRAdilMhIDgNMubPlHieD2ZUYyplZoKziiI+92LPKvG6zpE\nh1DltFtnKGXinOfbcTy2zMLGcVfgpid7K8u1txegS9J7OGbqfrJPhs3NJmC4rE5J\nlTdc0U+ZwFWNg82MDZpbMTni67S974msFWRGr/dcH86+8nKMii93GfMX3Vgnkq6Q\nckyFmqdBEhzdVw2PHn5bBJAoTaSVHvAHQCwAizSXEDjDEf/4RQIczZLmV+syJ3Pt\nw5eYyZ4nAgMBAAECggEAaeBzhIw0enggD9kulKAaH3BFm5GXVOHdxNtFuA1zONJo\n2HL6vyzL/NCg/TeJ68rMydU4wpMsB6v9GdKFs2YDIeiXs+wO5MVIgeeMHPa6iIFj\n25Xs2V2GkbZAuUBSlDOrUZxdDk8k7RMFnYkZYwmYIppe935EAGSUOrsGueHwoycz\n58otdgE/f08Jtwwtlmg1eAdB8YTpG+8g5GgyqPX9Pmsl/1QPkkoya4yHuu6ytkMA\nnPqdqpNxczFSD9BcckmmvDPl/gMbvrX2TwQ29LCm0uwpzAVLODlF6z/IglDFJ12a\nqCloWR+0GT32rJbJMKAzSm8j0iM8TKZK7tr5aDElwQKBgQDn53tc4GPekaBXoqU4\n0zTqdZGSArzFcspRFFf+F77pQheskFSvQDVuynGT0RNTxepRqvJM95tyxOUqpeR2\nkL58BJWT1EjJbGiPJWD4ZRiCX2xFPuyBa0V2IFw08ZA3k/MyRhRGgDvzro+QX2pM\nwXmpqB/ByDiFQN9quO0EB7O3cQKBgQCfj0M8Df80O818dLzACEMHuY2Dajei10K6\n3ITjmYQ601/mJDgHnrGnZCxSVstY/ygyRvpQZOKEyjy8QmxW0RdYP7Q8srfGmNsQ\n2tZsk+jM7Txbt/ml8+TRCz28eywtbYQVAkhx4ttjtv5nCjJ3x36Ak0v+Oaen+5zp\ntJ67OZLTFwKBgCOVWVB+/dQA9GF+C2wUvGHdeGC3GtARNQoL3RSYACs6gPdxjgz2\nBTziw1qzEgwgqjutx1AYDjomDCPnII8w1omhCnKMeD6v67tLOP3kRUZ77dkSNqgF\nFbtya7OT/VUJ1p84MZQ/yPMzLcQxX9Y3ObvWmEjbuBB6S83MYlHj/KeBAoGACBSi\nTA1NamDI9E+ZK4R/mImOICSl8qpCJ+J5HGmu56fCyI33BHPF/Xs2P2lD3Rr29yzf\nCmlBi4YOc15NzEvEieSYBSbr5bPiDEV47IDFHnO5Rc/YZc4nPWr7UmtOfnJ4aPP3\npUTe5XrkAWXjzmsc/ff3tkVHN1unw7IxA7xTsjsCgYAma2fYRuBGnOu3BP7AQ2xa\nBdbTyLndP1N3e6BfVufqPM7yWrDCLn97xxscShQg4TYjz0BcATnP1SSsjKtUkQqk\nvAHiB4Q3zWSUFFkaW6iAOJuYZI1jbI+J59C/NRWM+kCRYLLwcsJMP0++QdmKZq3E\n8lhVy3sgqT14vVKsiy+LdQ==\n-----END PRIVATE KEY-----\n", + "client_email": "test-484@mg-test-1210.iam.gserviceaccount.com", + "client_id": "100772742249150578262", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-484%40mg-test-1210.iam.gserviceaccount.com" +} diff --git a/tests/testutils.py b/tests/testutils.py new file mode 100644 index 000000000..eb6fccbcb --- /dev/null +++ b/tests/testutils.py @@ -0,0 +1,36 @@ +import httplib2 +import os + +import firebase + + +def resource_filename(filename): + return os.path.join(os.path.dirname(__file__), 'data', filename) + + +def resource(filename): + with open(resource_filename(filename), 'r') as file_obj: + return file_obj.read() + + +def cleanup_apps(): + with firebase._apps_lock: + for name in firebase._apps.keys(): + firebase.delete_app(name) + + +class HttpMock(object): + """A mock HTTP client implementation. + + This can be used whenever an HTTP interaction needs to be mocked + for testing purposes. For example HTTP calls to fetch public key + certificates, and HTTP calls to retrieve access tokens can be + mocked using this class. + """ + + def __init__(self, status, response): + self.status = status + self.response = response + + def request(self, *args, **kwargs): + return httplib2.Response({'status': self.status}), self.response From b0a21b958d2e86512bb7ac3143e95d0ac63eaa16 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 17 Mar 2017 16:59:24 -0700 Subject: [PATCH 02/12] Added linter integration --- .pylintrc | 410 ++++++++++++++++++++++++++ .test_pylintrc | 410 ++++++++++++++++++++++++++ README.md | 48 ++++ firebase/__init__.py | 257 ++++++++--------- firebase/auth.py | 559 ++++++++++++++++++------------------ firebase/jwt.py | 226 +++++++-------- lint.sh | 28 ++ tests/app_test.py | 128 ++++----- tests/auth_test.py | 664 ++++++++++++++++++++++--------------------- tests/testutils.py | 49 ++-- 10 files changed, 1855 insertions(+), 924 deletions(-) create mode 100644 .pylintrc create mode 100644 .test_pylintrc create mode 100755 lint.sh diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..0c71c9af4 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,410 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. This option is deprecated +# and it will be removed in Pylint 2.0. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=indexing-exception,old-raise-syntax + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +bad-functions=input,apply,reduce + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]*$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]*$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]*$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]*$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main) + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=10 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.test_pylintrc b/.test_pylintrc new file mode 100644 index 000000000..bfe9124b7 --- /dev/null +++ b/.test_pylintrc @@ -0,0 +1,410 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. This option is deprecated +# and it will be removed in Pylint 2.0. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=indexing-exception,old-raise-syntax + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,protected-access + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +bad-functions=input,apply,reduce + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]*$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]*$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]*$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]*$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main) + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=10 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/README.md b/README.md index d31ee544f..99812a6f9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,50 @@ # Firebase Admin Python SDK +## Running Linters +We recommend using [pylint](https://pylint.org/) for verifying source code +format, and enforcing other Python programming best practices. Install pylint +1.6.4 or higher using pip. + +``` +sudo pip install pylint +``` + +Specify a pylint version explicitly if the above command installs an older +version. + +``` +sudo pip install pylint==1.6.4 +``` + +Once installed, you can check the version of the installed binary by running +the following command. + +``` +pylint --version +``` + +There are two pylint configuration files at the root of this repository. + * .pylintrc: Settings for validating the source files in firebase module. + * .test_pylintrc: Settings for validating the test files. This is a marginally + relaxed version of .pylintrc. + +You can run pylint directly using the above configuration files. + +``` +pylint --rcfile .pylintrc firebase +pylint --rcfile .test_pylintrc tests +``` + +Alternatively you can use the `lint.sh` bash script to invoke pylint. By default +this script will only validate the locally modified source files. To validate +all source files, pass `all` as an argument. + +``` +./lint.sh +./lint.sh all +``` + +Ideally you should not see any pylint errors or warnings when you run the linter. +This means source files are properly formatted, and the linter has not found any +issues. If you do observe any errors, take action to correct them before sending +a pull request. diff --git a/firebase/__init__.py b/firebase/__init__.py index 1681a6565..3c1d6fd0f 100644 --- a/firebase/__init__.py +++ b/firebase/__init__.py @@ -9,148 +9,149 @@ def initialize_app(options, name=_DEFAULT_APP_NAME): - """Initializes and returns a new App instance. - - Creates a new App intance using the specified options - and the app name. If an instance already exists by the same - app name a ValueError is raised. Use this function whenever - a new App instance is required. Do not directly invoke the - App constructor. - - Args: - options: A dictionary of configuration options. - name: Name of the app (optional). - - Returns: - A newly initialized instance of App. - - Raises: - ValueError: If the app name is already in use, or any of the - provided arguments are invalid. - """ - app = App(name, options) - with _apps_lock: - if app.name not in _apps: - _apps[app.name] = app - return app - - if name == _DEFAULT_APP_NAME: - raise ValueError(( - 'The default Firebase app already exists. This means you called ' - 'initialize_app() more than once without providing an app name as the ' - 'second argument. In most cases you only need to call initialize_app()' - ' once. But if you do want to initialize multiple apps, pass a second ' - 'argument to initialize_app() to give each app a unique name.')) - else: - raise ValueError(( - 'Firebase app named "{0}" already exists. This means you called ' - 'initialize_app() more than once with the same app name as the second ' - 'argument. Make sure you provide a unique name every time you call ' - 'initialize_app().').format(name)) + """Initializes and returns a new App instance. + + Creates a new App intance using the specified options + and the app name. If an instance already exists by the same + app name a ValueError is raised. Use this function whenever + a new App instance is required. Do not directly invoke the + App constructor. + + Args: + options: A dictionary of configuration options. + name: Name of the app (optional). + + Returns: + A newly initialized instance of App. + + Raises: + ValueError: If the app name is already in use, or any of the + provided arguments are invalid. + """ + app = App(name, options) + with _apps_lock: + if app.name not in _apps: + _apps[app.name] = app + return app + + if name == _DEFAULT_APP_NAME: + raise ValueError(( + 'The default Firebase app already exists. This means you called ' + 'initialize_app() more than once without providing an app name as ' + 'the second argument. In most cases you only need to call ' + 'initialize_app() once. But if you do want to initialize multiple ' + 'apps, pass a second argument to initialize_app() to give each app ' + 'a unique name.')) + else: + raise ValueError(( + 'Firebase app named "{0}" already exists. This means you called ' + 'initialize_app() more than once with the same app name as the ' + 'second argument. Make sure you provide a unique name every time ' + 'you call initialize_app().').format(name)) def delete_app(name): - """Gracefully deletes an App instance. - - Args: - name: Name of the app instance to be deleted. - - Raises: - ValueError: If the name is not a string. - """ - if not isinstance(name, basestring): - raise ValueError('Illegal app name argument type: "{}". App name ' - 'must be a string.'.format(type(name))) - with _apps_lock: - if name in _apps: - del _apps[name] - return - if name == _DEFAULT_APP_NAME: - raise ValueError( - ('The default Firebase app does not exist. Make sure to initialize the' - ' SDK by calling initialize_app().')) - else: - raise ValueError( - ('Firebase app named "{0}" does not exist. Make sure to initialize the' - ' SDK by calling initialize_app() with your app name as the second' - ' argument.').format(name)) + """Gracefully deletes an App instance. + + Args: + name: Name of the app instance to be deleted. + + Raises: + ValueError: If the name is not a string. + """ + if not isinstance(name, basestring): + raise ValueError('Illegal app name argument type: "{}". App name ' + 'must be a string.'.format(type(name))) + with _apps_lock: + if name in _apps: + del _apps[name] + return + if name == _DEFAULT_APP_NAME: + raise ValueError( + 'The default Firebase app does not exist. Make sure to initialize ' + 'the SDK by calling initialize_app().') + else: + raise ValueError( + ('Firebase app named "{0}" does not exist. Make sure to initialize ' + 'the SDK by calling initialize_app() with your app name as the ' + 'second argument.').format(name)) def get_app(name=_DEFAULT_APP_NAME): - """Retrieves an App instance by name. - - Args: - name: Name of the App instance to retrieve (optional). - - Returns: - An App instance. - - Raises: - ValueError: If the specified name is not a string, or if the specified - app does not exist. - """ - if not isinstance(name, basestring): - raise ValueError('Illegal app name argument type: "{}". App name ' - 'must be a string.'.format(type(name))) - with _apps_lock: - if name in _apps: - return _apps[name] - - if name == _DEFAULT_APP_NAME: - raise ValueError( - 'The default Firebase app does not exist. Make sure to initialize the' - ' SDK by calling initialize_app().') - else: - raise ValueError( - ('Firebase app named "{0}" does not exist. Make sure to initialize the' - ' SDK by calling initialize_app() with your app name as the second' - ' argument.').format(name)) + """Retrieves an App instance by name. + Args: + name: Name of the App instance to retrieve (optional). -class _AppOptions(object): - """A collection of configuration options for an App.""" + Returns: + An App instance. - def __init__(self, options): - if not isinstance(options, dict): - raise ValueError('Illegal Firebase app options type: {0}. Options ' - 'must be a dictionary.'.format(type(options))) - self._credential = options.get('credential', None) - if not self._credential: - raise ValueError('Options must be a dict containing at least a' - ' "credential" key.') + Raises: + ValueError: If the specified name is not a string, or if the specified + app does not exist. + """ + if not isinstance(name, basestring): + raise ValueError('Illegal app name argument type: "{}". App name ' + 'must be a string.'.format(type(name))) + with _apps_lock: + if name in _apps: + return _apps[name] + + if name == _DEFAULT_APP_NAME: + raise ValueError( + 'The default Firebase app does not exist. Make sure to initialize ' + 'the SDK by calling initialize_app().') + else: + raise ValueError( + ('Firebase app named "{0}" does not exist. Make sure to initialize ' + 'the SDK by calling initialize_app() with your app name as the ' + 'second argument.').format(name)) - @property - def credential(self): - return self._credential +class _AppOptions(object): + """A collection of configuration options for an App.""" -class App(object): - """The entry point for Firebase Python SDK. + def __init__(self, options): + if not isinstance(options, dict): + raise ValueError('Illegal Firebase app options type: {0}. Options ' + 'must be a dictionary.'.format(type(options))) + self._credential = options.get('credential', None) + if not self._credential: + raise ValueError('Options must be a dict containing at least a' + ' "credential" key.') - Represents a Firebase app, while holding the configuration and state - common to all Firebase APIs. - """ + @property + def credential(self): + return self._credential - def __init__(self, name, options): - """Constructs a new App using the provided name and options. - Args: - name: Name of the application. - options: A dictionary of configuration options. +class App(object): + """The entry point for Firebase Python SDK. - Raises: - ValueError: If an argument is None or invalid. + Represents a Firebase app, while holding the configuration and state + common to all Firebase APIs. """ - if not name or not isinstance(name, basestring): - raise ValueError('Illegal Firebase app name "{0}" provided. ' - 'App name must be a non-empty string.'.format(name)) - self._name = name - self._options = _AppOptions(options) - - @property - def name(self): - return self._name - - @property - def options(self): - return self._options + + def __init__(self, name, options): + """Constructs a new App using the provided name and options. + + Args: + name: Name of the application. + options: A dictionary of configuration options. + + Raises: + ValueError: If an argument is None or invalid. + """ + if not name or not isinstance(name, basestring): + raise ValueError('Illegal Firebase app name "{0}" provided. App ' + 'name must be a non-empty string.'.format(name)) + self._name = name + self._options = _AppOptions(options) + + @property + def name(self): + return self._name + + @property + def options(self): + return self._options diff --git a/firebase/auth.py b/firebase/auth.py index fcbb102b9..d8ac86a39 100644 --- a/firebase/auth.py +++ b/firebase/auth.py @@ -13,12 +13,15 @@ import httplib2 from oauth2client import client from oauth2client import crypt -from OpenSSL import crypto import firebase from firebase import jwt _auth_lock = threading.Lock() + +"""Provided for overriding during tests. (OAuth2 client uses a caching-enabled + HTTP client internally if none provided) +""" _http = None _AUTH_ATTRIBUTE = '_auth' @@ -26,119 +29,50 @@ def _get_initialized_app(app): - if app is None: - return firebase.get_app() - elif isinstance(app, firebase.App): - initialized_app = firebase.get_app(app.name) - if app is not initialized_app: - raise ValueError('Illegal app argument. App instance not initialized' - ' via the firebase module.') - return app - else: - raise ValueError('Illegal app argument. Argument must be of type ' - ' firebase.App, but given "{0}".'.format(type(app))) + if app is None: + return firebase.get_app() + elif isinstance(app, firebase.App): + initialized_app = firebase.get_app(app.name) + if app is not initialized_app: + raise ValueError('Illegal app argument. App instance not ' + 'initialized via the firebase module.') + return app + else: + raise ValueError('Illegal app argument. Argument must be of type ' + ' firebase.App, but given "{0}".'.format(type(app))) def _get_token_generator(app): - """Returns a _TokenGenerator instance for an App. + """Returns a _TokenGenerator instance for an App. - If the App already has a _TokenGenerator associated with it, simply returns - it. Otherwise creates a new _TokenGenerator, and adds it to the App before - returning it. + If the App already has a _TokenGenerator associated with it, simply returns + it. Otherwise creates a new _TokenGenerator, and adds it to the App before + returning it. - Args: - app: A Firebase App instance (or None to use the default App). + Args: + app: A Firebase App instance (or None to use the default App). - Returns: - A _TokenGenerator instance. + Returns: + A _TokenGenerator instance. - Raises: - ValueError: If the app argument is invalid. - """ - app = _get_initialized_app(app) - with _auth_lock: - if not hasattr(app, _AUTH_ATTRIBUTE): - setattr(app, _AUTH_ATTRIBUTE, _TokenGenerator(app)) - return getattr(app, _AUTH_ATTRIBUTE) + Raises: + ValueError: If the app argument is invalid. + """ + app = _get_initialized_app(app) + with _auth_lock: + if not hasattr(app, _AUTH_ATTRIBUTE): + setattr(app, _AUTH_ATTRIBUTE, _TokenGenerator(app)) + return getattr(app, _AUTH_ATTRIBUTE) def create_custom_token(uid, developer_claims=None, app=None): - """Builds and signs a Firebase custom auth token. - - Args: - uid: ID of the user for whom the token is created. - developer_claims: A dictionary of claims to be included in the token - (optional). - app: An App instance (optional). - - Returns: - A token string minted from the input parameters. - - Raises: - ValueError: If input parameters are invalid. - """ - token_generator = _get_token_generator(app) - return token_generator.create_custom_token(uid, developer_claims) - - -def verify_id_token(id_token, app=None): - """Verifies the signature and data for the provided JWT. - - Accepts a signed token string, verifies that it is current, and issued - to this project, and that it was correctly signed by Google. - - Args: - id_token: A string of the encoded JWT. - app: An App instance (optional). - - Returns: - A dict consisting of the key-value pairs parsed from the decoded JWT. - - Raises: - ValueError: If the input parameters are invalid, or if the App was not - initialized with a CertificateCredential. - AppIdenityError: The JWT was found to be invalid, the message will contain - details. - """ - token_generator = _get_token_generator(app) - return token_generator.verify_id_token(id_token) - - -class _TokenGenerator(object): - """Generates custom tokens, and validates ID tokens.""" - - FIREBASE_CERT_URI = ('https://www.googleapis.com/robot/v1/metadata/x509/' - 'securetoken@system.gserviceaccount.com') - - ISSUER_PREFIX = 'https://securetoken.google.com/' - - MAX_TOKEN_LIFETIME_SECONDS = 3600 # One Hour, in Seconds - FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis.com/google.identity.' - 'identitytoolkit.v1.IdentityToolkit') - - # Key names we don't allow to appear in the developer_claims. - _RESERVED_CLAIMS_ = set([ - 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', - 'exp', 'firebase', 'iat', 'iss', 'jti', 'nbf', 'nonce', 'sub' - ]) - """Provided for overriding during tests. (OAuth2 client uses a caching-enabled - HTTP client internally if none provided) - """ - - def __init__(self, app): - """Initializes FirebaseAuth from a FirebaseApp instance. - - Args: - app: A FirebaseApp instance. - """ - self._app = app - - def create_custom_token(self, uid, developer_claims=None): - """Builds and signs a FirebaseCustomAuthToken. + """Builds and signs a Firebase custom auth token. Args: uid: ID of the user for whom the token is created. - developer_claims: A dictionary of claims to be included in the token. + developer_claims: A dictionary of claims to be included in the token + (optional). + app: An App instance (optional). Returns: A token string minted from the input parameters. @@ -146,200 +80,277 @@ def create_custom_token(self, uid, developer_claims=None): Raises: ValueError: If input parameters are invalid. """ - credential = self._app.options.credential - if not isinstance(credential, CertificateCredential): - raise ValueError( - 'Must initialize Firebase App with a certificate credential' - 'to call create_custom_token().') - - if developer_claims is not None: - if not isinstance(developer_claims, dict): - raise ValueError('developer_claims must be a dictionary') - - disallowed_keys = set(developer_claims.keys()) & self._RESERVED_CLAIMS_ - if disallowed_keys: - if len(disallowed_keys) > 1: - error_message = ('Developer claims {0} are reserved and cannot be ' - 'specified.'.format(', '.join(disallowed_keys))) - else: - error_message = ('Developer claim {0} is reserved and cannot be ' - 'specified.'.format(', '.join(disallowed_keys))) - raise ValueError(error_message) - - if not uid or not isinstance(uid, basestring) or len(uid) > 128: - raise ValueError('uid must be a string between 1 and 128 characters.') - - now = int(time.time()) - payload = { - 'iss': credential.service_account_email, - 'sub': credential.service_account_email, - 'aud': self.FIREBASE_AUDIENCE, - 'uid': uid, - 'iat': now, - 'exp': now + self.MAX_TOKEN_LIFETIME_SECONDS, - } - - if developer_claims is not None: - payload['claims'] = developer_claims - - return jwt.encode(payload, credential.signer) - - def verify_id_token(self, id_token): + token_generator = _get_token_generator(app) + return token_generator.create_custom_token(uid, developer_claims) + + +def verify_id_token(id_token, app=None): """Verifies the signature and data for the provided JWT. - Accepts a signed token string, verifies that is the current, and issued + Accepts a signed token string, verifies that it is current, and issued to this project, and that it was correctly signed by Google. Args: id_token: A string of the encoded JWT. + app: An App instance (optional). Returns: A dict consisting of the key-value pairs parsed from the decoded JWT. Raises: - ValueError: The app was not initialized with a CertificateCredential + ValueError: If the input parameters are invalid, or if the App was not + initialized with a CertificateCredential. AppIdenityError: The JWT was found to be invalid, the message will contain details. """ - if not id_token or not isinstance(id_token, basestring): - raise ValueError('Illegal ID token provided: {0}. ID token ' - 'must be a non-empty string.'.format(id_token)) - - credential = self._app.options.credential - try: - project_id = credential.project_id - except AttributeError: - project_id = os.environ.get(GCLOUD_PROJECT_ENV_VAR) - - if not project_id: - raise ValueError('Must initialize app with a CertificateCredential or ' - 'set your Firebase project ID as the GCLOUD_PROJECT ' - 'environment variable to call verify_id_token().') - - header, payload = jwt.decode(id_token) - issuer = payload.get('iss') - audience = payload.get('aud') - subject = payload.get('sub') - expected_issuer = self.ISSUER_PREFIX + project_id - - project_id_match_msg = ('Make sure the ID token comes from the same' - ' Firebase project as the service account used' - ' to authenticate this SDK.') - verify_id_token_msg = ( - 'See https://firebase.google.com/docs/auth/admin/verify-id-tokens' - ' for details on how to retrieve an ID token.') - error_message = None - if not header.get('kid'): - if audience == self.FIREBASE_AUDIENCE: - error_message = ('verify_id_token() expects an ID token, but was' - ' given a custom token.') - elif header.get('alg') == 'HS256' and payload.get( - 'v') is 0 and 'uid' in payload.get('d', {}): - error_message = ('verify_id_token() expects an ID token, but was' - ' given a legacy custom token.') - else: - error_message = 'Firebase ID token has no "kid" claim.' - elif header.get('alg') != 'RS256': - error_message = ('Firebase ID token has incorrect algorithm. ' - 'Expected "RS256" but got "{0}". {1}'.format( - header.get('alg'), verify_id_token_msg)) - elif audience != project_id: - error_message = ( - 'Firebase ID token has incorrect "aud" (audience) claim. Expected ' - '"{0}" but got "{1}". {2} {3}'.format( - project_id, audience, project_id_match_msg, verify_id_token_msg)) - elif issuer != expected_issuer: - error_message = ('Firebase ID token has incorrect "iss" (issuer) claim.' - ' Expected "{0}" but got "{1}". {2} {3}'.format( - expected_issuer, issuer, project_id_match_msg, - verify_id_token_msg)) - elif subject is None or not isinstance(subject, basestring): - error_message = ('Firebase ID token has no "sub" (subject) ' - 'claim. ') + verify_id_token_msg - elif not subject: - error_message = ('Firebase ID token has an empty string "sub" (subject) ' - 'claim. ') + verify_id_token_msg - elif len(subject) > 128: - error_message = ('Firebase ID token has "sub" (subject) claim longer than' - ' 128 characters. ') + verify_id_token_msg - - if error_message: - raise crypt.AppIdentityError(error_message) - - return jwt.verify_id_token( - id_token, - self.FIREBASE_CERT_URI, - audience=project_id, - kid=header.get('kid'), - http=_http) + token_generator = _get_token_generator(app) + return token_generator.verify_id_token(id_token) -class Credential(object): - """Provides OAuth2 access tokens for accessing Firebase services. - """ +class _TokenGenerator(object): + """Generates custom tokens, and validates ID tokens.""" + + FIREBASE_CERT_URI = ('https://www.googleapis.com/robot/v1/metadata/x509/' + 'securetoken@system.gserviceaccount.com') + + ISSUER_PREFIX = 'https://securetoken.google.com/' + + MAX_TOKEN_LIFETIME_SECONDS = 3600 # One Hour, in Seconds + FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis.com/google.' + 'identity.identitytoolkit.v1.IdentityToolkit') + + # Key names we don't allow to appear in the developer_claims. + _RESERVED_CLAIMS_ = set([ + 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', + 'exp', 'firebase', 'iat', 'iss', 'jti', 'nbf', 'nonce', 'sub' + ]) + + + def __init__(self, app): + """Initializes FirebaseAuth from a FirebaseApp instance. + + Args: + app: A FirebaseApp instance. + """ + self._app = app + + def create_custom_token(self, uid, developer_claims=None): + """Builds and signs a FirebaseCustomAuthToken. + + Args: + uid: ID of the user for whom the token is created. + developer_claims: A dictionary of claims to be included in the token. + + Returns: + A token string minted from the input parameters. + + Raises: + ValueError: If input parameters are invalid. + """ + credential = self._app.options.credential + if not isinstance(credential, CertificateCredential): + raise ValueError( + 'Must initialize Firebase App with a certificate credential' + 'to call create_custom_token().') + + if developer_claims is not None: + if not isinstance(developer_claims, dict): + raise ValueError('developer_claims must be a dictionary') + + disallowed_keys = set(developer_claims.keys() + ) & self._RESERVED_CLAIMS_ + if disallowed_keys: + if len(disallowed_keys) > 1: + error_message = ('Developer claims {0} are reserved and ' + 'cannot be specified.'.format( + ', '.join(disallowed_keys))) + else: + error_message = ('Developer claim {0} is reserved and ' + 'cannot be specified.'.format( + ', '.join(disallowed_keys))) + raise ValueError(error_message) + + if not uid or not isinstance(uid, basestring) or len(uid) > 128: + raise ValueError( + 'uid must be a string between 1 and 128 characters.') + + now = int(time.time()) + payload = { + 'iss': credential.service_account_email, + 'sub': credential.service_account_email, + 'aud': self.FIREBASE_AUDIENCE, + 'uid': uid, + 'iat': now, + 'exp': now + self.MAX_TOKEN_LIFETIME_SECONDS, + } + + if developer_claims is not None: + payload['claims'] = developer_claims + + return jwt.encode(payload, credential.signer) + + def verify_id_token(self, id_token): + """Verifies the signature and data for the provided JWT. + + Accepts a signed token string, verifies that is the current, and issued + to this project, and that it was correctly signed by Google. + + Args: + id_token: A string of the encoded JWT. + + Returns: + A dict consisting of the key-value pairs parsed from the decoded JWT. + + Raises: + ValueError: The app was not initialized with a CertificateCredential + AppIdenityError: The JWT was found to be invalid, the message will + contain details. + """ + if not id_token or not isinstance(id_token, basestring): + raise ValueError('Illegal ID token provided: {0}. ID token ' + 'must be a non-empty string.'.format(id_token)) + + credential = self._app.options.credential + try: + project_id = credential.project_id + except AttributeError: + project_id = os.environ.get(GCLOUD_PROJECT_ENV_VAR) + + if not project_id: + raise ValueError('Must initialize app with a CertificateCredential ' + 'or set your Firebase project ID as the ' + 'GCLOUD_PROJECT environment variable to call ' + 'verify_id_token().') + + header, payload = jwt.decode(id_token) + issuer = payload.get('iss') + audience = payload.get('aud') + subject = payload.get('sub') + expected_issuer = self.ISSUER_PREFIX + project_id + + project_id_match_msg = ('Make sure the ID token comes from the same' + ' Firebase project as the service account used' + ' to authenticate this SDK.') + verify_id_token_msg = ( + 'See https://firebase.google.com/docs/auth/admin/verify-id-tokens' + ' for details on how to retrieve an ID token.') + error_message = None + if not header.get('kid'): + if audience == self.FIREBASE_AUDIENCE: + error_message = ('verify_id_token() expects an ID token, but ' + 'was given a custom token.') + elif header.get('alg') == 'HS256' and payload.get( + 'v') is 0 and 'uid' in payload.get('d', {}): + error_message = ('verify_id_token() expects an ID token, but ' + 'was given a legacy custom token.') + else: + error_message = 'Firebase ID token has no "kid" claim.' + elif header.get('alg') != 'RS256': + error_message = ('Firebase ID token has incorrect algorithm. ' + 'Expected "RS256" but got "{0}". {1}'.format( + header.get('alg'), verify_id_token_msg)) + elif audience != project_id: + error_message = ( + 'Firebase ID token has incorrect "aud" (audience) claim. ' + 'Expected "{0}" but got "{1}". {2} {3}'.format( + project_id, audience, project_id_match_msg, + verify_id_token_msg)) + elif issuer != expected_issuer: + error_message = ('Firebase ID token has incorrect "iss" (issuer) ' + 'claim. Expected "{0}" but got "{1}". {2} {3}' + .format(expected_issuer, issuer, + project_id_match_msg, + verify_id_token_msg)) + elif subject is None or not isinstance(subject, basestring): + error_message = ('Firebase ID token has no "sub" (subject) ' + 'claim. ') + verify_id_token_msg + elif not subject: + error_message = ('Firebase ID token has an empty string "sub" ' + '(subject) claim. ') + verify_id_token_msg + elif len(subject) > 128: + error_message = ('Firebase ID token has a "sub" (subject) ' + 'claim longer than 128 ' + 'characters. ') + verify_id_token_msg + + if error_message: + raise crypt.AppIdentityError(error_message) + + return jwt.verify_id_token( + id_token, + self.FIREBASE_CERT_URI, + audience=project_id, + kid=header.get('kid'), + http=_http) - def get_access_token(self, force_refresh=False): - """Fetches a Google OAuth2 access token using this credential instance. - Args: - force_refresh: A boolean value indicating whether to fetch a new token - or use a cached one if available. +class Credential(object): + """Provides OAuth2 access tokens for accessing Firebase services. """ - raise NotImplementedError - - def get_credential(self): - """Returns the underlying credential instance used for authentication.""" - raise NotImplementedError - -class CertificateCredential(Credential): - """A Credential initialized from a JSON keyfile.""" + def get_access_token(self, force_refresh=False): + """Fetches a Google OAuth2 access token using this credential instance. - def __init__(self, file_path): - """Initializes a credential from a certificate file. + Args: + force_refresh: A boolean value indicating whether to fetch a new token + or use a cached one if available. + """ + raise NotImplementedError - Parses the specified certificate file (service account file), and - creates a credential instance from it. + def get_credential(self): + """Returns the credential instance used for authentication.""" + raise NotImplementedError - Args: - file_path: Path to a service account certificate file. - Raises: - IOError: If the specified file doesn't exist or cannot be read. - ValueError: If an error occurs while parsing the file content. - """ - super(CertificateCredential, self).__init__() - # TODO(hkj): Clean this up once we are able to take a dependency - # TODO(hkj): on latest oauth2client. - with open(file_path) as json_keyfile: - json_data = json.load(json_keyfile) - self._project_id = json_data.get('project_id') - try: - self._signer = crypt.Signer.from_string(json_data.get('private_key')) - except crypto.Error as error: - err_type, err_value, err_traceback = sys.exc_info() - err_message = 'Failed to parse the private key string: {0}'.format(error) - raise ValueError, (err_message, err_type, err_value), err_traceback - self._service_account_email = json_data.get('client_email') - self._g_credential = client.GoogleCredentials.from_stream(file_path) - - @property - def project_id(self): - return self._project_id - - @property - def signer(self): - return self._signer - - @property - def service_account_email(self): - return self._service_account_email - - def get_access_token(self, force_refresh=False): - if force_refresh: - self._g_credential.refresh(httplib2.Http()) - token_info = self._g_credential.get_access_token() - return token_info.access_token - - def get_credential(self): - return self._g_credential +class CertificateCredential(Credential): + """A Credential initialized from a JSON keyfile.""" + + def __init__(self, file_path): + """Initializes a credential from a certificate file. + + Parses the specified certificate file (service account file), and + creates a credential instance from it. + + Args: + file_path: Path to a service account certificate file. + + Raises: + IOError: If the specified file doesn't exist or cannot be read. + ValueError: If an error occurs while parsing the file content. + """ + super(CertificateCredential, self).__init__() + # TODO(hkj): Clean this up once we are able to take a dependency + # TODO(hkj): on latest oauth2client. + with open(file_path) as json_keyfile: + json_data = json.load(json_keyfile) + self._project_id = json_data.get('project_id') + try: + self._signer = crypt.Signer.from_string( + json_data.get('private_key')) + except Exception as error: + err_type, err_value, err_traceback = sys.exc_info() + err_message = 'Failed to parse the private key string: {0}'.format( + error) + raise ValueError, (err_message, err_type, err_value), err_traceback + self._service_account_email = json_data.get('client_email') + self._g_credential = client.GoogleCredentials.from_stream(file_path) + + @property + def project_id(self): + return self._project_id + + @property + def signer(self): + return self._signer + + @property + def service_account_email(self): + return self._service_account_email + + def get_access_token(self, force_refresh=False): + if force_refresh: + self._g_credential.refresh(httplib2.Http()) + token_info = self._g_credential.get_access_token() + return token_info.access_token + + def get_credential(self): + return self._g_credential diff --git a/firebase/jwt.py b/firebase/jwt.py index e71723f55..d0d45bcbd 100644 --- a/firebase/jwt.py +++ b/firebase/jwt.py @@ -11,139 +11,139 @@ import json import httplib2 +import six + from oauth2client import client from oauth2client import crypt -import six try: - # Newer versions of oauth2client (> v1.4) - # pylint: disable=g-import-not-at-top - from oauth2client import transport - _cached_http = httplib2.Http(transport.MemoryCache()) + # Newer versions of oauth2client (> v1.4) + # pylint: disable=g-import-not-at-top + from oauth2client import transport + _cached_http = httplib2.Http(transport.MemoryCache()) except ImportError: - # Older versions of oauth2client (<= v1.4) - _cached_http = httplib2.Http(client.MemoryCache()) + # Older versions of oauth2client (<= v1.4) + _cached_http = httplib2.Http(client.MemoryCache()) def _to_bytes(value, encoding='ascii'): - result = (value.encode(encoding) - if isinstance(value, six.text_type) else value) - if isinstance(result, six.binary_type): - return result - else: - raise ValueError('{0!r} could not be converted to bytes'.format(value)) + result = (value.encode(encoding) + if isinstance(value, six.text_type) else value) + if isinstance(result, six.binary_type): + return result + else: + raise ValueError('{0!r} could not be converted to bytes'.format(value)) def _urlsafe_b64encode(raw_bytes): - raw_bytes = _to_bytes(raw_bytes, encoding='utf-8') - return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=') + raw_bytes = _to_bytes(raw_bytes, encoding='utf-8') + return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=') def _urlsafe_b64decode(b64string): - b64string = _to_bytes(b64string) - padded = b64string + b'=' * (4 - len(b64string) % 4) - return base64.urlsafe_b64decode(padded) + b64string = _to_bytes(b64string) + padded = b64string + b'=' * (4 - len(b64string) % 4) + return base64.urlsafe_b64decode(padded) def encode(payload, signer, headers=None): - """Encodes the provided payload into a signed JWT. - - Creates a signed JWT from the given dictionary payload of claims. - By default this function only adds the 'typ' and 'alg' headers to - the encoded JWT. The 'headers' argument can be used to set additional - JWT headers, and override the defaults. This function provides the - bare minimal token encoding and signing functionality. Any validations - on individual claims should be performed by the caller. - - Args: - payload: A dictionary of claims. - signer: An oauth2client.crypt.Signer instance for signing tokens. - headers: An dictionary of headers (optional). - - Returns: - A signed JWT token as a string - """ - header = {'typ': 'JWT', 'alg': 'RS256'} - if headers: - header.update(headers) - segments = [ - _urlsafe_b64encode(json.dumps(header, separators=(',', ':'))), - _urlsafe_b64encode(json.dumps(payload, separators=(',', ':'))), - ] - signing_input = b'.'.join(segments) - signature = signer.sign(signing_input) - segments.append(_urlsafe_b64encode(signature)) - return b'.'.join(segments) + """Encodes the provided payload into a signed JWT. + + Creates a signed JWT from the given dictionary payload of claims. + By default this function only adds the 'typ' and 'alg' headers to + the encoded JWT. The 'headers' argument can be used to set additional + JWT headers, and override the defaults. This function provides the + bare minimal token encoding and signing functionality. Any validations + on individual claims should be performed by the caller. + + Args: + payload: A dictionary of claims. + signer: An oauth2client.crypt.Signer instance for signing tokens. + headers: An dictionary of headers (optional). + + Returns: + A signed JWT token as a string + """ + header = {'typ': 'JWT', 'alg': 'RS256'} + if headers: + header.update(headers) + segments = [ + _urlsafe_b64encode(json.dumps(header, separators=(',', ':'))), + _urlsafe_b64encode(json.dumps(payload, separators=(',', ':'))), + ] + signing_input = b'.'.join(segments) + signature = signer.sign(signing_input) + segments.append(_urlsafe_b64encode(signature)) + return b'.'.join(segments) def decode(token): - """Decodes the provided JWT into dictionaries. - - Parses the provided token and extracts its header values and claims. - Note that this function does not perform any verification on the - token content. Nor does it attempt to verify the token signature. - Th only validation it performs is for the proper formatting/encoding - of the JWT token, which is necessary to parse it. Simply use this - function to unpack, and inspect the contents of a JWT. - - Args: - token: A signed JWT token as a string. - - Returns: - A 2-tuple where the first element is a dictionary of JWT headers, - and the second element is a dictionary of payload claims. - - Raises: - AppIdentityError: If the token is malformed or badly formatted - """ - if token.count(b'.') != 2: - raise crypt.AppIdentityError(('Wrong number of segments' - ' in token: {0}').format(token)) - header, payload, _ = token.split(b'.') - header_dict = json.loads(_urlsafe_b64decode(header).decode('utf-8')) - payload_dict = json.loads(_urlsafe_b64decode(payload).decode('utf-8')) - return (header_dict, payload_dict) + """Decodes the provided JWT into dictionaries. + + Parses the provided token and extracts its header values and claims. + Note that this function does not perform any verification on the + token content. Nor does it attempt to verify the token signature. + Th only validation it performs is for the proper formatting/encoding + of the JWT token, which is necessary to parse it. Simply use this + function to unpack, and inspect the contents of a JWT. + + Args: + token: A signed JWT token as a string. + + Returns: + A 2-tuple where the first element is a dictionary of JWT headers, + and the second element is a dictionary of payload claims. + + Raises: + AppIdentityError: If the token is malformed or badly formatted + """ + if token.count(b'.') != 2: + raise crypt.AppIdentityError(('Wrong number of segments' + ' in token: {0}').format(token)) + header, payload, _ = token.split(b'.') + header_dict = json.loads(_urlsafe_b64decode(header).decode('utf-8')) + payload_dict = json.loads(_urlsafe_b64decode(payload).decode('utf-8')) + return (header_dict, payload_dict) def verify_id_token(id_token, cert_uri, audience=None, kid=None, http=None): - """Verifies the provided ID token. - - Checks for token integrity by verifying its signature against - a set of public key certificates. Certificates are downloaded - from cert_uri, and cached according to the HTTP cache control - requirements. If provided, the audience and kid fields of the - ID token are also validated. - - Args: - id_token: JWT ID token to be validated. - cert_uri: A URI string pointing to public key certificates. - audience: Audience string that should be present in the token. - kid: JWT key ID header to locate the public key certificate. - http: An httplib2 HTTP client instance. - - Returns: - A dictionary of claims extracted from the ID token. - - Raises: - ValueError: Certificate URI is None or empty. - AppIdentityError: Token integrity check failed. - VerifyJwtTokenError: Failed to load public keys or invalid kid header. - """ - if not cert_uri: - raise ValueError('Certificate URI is required') - if not http: - http = _cached_http - resp, content = http.request(cert_uri) - if resp.status != 200: - raise client.VerifyJwtTokenError( - ('Failed to load public key' - ' certificates from URL "{0}". Received HTTP status code {1}.').format( - cert_uri, resp.status)) - certs = json.loads(content.decode('utf-8')) - if kid and not certs.has_key(kid): - raise client.VerifyJwtTokenError( - ('Firebase ID token has "kid" claim which does' - ' not correspond to a known public key. Most' - ' likely the ID token is expired, so get a' - ' fresh token from your client app and try again.')) - return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) + """Verifies the provided ID token. + + Checks for token integrity by verifying its signature against + a set of public key certificates. Certificates are downloaded + from cert_uri, and cached according to the HTTP cache control + requirements. If provided, the audience and kid fields of the + ID token are also validated. + + Args: + id_token: JWT ID token to be validated. + cert_uri: A URI string pointing to public key certificates. + audience: Audience string that should be present in the token. + kid: JWT key ID header to locate the public key certificate. + http: An httplib2 HTTP client instance. + + Returns: + A dictionary of claims extracted from the ID token. + + Raises: + ValueError: Certificate URI is None or empty. + AppIdentityError: Token integrity check failed. + VerifyJwtTokenError: Failed to load public keys or invalid kid header. + """ + if not cert_uri: + raise ValueError('Certificate URI is required') + if not http: + http = _cached_http + resp, content = http.request(cert_uri) + if resp.status != 200: + raise client.VerifyJwtTokenError( + ('Failed to load public key certificates from URL "{0}". Received ' + 'HTTP status code {1}.').format(cert_uri, resp.status)) + certs = json.loads(content.decode('utf-8')) + if kid and not certs.has_key(kid): + raise client.VerifyJwtTokenError( + 'Firebase ID token has "kid" claim which does' + ' not correspond to a known public key. Most' + ' likely the ID token is expired, so get a' + ' fresh token from your client app and try again.') + return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) diff --git a/lint.sh b/lint.sh new file mode 100755 index 000000000..9f17fea52 --- /dev/null +++ b/lint.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +function lintAllFiles () { + files=`find $1 -name *.py` + for f in $files + do + echo "Running linter on $f" + pylint --rcfile $2 $f + done +} + +function lintChangedFiles () { + files=`git status -s $1 | awk '{print $2}' | grep .py$` + for f in $files + do + echo "Running linter on $f" + pylint --rcfile $2 $f + done +} + +if [[ $1 = "all" ]] +then + lintAllFiles firebase .pylintrc + lintAllFiles tests .test_pylintrc +else + lintChangedFiles firebase .pylintrc + lintChangedFiles tests .test_pylintrc +fi diff --git a/tests/app_test.py b/tests/app_test.py index 0e08b1e5d..230567679 100644 --- a/tests/app_test.py +++ b/tests/app_test.py @@ -3,70 +3,70 @@ import firebase from firebase import auth -import testutils +from tests import testutils class FirebaseAppTest(unittest.TestCase): - - SERVICE_ACCOUNT_PATH = 'service_account.json' - CREDENTIAL = auth.CertificateCredential( - testutils.resource_filename(SERVICE_ACCOUNT_PATH)) - OPTIONS = {'credential': CREDENTIAL} - - def tearDown(self): - testutils.cleanup_apps() - - def testDefaultAppInit(self): - app = firebase.initialize_app(self.OPTIONS) - self.assertEquals(firebase._DEFAULT_APP_NAME, app.name) - self.assertIs(self.CREDENTIAL, app.options.credential) - with self.assertRaises(ValueError): - firebase.initialize_app(self.OPTIONS) - - def testNonDefaultAppInit(self): - app = firebase.initialize_app(self.OPTIONS, 'myApp') - self.assertEquals('myApp', app.name) - self.assertIs(self.CREDENTIAL, app.options.credential) - with self.assertRaises(ValueError): - firebase.initialize_app(self.OPTIONS, 'myApp') - - def testAppInitWithEmptyOptions(self): - with self.assertRaises(ValueError): - firebase.initialize_app({}) - - def testAppInitWithNoCredential(self): - options = {'key': 'value'} - with self.assertRaises(ValueError): - firebase.initialize_app(options) - - def testAppInitWithInvalidOptions(self): - for options in [None, 0, 1, 'foo', list(), tuple(), True, False]: - with self.assertRaises(ValueError): - firebase.initialize_app(options) - - def testAppInitWithInvalidName(self): - for name in [None, '', 0, 1, dict(), list(), tuple(), True, False]: - with self.assertRaises(ValueError): - firebase.initialize_app(self.OPTIONS, name) - - def testDefaultAppGet(self): - app = firebase.initialize_app(self.OPTIONS) - self.assertIs(app, firebase.get_app()) - - def testNonDefaultAppGet(self): - app = firebase.initialize_app(self.OPTIONS, 'myApp') - self.assertIs(app, firebase.get_app('myApp')) - - def testNonExistingDefaultAppGet(self): - with self.assertRaises(ValueError): - self.assertIsNone(firebase.get_app()) - - def testNonExistingAppGet(self): - with self.assertRaises(ValueError): - self.assertIsNone(firebase.get_app('myApp')) - - def testAppGetWithInvalidName(self): - for name in [None, '', 0, 1, dict(), list(), tuple(), True, False]: - with self.assertRaises(ValueError): - firebase.initialize_app(self.OPTIONS, name) - + """Test cases for App initialization and life cycle.""" + + SERVICE_ACCOUNT_PATH = 'service_account.json' + CREDENTIAL = auth.CertificateCredential( + testutils.resource_filename(SERVICE_ACCOUNT_PATH)) + OPTIONS = {'credential': CREDENTIAL} + + def tearDown(self): + testutils.cleanup_apps() + + def test_default_app_init(self): + app = firebase.initialize_app(self.OPTIONS) + self.assertEquals(firebase._DEFAULT_APP_NAME, app.name) + self.assertIs(self.CREDENTIAL, app.options.credential) + with self.assertRaises(ValueError): + firebase.initialize_app(self.OPTIONS) + + def test_non_default_app_init(self): + app = firebase.initialize_app(self.OPTIONS, 'myApp') + self.assertEquals('myApp', app.name) + self.assertIs(self.CREDENTIAL, app.options.credential) + with self.assertRaises(ValueError): + firebase.initialize_app(self.OPTIONS, 'myApp') + + def test_app_init_with_empty_options(self): + with self.assertRaises(ValueError): + firebase.initialize_app({}) + + def test_app_init_with_no_credential(self): + options = {'key': 'value'} + with self.assertRaises(ValueError): + firebase.initialize_app(options) + + def test_app_init_with_invalid_options(self): + for options in [None, 0, 1, 'foo', list(), tuple(), True, False]: + with self.assertRaises(ValueError): + firebase.initialize_app(options) + + def test_app_init_with_invalid_name(self): + for name in [None, '', 0, 1, dict(), list(), tuple(), True, False]: + with self.assertRaises(ValueError): + firebase.initialize_app(self.OPTIONS, name) + + def test_default_app_get(self): + app = firebase.initialize_app(self.OPTIONS) + self.assertIs(app, firebase.get_app()) + + def test_non_default_app_get(self): + app = firebase.initialize_app(self.OPTIONS, 'myApp') + self.assertIs(app, firebase.get_app('myApp')) + + def test_non_existing_default_app_get(self): + with self.assertRaises(ValueError): + self.assertIsNone(firebase.get_app()) + + def test_non_existing_app_get(self): + with self.assertRaises(ValueError): + self.assertIsNone(firebase.get_app('myApp')) + + def test_app_get_with_invalid_name(self): + for name in [None, '', 0, 1, dict(), list(), tuple(), True, False]: + with self.assertRaises(ValueError): + firebase.initialize_app(self.OPTIONS, name) diff --git a/tests/auth_test.py b/tests/auth_test.py index 56594d089..f0549299f 100644 --- a/tests/auth_test.py +++ b/tests/auth_test.py @@ -9,336 +9,352 @@ import firebase from firebase import auth from firebase import jwt -import testutils +from tests import testutils class _AbstractAuthTest(unittest.TestCase): - """Super class for auth-related tests. - - Defines constants used in auth-related tests, and provides a method for - asserting the validity of custom tokens. - """ - SERVICE_ACCOUNT_EMAIL = 'test-484@mg-test-1210.iam.gserviceaccount.com' - PROJECT_ID = 'test-484' - CLIENT_CERT_URL = ('https://www.googleapis.com/robot/v1/metadata/x509/' - 'test-484%40mg-test-1210.iam.gserviceaccount.com') - - FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis' - '.com/google.identity.identitytoolkit.' - 'v1.IdentityToolkit') - USER = 'user1' - ISSUER = 'test-484@mg-test-1210.iam.gserviceaccount.com' - CLAIMS = {'one': 2, 'three': 'four'} - - CREDENTIAL = auth.CertificateCredential( - testutils.resource_filename('service_account.json')) - PUBLIC_CERTS = testutils.resource('public_certs.json') - PRIVATE_KEY = testutils.resource('private_key.pem') - - def verify_custom_token(self, custom_token, verify_claims=True): - token = client.verify_id_token( - custom_token, - self.FIREBASE_AUDIENCE, - http=testutils.HttpMock(200, self.PUBLIC_CERTS), - cert_uri=self.CLIENT_CERT_URL) - self.assertEquals(token['uid'], self.USER) - self.assertEquals(token['iss'], self.SERVICE_ACCOUNT_EMAIL) - self.assertEquals(token['sub'], self.SERVICE_ACCOUNT_EMAIL) - if verify_claims: - self.assertEquals(token['claims']['one'], self.CLAIMS['one']) - self.assertEquals(token['claims']['three'], self.CLAIMS['three']) - - def _merge_jwt_claims(self, defaults, overrides): - defaults.update(overrides) - for k, v in overrides.items(): - if v is None: - del defaults[k] - return defaults - - def get_id_token(self, payload_overrides=None, header_overrides=None): - signer = crypt.Signer.from_string(self.PRIVATE_KEY) - headers = { - 'kid': 'd98d290613ae1468f7e5f5cf604ead38ca9c8358' - } - payload = { - 'aud': 'mg-test-1210', - 'iss': 'https://securetoken.google.com/mg-test-1210', - 'iat': int(time.time()) - 100, - 'exp': int(time.time()) + 3600, - 'sub': '1234567890', - 'uid': self.USER, - 'admin': True, - } - if header_overrides: - headers = self._merge_jwt_claims(headers, header_overrides) - if payload_overrides: - payload = self._merge_jwt_claims(payload, payload_overrides) - return jwt.encode(payload, signer, headers=headers) + """Super class for auth-related tests. + + Defines constants used in auth-related tests, and provides a method for + asserting the validity of custom tokens. + """ + SERVICE_ACCOUNT_EMAIL = 'test-484@mg-test-1210.iam.gserviceaccount.com' + PROJECT_ID = 'test-484' + CLIENT_CERT_URL = ('https://www.googleapis.com/robot/v1/metadata/x509/' + 'test-484%40mg-test-1210.iam.gserviceaccount.com') + + FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis' + '.com/google.identity.identitytoolkit.' + 'v1.IdentityToolkit') + USER = 'user1' + ISSUER = 'test-484@mg-test-1210.iam.gserviceaccount.com' + CLAIMS = {'one': 2, 'three': 'four'} + + CREDENTIAL = auth.CertificateCredential( + testutils.resource_filename('service_account.json')) + PUBLIC_CERTS = testutils.resource('public_certs.json') + PRIVATE_KEY = testutils.resource('private_key.pem') + + def verify_custom_token(self, custom_token, verify_claims=True): + token = client.verify_id_token( + custom_token, + self.FIREBASE_AUDIENCE, + http=testutils.HttpMock(200, self.PUBLIC_CERTS), + cert_uri=self.CLIENT_CERT_URL) + self.assertEquals(token['uid'], self.USER) + self.assertEquals(token['iss'], self.SERVICE_ACCOUNT_EMAIL) + self.assertEquals(token['sub'], self.SERVICE_ACCOUNT_EMAIL) + if verify_claims: + self.assertEquals(token['claims']['one'], self.CLAIMS['one']) + self.assertEquals(token['claims']['three'], self.CLAIMS['three']) + + def _merge_jwt_claims(self, defaults, overrides): + defaults.update(overrides) + for key, value in overrides.items(): + if value is None: + del defaults[key] + return defaults + + def get_id_token(self, payload_overrides=None, header_overrides=None): + """Generates a signed ID token for testing. + + Args: + payload_overrides: A dictionary of overrides for payload fields + header_overrides: A dictionary of overrides for header fields + + Returns: + A signed JWT ID token string. + """ + signer = crypt.Signer.from_string(self.PRIVATE_KEY) + headers = { + 'kid': 'd98d290613ae1468f7e5f5cf604ead38ca9c8358' + } + payload = { + 'aud': 'mg-test-1210', + 'iss': 'https://securetoken.google.com/mg-test-1210', + 'iat': int(time.time()) - 100, + 'exp': int(time.time()) + 3600, + 'sub': '1234567890', + 'uid': self.USER, + 'admin': True, + } + if header_overrides: + headers = self._merge_jwt_claims(headers, header_overrides) + if payload_overrides: + payload = self._merge_jwt_claims(payload, payload_overrides) + return jwt.encode(payload, signer, headers=headers) class TokenGeneratorTest(_AbstractAuthTest): - - APP = firebase.App('test-app', {'credential': _AbstractAuthTest.CREDENTIAL}) - TOKEN_GEN = auth._TokenGenerator(APP) - - def testCustomTokenCreation(self): - token_string = self.TOKEN_GEN.create_custom_token(self.USER, self.CLAIMS) - self.assertIsInstance(token_string, basestring) - self.verify_custom_token(token_string) - - def testCustomTokenCreationWithCorrectHeader(self): - token_string = self.TOKEN_GEN.create_custom_token(self.USER, self.CLAIMS) - header, _ = jwt.decode(token_string) - self.assertEquals('JWT', header.get('typ')) - self.assertEquals('RS256', header.get('alg')) - - def testCustomTokenCreationWithoutDevClaims(self): - token_string = self.TOKEN_GEN.create_custom_token(self.USER) - self.verify_custom_token(token_string, False) - - def testCustomTokenCreationWithEmptyDevClaims(self): - token_string = self.TOKEN_GEN.create_custom_token(self.USER, {}) - self.verify_custom_token(token_string, False) - - def testCustomTokenCreationWithNoUid(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(None) - - def testCustomTokenCreationWithEmptyUid(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token('') - - def testCustomTokenCreationWithLongUid(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token('x' * 129) - - def testCustomTokenCreationWithNonStringUid(self): - for item in [True, False, 0, 1, [], {}, {'a': 1}]: - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(item) - - def testCustomTokenCreationWithBadClaims(self): - for item in [True, False, 0, 1, 'foo', [], (1, 2)]: - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token('user1', item) - - def testCustomTokenCreationWithNonCertCredential(self): - app = firebase.initialize_app({'credential': auth.Credential()}, 'test-app') - token_generator = auth._TokenGenerator(app) - with self.assertRaises(ValueError): - token_generator.create_custom_token(self.USER) - - def testCustomTokenCreationFailsWithReservedClaim(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(self.USER, {'sub': '1234'}) - - def testCustomTokenCreationWithMalformedDeveloperClaims(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(self.USER, 'bad_value') - - def testVerifyValidToken(self): - id_token = self.get_id_token() - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - claims = self.TOKEN_GEN.verify_id_token(id_token) - self.assertEquals(claims['admin'], True) - self.assertEquals(claims['uid'], self.USER) - - def testVerifyValidTokenWithProjectIdEnvVariable(self): - id_token = self.get_id_token() - gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) - try: - os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = 'mg-test-1210' - app = firebase.App('test-app', {'credential': auth.Credential()}) - token_generator = auth._TokenGenerator(app) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - claims = token_generator.verify_id_token(id_token) - self.assertEquals(claims['admin'], True) - self.assertEquals(claims['uid'], self.USER) - finally: - if gcloud_project: - os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project - else: - del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] - - def testVerifyTokenWithoutProjectId(self): - id_token = self.get_id_token() - if os.environ.has_key(auth.GCLOUD_PROJECT_ENV_VAR): - del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] - - app = firebase.App('test-app', {'credential': auth.Credential()}) - token_generator = auth._TokenGenerator(app) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(ValueError): - token_generator.verify_id_token(id_token) - - def testVerifyTokenWithNoKeyId(self): - id_token = self.get_id_token(header_overrides={'kid': None}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyTokenWithWrongKeyId(self): - id_token = self.get_id_token(header_overrides={'kid': 'foo'}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(client.VerifyJwtTokenError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyTokenWithWrongAlgorithm(self): - id_token = self.get_id_token(header_overrides={'alg': 'HS256'}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyInvalidTokenWithCustomToken(self): - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - id_token = self.TOKEN_GEN.create_custom_token(self.USER) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyInvalidTokenWithBadAudience(self): - id_token = self.get_id_token({'aud': 'bad-audience'}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyInvalidTokenWithBadIssuer(self): - id_token = self.get_id_token({ - 'iss': 'https://securetoken.google.com/wrong-issuer' - }) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyInvalidTokenWithEmptySubject(self): - id_token = self.get_id_token({'sub': ''}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyInvalidTokenWithNonStringSubject(self): - id_token = self.get_id_token({'sub': 10}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyInvalidTokenWithLongSubject(self): - id_token = self.get_id_token({'sub': 'a' * 129}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyInvalidTokenWithFutureToken(self): - id_token = self.get_id_token({'iat': int(time.time()) + 1000}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyInvalidTokenWithExpiredToken(self): - id_token = self.get_id_token({ - 'iat': int(time.time()) - 10000, - 'exp': int(time.time()) - 3600 - }) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyTokenWithCertificateRequestFailure(self): - id_token = self.get_id_token() - auth._http = testutils.HttpMock(404, 'not found') - with self.assertRaises(client.VerifyJwtTokenError): - self.TOKEN_GEN.verify_id_token(id_token) - - def testVerifyNoneToken(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.verify_id_token(None) - - def testVerifyEmptyToken(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.verify_id_token('') - - def testVerifyNonStringToken(self): - for item in [True, False, 0, 1, [], {}, {'a': 1}]: - with self.assertRaises(ValueError): - self.TOKEN_GEN.verify_id_token(item) - - def testVerifyBadFormatToken(self): - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token('foobar') - - def testMalformedDeveloperClaims(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(self.USER, 'bad_value') + """Test cases for the firebase.auth._TokenGenerator class.""" + + APP = firebase.App( + 'test-app', {'credential': _AbstractAuthTest.CREDENTIAL}) + TOKEN_GEN = auth._TokenGenerator(APP) + + def test_custom_token_creation(self): + token_string = self.TOKEN_GEN.create_custom_token( + self.USER, self.CLAIMS) + self.assertIsInstance(token_string, basestring) + self.verify_custom_token(token_string) + + def test_custom_token_creation_with_correct_header(self): + token_string = self.TOKEN_GEN.create_custom_token( + self.USER, self.CLAIMS) + header, _ = jwt.decode(token_string) + self.assertEquals('JWT', header.get('typ')) + self.assertEquals('RS256', header.get('alg')) + + def test_custom_token_creation_without_dev_claims(self): + token_string = self.TOKEN_GEN.create_custom_token(self.USER) + self.verify_custom_token(token_string, False) + + def test_custom_token_creation_with_empty_dev_claims(self): + token_string = self.TOKEN_GEN.create_custom_token(self.USER, {}) + self.verify_custom_token(token_string, False) + + def test_custom_token_creation_with_no_uid(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(None) + + def test_custom_token_creation_with_empty_uid(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token('') + + def test_custom_token_creation_with_long_uid(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token('x' * 129) + + def test_custom_token_creation_with_non_string_uid(self): + for item in [True, False, 0, 1, [], {}, {'a': 1}]: + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(item) + + def test_custom_token_creation_with_bad_claims(self): + for item in [True, False, 0, 1, 'foo', [], (1, 2)]: + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token('user1', item) + + def test_custom_token_creation_with_non_cert_credential(self): + app = firebase.initialize_app( + {'credential': auth.Credential()}, 'test-app') + token_generator = auth._TokenGenerator(app) + with self.assertRaises(ValueError): + token_generator.create_custom_token(self.USER) + + def test_custom_token_creation_fails_with_reserved_claim(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(self.USER, {'sub': '1234'}) + + def test_custom_token_creation_with_malformed_developer_claims(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(self.USER, 'bad_value') + + def test_verify_valid_token(self): + id_token = self.get_id_token() + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + claims = self.TOKEN_GEN.verify_id_token(id_token) + self.assertEquals(claims['admin'], True) + self.assertEquals(claims['uid'], self.USER) + + def test_verify_valid_token_with_project_id_env_var(self): + id_token = self.get_id_token() + gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) + try: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = 'mg-test-1210' + app = firebase.App('test-app', {'credential': auth.Credential()}) + token_generator = auth._TokenGenerator(app) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + claims = token_generator.verify_id_token(id_token) + self.assertEquals(claims['admin'], True) + self.assertEquals(claims['uid'], self.USER) + finally: + if gcloud_project: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project + else: + del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] + + def test_verify_token_without_project_id(self): + id_token = self.get_id_token() + if os.environ.has_key(auth.GCLOUD_PROJECT_ENV_VAR): + del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] + + app = firebase.App('test-app', {'credential': auth.Credential()}) + token_generator = auth._TokenGenerator(app) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(ValueError): + token_generator.verify_id_token(id_token) + + def test_verify_token_without_key_id(self): + id_token = self.get_id_token(header_overrides={'kid': None}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_wrong_key_id(self): + id_token = self.get_id_token(header_overrides={'kid': 'foo'}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(client.VerifyJwtTokenError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_wrong_algorithm(self): + id_token = self.get_id_token(header_overrides={'alg': 'HS256'}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_custom_token(self): + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + id_token = self.TOKEN_GEN.create_custom_token(self.USER) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_bad_audience(self): + id_token = self.get_id_token({'aud': 'bad-audience'}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_bad_issuer(self): + id_token = self.get_id_token({ + 'iss': 'https://securetoken.google.com/wrong-issuer' + }) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_empty_subject(self): + id_token = self.get_id_token({'sub': ''}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_non_string_subject(self): + id_token = self.get_id_token({'sub': 10}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_long_subject(self): + id_token = self.get_id_token({'sub': 'a' * 129}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_future_token(self): + id_token = self.get_id_token({'iat': int(time.time()) + 1000}) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_expired_token(self): + id_token = self.get_id_token({ + 'iat': int(time.time()) - 10000, + 'exp': int(time.time()) - 3600 + }) + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_token_with_certificate_request_failure(self): + id_token = self.get_id_token() + auth._http = testutils.HttpMock(404, 'not found') + with self.assertRaises(client.VerifyJwtTokenError): + self.TOKEN_GEN.verify_id_token(id_token) + + def test_verify_non_token(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.verify_id_token(None) + + def test_verify_empty_token(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.verify_id_token('') + + def test_verify_non_string_token(self): + for item in [True, False, 0, 1, [], {}, {'a': 1}]: + with self.assertRaises(ValueError): + self.TOKEN_GEN.verify_id_token(item) + + def test_verify_bad_format_token(self): + with self.assertRaises(crypt.AppIdentityError): + self.TOKEN_GEN.verify_id_token('foobar') + + def test_malformed_developer_claims(self): + with self.assertRaises(ValueError): + self.TOKEN_GEN.create_custom_token(self.USER, 'bad_value') class AuthApiTest(_AbstractAuthTest): - - def setUp(self): - super(AuthApiTest, self).setUp() - firebase.initialize_app({'credential': self.CREDENTIAL}) - - def tearDown(self): - testutils.cleanup_apps() - super(AuthApiTest, self).tearDown() - - def testCustomTokenCreation(self): - token_string = auth.create_custom_token(self.USER, self.CLAIMS) - self.assertIsInstance(token_string, basestring) - self.verify_custom_token(token_string) - - def testCustomTokenCreationForNonDefaultApp(self): - app = firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') - token_string = auth.create_custom_token(self.USER, self.CLAIMS, app) - self.assertIsInstance(token_string, basestring) - self.verify_custom_token(token_string) - - def testCustomTokenCreationForUninitializedApp(self): - app = firebase.App('test-app', {'credential': self.CREDENTIAL}) - with self.assertRaises(ValueError): - auth.create_custom_token(self.USER, self.CLAIMS, app) - - def testCustomTokenCreationForUninitializedDuplicateApp(self): - firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') - app = firebase.App('test-app', {'credential': self.CREDENTIAL}) - with self.assertRaises(ValueError): - auth.create_custom_token(self.USER, self.CLAIMS, app) - - def testCustomTokenCreationForInvalidApp(self): - for app in ['foo', 1, 0, True, False, dict(), list(), tuple()]: - with self.assertRaises(ValueError): - auth.create_custom_token(self.USER, self.CLAIMS, app) - - def testVerifyIdToken(self): - id_token = self.get_id_token() - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - claims = auth.verify_id_token(id_token) - self.assertEquals(claims['admin'], True) - self.assertEquals(claims['uid'], self.USER) - - def testVerifyIdTokenForNonDefaultApp(self): - app = firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') - id_token = self.get_id_token() - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - claims = auth.verify_id_token(id_token, app) - self.assertEquals(claims['admin'], True) - self.assertEquals(claims['uid'], self.USER) - - def testVerifyIdTokenForUninitializedApp(self): - app = firebase.App('test-app', {'credential': self.CREDENTIAL}) - id_token = self.get_id_token() - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(ValueError): - auth.verify_id_token(id_token, app) - - def testVerifyIdTokenForUninitializedDuplicateApp(self): - firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') - app = firebase.App('test-app', {'credential': self.CREDENTIAL}) - id_token = self.get_id_token() - with self.assertRaises(ValueError): - auth.verify_id_token(id_token, app) - - def testVerifyIdTokenForInvalidApp(self): - id_token = self.get_id_token() - for app in ['foo', 1, 0, True, False, dict(), list(), tuple()]: - with self.assertRaises(ValueError): - auth.verify_id_token(id_token, app) - + """Test cases for the firebase.auth public API.""" + + def setUp(self): + super(AuthApiTest, self).setUp() + firebase.initialize_app({'credential': self.CREDENTIAL}) + + def tearDown(self): + testutils.cleanup_apps() + super(AuthApiTest, self).tearDown() + + def test_custom_token_creation(self): + token_string = auth.create_custom_token(self.USER, self.CLAIMS) + self.assertIsInstance(token_string, basestring) + self.verify_custom_token(token_string) + + def test_custom_token_creation_for_non_default_app(self): + app = firebase.initialize_app( + {'credential': self.CREDENTIAL}, 'test-app') + token_string = auth.create_custom_token(self.USER, self.CLAIMS, app) + self.assertIsInstance(token_string, basestring) + self.verify_custom_token(token_string) + + def test_custom_token_creation_for_uninitialized_app(self): + app = firebase.App('test-app', {'credential': self.CREDENTIAL}) + with self.assertRaises(ValueError): + auth.create_custom_token(self.USER, self.CLAIMS, app) + + def test_custom_token_creation_for_uninitialized_duplicate_app(self): + firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') + app = firebase.App('test-app', {'credential': self.CREDENTIAL}) + with self.assertRaises(ValueError): + auth.create_custom_token(self.USER, self.CLAIMS, app) + + def test_custom_token_creation_for_invalid_app(self): + for app in ['foo', 1, 0, True, False, dict(), list(), tuple()]: + with self.assertRaises(ValueError): + auth.create_custom_token(self.USER, self.CLAIMS, app) + + def test_verify_id_token(self): + id_token = self.get_id_token() + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + claims = auth.verify_id_token(id_token) + self.assertEquals(claims['admin'], True) + self.assertEquals(claims['uid'], self.USER) + + def test_verify_id_token_for_non_default_app(self): + app = firebase.initialize_app( + {'credential': self.CREDENTIAL}, 'test-app') + id_token = self.get_id_token() + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + claims = auth.verify_id_token(id_token, app) + self.assertEquals(claims['admin'], True) + self.assertEquals(claims['uid'], self.USER) + + def test_verify_id_token_for_uninitialized_app(self): + app = firebase.App('test-app', {'credential': self.CREDENTIAL}) + id_token = self.get_id_token() + auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) + with self.assertRaises(ValueError): + auth.verify_id_token(id_token, app) + + def test_verify_id_token_for_uninitialized_duplicate_app(self): + firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') + app = firebase.App('test-app', {'credential': self.CREDENTIAL}) + id_token = self.get_id_token() + with self.assertRaises(ValueError): + auth.verify_id_token(id_token, app) + + def test_verify_id_token_for_invalid_app(self): + id_token = self.get_id_token() + for app in ['foo', 1, 0, True, False, dict(), list(), tuple()]: + with self.assertRaises(ValueError): + auth.verify_id_token(id_token, app) diff --git a/tests/testutils.py b/tests/testutils.py index eb6fccbcb..06ad5f096 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -1,36 +1,43 @@ -import httplib2 +"""Common utility classes and functions for testing.""" import os +import httplib2 + import firebase def resource_filename(filename): - return os.path.join(os.path.dirname(__file__), 'data', filename) + """Returns the absolute path to a test resource.""" + return os.path.join(os.path.dirname(__file__), 'data', filename) def resource(filename): - with open(resource_filename(filename), 'r') as file_obj: - return file_obj.read() + """Returns the contents of a test resource.""" + with open(resource_filename(filename), 'r') as file_obj: + return file_obj.read() def cleanup_apps(): - with firebase._apps_lock: - for name in firebase._apps.keys(): - firebase.delete_app(name) + with firebase._apps_lock: + app_names = list(firebase._apps.keys()) + for name in app_names: + firebase.delete_app(name) class HttpMock(object): - """A mock HTTP client implementation. - - This can be used whenever an HTTP interaction needs to be mocked - for testing purposes. For example HTTP calls to fetch public key - certificates, and HTTP calls to retrieve access tokens can be - mocked using this class. - """ - - def __init__(self, status, response): - self.status = status - self.response = response - - def request(self, *args, **kwargs): - return httplib2.Response({'status': self.status}), self.response + """A mock HTTP client implementation. + + This can be used whenever an HTTP interaction needs to be mocked + for testing purposes. For example HTTP calls to fetch public key + certificates, and HTTP calls to retrieve access tokens can be + mocked using this class. + """ + + def __init__(self, status, response): + self.status = status + self.response = response + + def request(self, *args, **kwargs): + del args + del kwargs + return httplib2.Response({'status': self.status}), self.response From 6d05c42b02212b95f70008a7d0ead1ec1996d947 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 17 Mar 2017 17:08:30 -0700 Subject: [PATCH 03/12] Added link to pylint docs --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 99812a6f9..5d4b40bf9 100644 --- a/README.md +++ b/README.md @@ -46,5 +46,6 @@ all source files, pass `all` as an argument. Ideally you should not see any pylint errors or warnings when you run the linter. This means source files are properly formatted, and the linter has not found any -issues. If you do observe any errors, take action to correct them before sending -a pull request. +issues. If you do observe any errors, fix them before sending a pull request. +Details on how to interpret pylint errors are available here +[here](https://pylint.readthedocs.io/en/latest/user_guide/output.html). From b5bf2d00b934f75dd2afe576507849b84c85053d Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 17 Mar 2017 17:47:42 -0700 Subject: [PATCH 04/12] Improved linter script and documentation --- README.md | 22 ++++++++++++++++------ lint.sh | 7 ++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5d4b40bf9..f052dab2d 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,13 @@ the following command. pylint --version ``` -There are two pylint configuration files at the root of this repository. - * .pylintrc: Settings for validating the source files in firebase module. - * .test_pylintrc: Settings for validating the test files. This is a marginally - relaxed version of .pylintrc. +There are two pylint configuration files at the root of this Git repository. + * `.pylintrc`: Settings for validating the source files in firebase module. + * `.test_pylintrc`: Settings for validating the test files. This is a + marginally relaxed version of .pylintrc. -You can run pylint directly using the above configuration files. +You can run pylint directly from the command-line using the above configuration +files. ``` pylint --rcfile .pylintrc firebase @@ -47,5 +48,14 @@ all source files, pass `all` as an argument. Ideally you should not see any pylint errors or warnings when you run the linter. This means source files are properly formatted, and the linter has not found any issues. If you do observe any errors, fix them before sending a pull request. -Details on how to interpret pylint errors are available here +Details on how to interpret pylint errors are available [here](https://pylint.readthedocs.io/en/latest/user_guide/output.html). + +Our configuration files suppress the verbose reports usually generated +by pylint, and only output the detected issues. If you wish to obtain the +comprehensive reports, run pylint from command-line with the `-r` flag. + +``` +pylint -r yes --rcfile .pylintrc firebase +pylint -r yes --rcfile .test_pylintrc tests +``` diff --git a/lint.sh b/lint.sh index 9f17fea52..3be9541ec 100755 --- a/lint.sh +++ b/lint.sh @@ -2,11 +2,8 @@ function lintAllFiles () { files=`find $1 -name *.py` - for f in $files - do - echo "Running linter on $f" - pylint --rcfile $2 $f - done + echo "Running linter on module $1" + pylint --rcfile $2 $files } function lintChangedFiles () { From 15641ad75b3d61a2249292d0f7e1a60463cdf30c Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 20 Mar 2017 13:34:53 -0700 Subject: [PATCH 05/12] Updated tests to use pytest; Updated lint.sh to use one config file instead of two. --- .gitignore | 3 + .test_pylintrc | 410 --------------------------------------------- README.md | 39 ++--- lint.sh | 17 +- tests/app_test.py | 72 -------- tests/auth_test.py | 360 --------------------------------------- tests/test_app.py | 72 ++++++++ tests/test_auth.py | 240 ++++++++++++++++++++++++++ 8 files changed, 342 insertions(+), 871 deletions(-) delete mode 100644 .test_pylintrc delete mode 100644 tests/app_test.py delete mode 100644 tests/auth_test.py create mode 100644 tests/test_app.py create mode 100644 tests/test_auth.py diff --git a/.gitignore b/.gitignore index 07f8a98e1..b894ee771 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.pyc +.python-version .cache/ +.tox/ +*.egg-info/ diff --git a/.test_pylintrc b/.test_pylintrc deleted file mode 100644 index bfe9124b7..000000000 --- a/.test_pylintrc +++ /dev/null @@ -1,410 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Pickle collected data for later comparisons. -persistent=no - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. This option is deprecated -# and it will be removed in Pylint 2.0. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=indexing-exception,old-raise-syntax - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,protected-access - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". This option is deprecated -# and it will be removed in Pylint 2.0. -files-output=no - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,future.builtins - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[BASIC] - -# Good variable names which should always be accepted, separated by a comma -good-names=main,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names= - -bad-functions=input,apply,reduce - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]*$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]*$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ - - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]*$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]*$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=(__.*__|main) - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=10 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/README.md b/README.md index f052dab2d..c82089084 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Firebase Admin Python SDK ## Running Linters -We recommend using [pylint](https://pylint.org/) for verifying source code -format, and enforcing other Python programming best practices. Install pylint +We use [pylint](https://pylint.org/) for verifying source code format, +and enforcing other Python programming best practices. Install pylint 1.6.4 or higher using pip. ``` sudo pip install pylint ``` -Specify a pylint version explicitly if the above command installs an older +Specify a pylint version explicitly if the above command installs an older version. ``` @@ -23,32 +23,30 @@ the following command. pylint --version ``` -There are two pylint configuration files at the root of this Git repository. - * `.pylintrc`: Settings for validating the source files in firebase module. - * `.test_pylintrc`: Settings for validating the test files. This is a - marginally relaxed version of .pylintrc. - -You can run pylint directly from the command-line using the above configuration -files. +There is a pylint configuration file (`.pylintrc`) at the root of this Git +repository. This enables you to invoke pylint directly from the command line. ``` -pylint --rcfile .pylintrc firebase -pylint --rcfile .test_pylintrc tests +pylint firebase ``` -Alternatively you can use the `lint.sh` bash script to invoke pylint. By default -this script will only validate the locally modified source files. To validate -all source files, pass `all` as an argument. +However, it is recommended that you use the `lint.sh` bash script to invoke +pylint. This script will run the linter on both firebase and the corresponding +tests module. It suprresses some of the noisy warnings that get generated +when running pylint on test code. Note that by default `lint.sh` will only +validate the locally modified source files. To validate all source files, +pass `all` as an argument. ``` ./lint.sh ./lint.sh all ``` -Ideally you should not see any pylint errors or warnings when you run the linter. -This means source files are properly formatted, and the linter has not found any -issues. If you do observe any errors, fix them before sending a pull request. -Details on how to interpret pylint errors are available +Ideally you should not see any pylint errors or warnings when you run the +linter. This means source files are properly formatted, and the linter has +not found any issues. If you do observe any errors, fix them before +committing or sending a pull request. Details on how to interpret pylint +errors are available [here](https://pylint.readthedocs.io/en/latest/user_guide/output.html). Our configuration files suppress the verbose reports usually generated @@ -56,6 +54,5 @@ by pylint, and only output the detected issues. If you wish to obtain the comprehensive reports, run pylint from command-line with the `-r` flag. ``` -pylint -r yes --rcfile .pylintrc firebase -pylint -r yes --rcfile .test_pylintrc tests +pylint -r yes firebase ``` diff --git a/lint.sh b/lint.sh index 3be9541ec..2982caacd 100755 --- a/lint.sh +++ b/lint.sh @@ -1,25 +1,26 @@ #!/bin/bash function lintAllFiles () { - files=`find $1 -name *.py` echo "Running linter on module $1" - pylint --rcfile $2 $files + pylint --disable=$2 $1 } function lintChangedFiles () { - files=`git status -s $1 | awk '{print $2}' | grep .py$` + files=`git status -s $1 | grep -v "^D" | awk '{print $NF}' | grep .py$` for f in $files do echo "Running linter on $f" - pylint --rcfile $2 $f + pylint --disable=$2 $f done } +SKIP_FOR_TESTS="redefined-outer-name,protected-access,missing-docstring" + if [[ $1 = "all" ]] then - lintAllFiles firebase .pylintrc - lintAllFiles tests .test_pylintrc + lintAllFiles firebase + lintAllFiles tests $SKIP_FOR_TESTS else - lintChangedFiles firebase .pylintrc - lintChangedFiles tests .test_pylintrc + lintChangedFiles firebase + lintChangedFiles tests $SKIP_FOR_TESTS fi diff --git a/tests/app_test.py b/tests/app_test.py deleted file mode 100644 index 230567679..000000000 --- a/tests/app_test.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Tests for firebase.App.""" -import unittest - -import firebase -from firebase import auth -from tests import testutils - - -class FirebaseAppTest(unittest.TestCase): - """Test cases for App initialization and life cycle.""" - - SERVICE_ACCOUNT_PATH = 'service_account.json' - CREDENTIAL = auth.CertificateCredential( - testutils.resource_filename(SERVICE_ACCOUNT_PATH)) - OPTIONS = {'credential': CREDENTIAL} - - def tearDown(self): - testutils.cleanup_apps() - - def test_default_app_init(self): - app = firebase.initialize_app(self.OPTIONS) - self.assertEquals(firebase._DEFAULT_APP_NAME, app.name) - self.assertIs(self.CREDENTIAL, app.options.credential) - with self.assertRaises(ValueError): - firebase.initialize_app(self.OPTIONS) - - def test_non_default_app_init(self): - app = firebase.initialize_app(self.OPTIONS, 'myApp') - self.assertEquals('myApp', app.name) - self.assertIs(self.CREDENTIAL, app.options.credential) - with self.assertRaises(ValueError): - firebase.initialize_app(self.OPTIONS, 'myApp') - - def test_app_init_with_empty_options(self): - with self.assertRaises(ValueError): - firebase.initialize_app({}) - - def test_app_init_with_no_credential(self): - options = {'key': 'value'} - with self.assertRaises(ValueError): - firebase.initialize_app(options) - - def test_app_init_with_invalid_options(self): - for options in [None, 0, 1, 'foo', list(), tuple(), True, False]: - with self.assertRaises(ValueError): - firebase.initialize_app(options) - - def test_app_init_with_invalid_name(self): - for name in [None, '', 0, 1, dict(), list(), tuple(), True, False]: - with self.assertRaises(ValueError): - firebase.initialize_app(self.OPTIONS, name) - - def test_default_app_get(self): - app = firebase.initialize_app(self.OPTIONS) - self.assertIs(app, firebase.get_app()) - - def test_non_default_app_get(self): - app = firebase.initialize_app(self.OPTIONS, 'myApp') - self.assertIs(app, firebase.get_app('myApp')) - - def test_non_existing_default_app_get(self): - with self.assertRaises(ValueError): - self.assertIsNone(firebase.get_app()) - - def test_non_existing_app_get(self): - with self.assertRaises(ValueError): - self.assertIsNone(firebase.get_app('myApp')) - - def test_app_get_with_invalid_name(self): - for name in [None, '', 0, 1, dict(), list(), tuple(), True, False]: - with self.assertRaises(ValueError): - firebase.initialize_app(self.OPTIONS, name) diff --git a/tests/auth_test.py b/tests/auth_test.py deleted file mode 100644 index f0549299f..000000000 --- a/tests/auth_test.py +++ /dev/null @@ -1,360 +0,0 @@ -"""Tests for firebase.auth.""" -import os -import time -import unittest - -from oauth2client import client -from oauth2client import crypt - -import firebase -from firebase import auth -from firebase import jwt -from tests import testutils - - -class _AbstractAuthTest(unittest.TestCase): - """Super class for auth-related tests. - - Defines constants used in auth-related tests, and provides a method for - asserting the validity of custom tokens. - """ - SERVICE_ACCOUNT_EMAIL = 'test-484@mg-test-1210.iam.gserviceaccount.com' - PROJECT_ID = 'test-484' - CLIENT_CERT_URL = ('https://www.googleapis.com/robot/v1/metadata/x509/' - 'test-484%40mg-test-1210.iam.gserviceaccount.com') - - FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis' - '.com/google.identity.identitytoolkit.' - 'v1.IdentityToolkit') - USER = 'user1' - ISSUER = 'test-484@mg-test-1210.iam.gserviceaccount.com' - CLAIMS = {'one': 2, 'three': 'four'} - - CREDENTIAL = auth.CertificateCredential( - testutils.resource_filename('service_account.json')) - PUBLIC_CERTS = testutils.resource('public_certs.json') - PRIVATE_KEY = testutils.resource('private_key.pem') - - def verify_custom_token(self, custom_token, verify_claims=True): - token = client.verify_id_token( - custom_token, - self.FIREBASE_AUDIENCE, - http=testutils.HttpMock(200, self.PUBLIC_CERTS), - cert_uri=self.CLIENT_CERT_URL) - self.assertEquals(token['uid'], self.USER) - self.assertEquals(token['iss'], self.SERVICE_ACCOUNT_EMAIL) - self.assertEquals(token['sub'], self.SERVICE_ACCOUNT_EMAIL) - if verify_claims: - self.assertEquals(token['claims']['one'], self.CLAIMS['one']) - self.assertEquals(token['claims']['three'], self.CLAIMS['three']) - - def _merge_jwt_claims(self, defaults, overrides): - defaults.update(overrides) - for key, value in overrides.items(): - if value is None: - del defaults[key] - return defaults - - def get_id_token(self, payload_overrides=None, header_overrides=None): - """Generates a signed ID token for testing. - - Args: - payload_overrides: A dictionary of overrides for payload fields - header_overrides: A dictionary of overrides for header fields - - Returns: - A signed JWT ID token string. - """ - signer = crypt.Signer.from_string(self.PRIVATE_KEY) - headers = { - 'kid': 'd98d290613ae1468f7e5f5cf604ead38ca9c8358' - } - payload = { - 'aud': 'mg-test-1210', - 'iss': 'https://securetoken.google.com/mg-test-1210', - 'iat': int(time.time()) - 100, - 'exp': int(time.time()) + 3600, - 'sub': '1234567890', - 'uid': self.USER, - 'admin': True, - } - if header_overrides: - headers = self._merge_jwt_claims(headers, header_overrides) - if payload_overrides: - payload = self._merge_jwt_claims(payload, payload_overrides) - return jwt.encode(payload, signer, headers=headers) - - -class TokenGeneratorTest(_AbstractAuthTest): - """Test cases for the firebase.auth._TokenGenerator class.""" - - APP = firebase.App( - 'test-app', {'credential': _AbstractAuthTest.CREDENTIAL}) - TOKEN_GEN = auth._TokenGenerator(APP) - - def test_custom_token_creation(self): - token_string = self.TOKEN_GEN.create_custom_token( - self.USER, self.CLAIMS) - self.assertIsInstance(token_string, basestring) - self.verify_custom_token(token_string) - - def test_custom_token_creation_with_correct_header(self): - token_string = self.TOKEN_GEN.create_custom_token( - self.USER, self.CLAIMS) - header, _ = jwt.decode(token_string) - self.assertEquals('JWT', header.get('typ')) - self.assertEquals('RS256', header.get('alg')) - - def test_custom_token_creation_without_dev_claims(self): - token_string = self.TOKEN_GEN.create_custom_token(self.USER) - self.verify_custom_token(token_string, False) - - def test_custom_token_creation_with_empty_dev_claims(self): - token_string = self.TOKEN_GEN.create_custom_token(self.USER, {}) - self.verify_custom_token(token_string, False) - - def test_custom_token_creation_with_no_uid(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(None) - - def test_custom_token_creation_with_empty_uid(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token('') - - def test_custom_token_creation_with_long_uid(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token('x' * 129) - - def test_custom_token_creation_with_non_string_uid(self): - for item in [True, False, 0, 1, [], {}, {'a': 1}]: - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(item) - - def test_custom_token_creation_with_bad_claims(self): - for item in [True, False, 0, 1, 'foo', [], (1, 2)]: - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token('user1', item) - - def test_custom_token_creation_with_non_cert_credential(self): - app = firebase.initialize_app( - {'credential': auth.Credential()}, 'test-app') - token_generator = auth._TokenGenerator(app) - with self.assertRaises(ValueError): - token_generator.create_custom_token(self.USER) - - def test_custom_token_creation_fails_with_reserved_claim(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(self.USER, {'sub': '1234'}) - - def test_custom_token_creation_with_malformed_developer_claims(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(self.USER, 'bad_value') - - def test_verify_valid_token(self): - id_token = self.get_id_token() - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - claims = self.TOKEN_GEN.verify_id_token(id_token) - self.assertEquals(claims['admin'], True) - self.assertEquals(claims['uid'], self.USER) - - def test_verify_valid_token_with_project_id_env_var(self): - id_token = self.get_id_token() - gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) - try: - os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = 'mg-test-1210' - app = firebase.App('test-app', {'credential': auth.Credential()}) - token_generator = auth._TokenGenerator(app) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - claims = token_generator.verify_id_token(id_token) - self.assertEquals(claims['admin'], True) - self.assertEquals(claims['uid'], self.USER) - finally: - if gcloud_project: - os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project - else: - del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] - - def test_verify_token_without_project_id(self): - id_token = self.get_id_token() - if os.environ.has_key(auth.GCLOUD_PROJECT_ENV_VAR): - del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] - - app = firebase.App('test-app', {'credential': auth.Credential()}) - token_generator = auth._TokenGenerator(app) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(ValueError): - token_generator.verify_id_token(id_token) - - def test_verify_token_without_key_id(self): - id_token = self.get_id_token(header_overrides={'kid': None}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_wrong_key_id(self): - id_token = self.get_id_token(header_overrides={'kid': 'foo'}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(client.VerifyJwtTokenError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_wrong_algorithm(self): - id_token = self.get_id_token(header_overrides={'alg': 'HS256'}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_custom_token(self): - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - id_token = self.TOKEN_GEN.create_custom_token(self.USER) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_bad_audience(self): - id_token = self.get_id_token({'aud': 'bad-audience'}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_bad_issuer(self): - id_token = self.get_id_token({ - 'iss': 'https://securetoken.google.com/wrong-issuer' - }) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_empty_subject(self): - id_token = self.get_id_token({'sub': ''}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_non_string_subject(self): - id_token = self.get_id_token({'sub': 10}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_long_subject(self): - id_token = self.get_id_token({'sub': 'a' * 129}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_future_token(self): - id_token = self.get_id_token({'iat': int(time.time()) + 1000}) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_expired_token(self): - id_token = self.get_id_token({ - 'iat': int(time.time()) - 10000, - 'exp': int(time.time()) - 3600 - }) - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_token_with_certificate_request_failure(self): - id_token = self.get_id_token() - auth._http = testutils.HttpMock(404, 'not found') - with self.assertRaises(client.VerifyJwtTokenError): - self.TOKEN_GEN.verify_id_token(id_token) - - def test_verify_non_token(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.verify_id_token(None) - - def test_verify_empty_token(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.verify_id_token('') - - def test_verify_non_string_token(self): - for item in [True, False, 0, 1, [], {}, {'a': 1}]: - with self.assertRaises(ValueError): - self.TOKEN_GEN.verify_id_token(item) - - def test_verify_bad_format_token(self): - with self.assertRaises(crypt.AppIdentityError): - self.TOKEN_GEN.verify_id_token('foobar') - - def test_malformed_developer_claims(self): - with self.assertRaises(ValueError): - self.TOKEN_GEN.create_custom_token(self.USER, 'bad_value') - - -class AuthApiTest(_AbstractAuthTest): - """Test cases for the firebase.auth public API.""" - - def setUp(self): - super(AuthApiTest, self).setUp() - firebase.initialize_app({'credential': self.CREDENTIAL}) - - def tearDown(self): - testutils.cleanup_apps() - super(AuthApiTest, self).tearDown() - - def test_custom_token_creation(self): - token_string = auth.create_custom_token(self.USER, self.CLAIMS) - self.assertIsInstance(token_string, basestring) - self.verify_custom_token(token_string) - - def test_custom_token_creation_for_non_default_app(self): - app = firebase.initialize_app( - {'credential': self.CREDENTIAL}, 'test-app') - token_string = auth.create_custom_token(self.USER, self.CLAIMS, app) - self.assertIsInstance(token_string, basestring) - self.verify_custom_token(token_string) - - def test_custom_token_creation_for_uninitialized_app(self): - app = firebase.App('test-app', {'credential': self.CREDENTIAL}) - with self.assertRaises(ValueError): - auth.create_custom_token(self.USER, self.CLAIMS, app) - - def test_custom_token_creation_for_uninitialized_duplicate_app(self): - firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') - app = firebase.App('test-app', {'credential': self.CREDENTIAL}) - with self.assertRaises(ValueError): - auth.create_custom_token(self.USER, self.CLAIMS, app) - - def test_custom_token_creation_for_invalid_app(self): - for app in ['foo', 1, 0, True, False, dict(), list(), tuple()]: - with self.assertRaises(ValueError): - auth.create_custom_token(self.USER, self.CLAIMS, app) - - def test_verify_id_token(self): - id_token = self.get_id_token() - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - claims = auth.verify_id_token(id_token) - self.assertEquals(claims['admin'], True) - self.assertEquals(claims['uid'], self.USER) - - def test_verify_id_token_for_non_default_app(self): - app = firebase.initialize_app( - {'credential': self.CREDENTIAL}, 'test-app') - id_token = self.get_id_token() - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - claims = auth.verify_id_token(id_token, app) - self.assertEquals(claims['admin'], True) - self.assertEquals(claims['uid'], self.USER) - - def test_verify_id_token_for_uninitialized_app(self): - app = firebase.App('test-app', {'credential': self.CREDENTIAL}) - id_token = self.get_id_token() - auth._http = testutils.HttpMock(200, self.PUBLIC_CERTS) - with self.assertRaises(ValueError): - auth.verify_id_token(id_token, app) - - def test_verify_id_token_for_uninitialized_duplicate_app(self): - firebase.initialize_app({'credential': self.CREDENTIAL}, 'test-app') - app = firebase.App('test-app', {'credential': self.CREDENTIAL}) - id_token = self.get_id_token() - with self.assertRaises(ValueError): - auth.verify_id_token(id_token, app) - - def test_verify_id_token_for_invalid_app(self): - id_token = self.get_id_token() - for app in ['foo', 1, 0, True, False, dict(), list(), tuple()]: - with self.assertRaises(ValueError): - auth.verify_id_token(id_token, app) diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 000000000..30dbce47a --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,72 @@ +"""Tests for firebase.App.""" +import pytest + +import firebase +from firebase import auth +from tests import testutils + + +CREDENTIAL = auth.CertificateCredential( + testutils.resource_filename('service_account.json')) +OPTIONS = {'credential': CREDENTIAL} + + +class TestFirebaseApp(object): + """Test cases for App initialization and life cycle.""" + + invalid_options = { + 'EmptyOptions': ({}, ValueError), + 'NoCredential': ({'k':'v'}, ValueError), + 'NoneOptions': (None, ValueError), + 'IntOptions': (1, ValueError), + 'StringOptions': ('foo', ValueError), + } + + invalid_names = [None, '', 0, 1, dict(), list(), tuple(), True, False] + + def teardown_method(self): + testutils.cleanup_apps() + + def test_default_app_init(self): + app = firebase.initialize_app(OPTIONS) + assert firebase._DEFAULT_APP_NAME == app.name + assert CREDENTIAL is app.options.credential + with pytest.raises(ValueError): + firebase.initialize_app(OPTIONS) + + def test_non_default_app_init(self): + app = firebase.initialize_app(OPTIONS, 'myApp') + assert app.name == 'myApp' + assert CREDENTIAL is app.options.credential + with pytest.raises(ValueError): + firebase.initialize_app(OPTIONS, 'myApp') + + @pytest.mark.parametrize('options,error', invalid_options.values(), + ids=invalid_options.keys()) + def test_app_init_with_invalid_options(self, options, error): + with pytest.raises(error): + firebase.initialize_app(options) + + @pytest.mark.parametrize('name', invalid_names) + def test_app_init_with_invalid_name(self, name): + with pytest.raises(ValueError): + firebase.initialize_app(OPTIONS, name) + + def test_default_app_get(self): + app = firebase.initialize_app(OPTIONS) + assert app is firebase.get_app() + + def test_non_default_app_get(self): + app = firebase.initialize_app(OPTIONS, 'myApp') + assert app is firebase.get_app('myApp') + + @pytest.mark.parametrize('args', [(), ('myApp',)], + ids=['DefaultApp', 'CustomApp']) + def test_non_existing_app_get(self, args): + with pytest.raises(ValueError): + firebase.get_app(*args) + + @pytest.mark.parametrize('name', invalid_names) + def test_app_get_with_invalid_name(self, name): + with pytest.raises(ValueError): + firebase.initialize_app(OPTIONS, name) diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 000000000..05577f87d --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,240 @@ +"""Test cases for firebase.auth module.""" +import os +import time + +from oauth2client import client +from oauth2client import crypt +import pytest + +import firebase +from firebase import auth +from firebase import jwt +from tests import testutils + + +SERVICE_ACCOUNT_EMAIL = 'test-484@mg-test-1210.iam.gserviceaccount.com' +CLIENT_CERT_URL = ('https://www.googleapis.com/robot/v1/metadata/x509/' + 'test-484%40mg-test-1210.iam.gserviceaccount.com') + +FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis.com/' + 'google.identity.identitytoolkit.v1.IdentityToolkit') +USER = 'user1' + +CREDENTIAL = auth.CertificateCredential( + testutils.resource_filename('service_account.json')) +PUBLIC_CERTS = testutils.resource('public_certs.json') +PRIVATE_KEY = testutils.resource('private_key.pem') + + +class AuthFixture(object): + def __init__(self, name=None): + if name: + self.app = firebase.get_app(name) + else: + self.app = None + + def create_custom_token(self, *args): + if self.app: + return auth.create_custom_token(*args, app=self.app) + else: + return auth.create_custom_token(*args) + + def verify_id_token(self, *args): + if self.app: + return auth.verify_id_token(*args, app=self.app) + else: + return auth.verify_id_token(*args) + +def setup_module(): + firebase.initialize_app({'credential': CREDENTIAL}) + firebase.initialize_app({'credential': CREDENTIAL}, 'testApp') + +def teardown_module(): + firebase.delete_app('[DEFAULT]') + firebase.delete_app('testApp') + +@pytest.fixture(params=[None, 'testApp'], ids=['DefaultApp', 'CustomApp']) +def authtest(request): + return AuthFixture(request.param) + +@pytest.fixture +def non_cert_app(): + app = firebase.initialize_app( + {'credential': auth.Credential()}, 'non-cert-app') + yield app + firebase.delete_app(app.name) + +def verify_custom_token(custom_token, expected_claims): + assert isinstance(custom_token, basestring) + token = client.verify_id_token( + custom_token, + FIREBASE_AUDIENCE, + http=testutils.HttpMock(200, PUBLIC_CERTS), + cert_uri=CLIENT_CERT_URL) + assert token['uid'] == USER + assert token['iss'] == SERVICE_ACCOUNT_EMAIL + assert token['sub'] == SERVICE_ACCOUNT_EMAIL + header, _ = jwt.decode(custom_token) + assert header.get('typ') == 'JWT' + assert header.get('alg') == 'RS256' + if expected_claims: + for key, value in expected_claims.items(): + assert value == token['claims'][key] + +def _merge_jwt_claims(defaults, overrides): + defaults.update(overrides) + for key, value in overrides.items(): + if value is None: + del defaults[key] + return defaults + +def get_id_token(payload_overrides=None, header_overrides=None): + signer = crypt.Signer.from_string(PRIVATE_KEY) + headers = { + 'kid': 'd98d290613ae1468f7e5f5cf604ead38ca9c8358' + } + payload = { + 'aud': 'mg-test-1210', + 'iss': 'https://securetoken.google.com/mg-test-1210', + 'iat': int(time.time()) - 100, + 'exp': int(time.time()) + 3600, + 'sub': '1234567890', + 'uid': USER, + 'admin': True, + } + if header_overrides: + headers = _merge_jwt_claims(headers, header_overrides) + if payload_overrides: + payload = _merge_jwt_claims(payload, payload_overrides) + return jwt.encode(payload, signer, headers=headers) + + +class TestCreateCustomToken(object): + + valid_args = { + 'Basic': (USER, {'one': 2, 'three': 'four'}), + 'NoDevClaims': (USER, None), + 'EmptyDevClaims': (USER, {}), + } + + invalid_args = { + 'NoUid': (None, None, ValueError), + 'EmptyUid': ('', None, ValueError), + 'LongUid': ('x'*129, None, ValueError), + 'BoolUid': (True, None, ValueError), + 'IntUid': (1, None, ValueError), + 'ListUid': ([], None, ValueError), + 'EmptyDictUid': ({}, None, ValueError), + 'NonEmptyDictUid': ({'a':1}, None, ValueError), + 'BoolClaims': (USER, True, ValueError), + 'IntClaims': (USER, 1, ValueError), + 'StrClaims': (USER, 'foo', ValueError), + 'ListClaims': (USER, [], ValueError), + 'TupleClaims': (USER, (1, 2), ValueError), + 'ReservedClaims': (USER, {'sub':'1234'}, ValueError), + } + + @pytest.mark.parametrize('user,claims', valid_args.values(), + ids=valid_args.keys()) + def test_valid_params(self, authtest, user, claims): + verify_custom_token(authtest.create_custom_token(user, claims), claims) + + @pytest.mark.parametrize('user,claims,error', invalid_args.values(), + ids=invalid_args.keys()) + def test_invalid_params(self, authtest, user, claims, error): + with pytest.raises(error): + authtest.create_custom_token(user, claims) + + def test_noncert_credential(self, non_cert_app): + with pytest.raises(ValueError): + auth.create_custom_token(USER, app=non_cert_app) + + +class TestVerifyIdToken(object): + + invalid_tokens = { + 'NoKid': (get_id_token(header_overrides={'kid': None}), + crypt.AppIdentityError), + 'WrongKid': (get_id_token(header_overrides={'kid': 'foo'}), + client.VerifyJwtTokenError), + 'WrongAlg': (get_id_token(header_overrides={'alg': 'HS256'}), + crypt.AppIdentityError), + 'BadAudience': (get_id_token({'aud': 'bad-audience'}), + crypt.AppIdentityError), + 'BadIssuer': (get_id_token({ + 'iss': 'https://securetoken.google.com/wrong-issuer' + }), crypt.AppIdentityError), + 'EmptySubject': (get_id_token({'sub': ''}), + crypt.AppIdentityError), + 'IntSubject': (get_id_token({'sub': 10}), + crypt.AppIdentityError), + 'LongStrSubject': (get_id_token({'sub': 'a' * 129}), + crypt.AppIdentityError), + 'FutureToken': (get_id_token({'iat': int(time.time()) + 1000}), + crypt.AppIdentityError), + 'ExpiredToken': (get_id_token({ + 'iat': int(time.time()) - 10000, + 'exp': int(time.time()) - 3600 + }), crypt.AppIdentityError), + 'NoneToken': (None, ValueError), + 'EmptyToken': ('', ValueError), + 'BoolToken': (True, ValueError), + 'IntToken': (1, ValueError), + 'ListToken': ([], ValueError), + 'EmptyDictToken': ({}, ValueError), + 'NonEmptyDictToken': ({'a': 1}, ValueError), + 'BadFormatToken': ('foobar', crypt.AppIdentityError) + } + + def setup_method(self): + auth._http = testutils.HttpMock(200, PUBLIC_CERTS) + + def test_valid_token(self, authtest): + id_token = get_id_token() + claims = authtest.verify_id_token(id_token) + assert claims['admin'] is True + assert claims['uid'] == USER + + @pytest.mark.parametrize('id_token,error', invalid_tokens.values(), + ids=invalid_tokens.keys()) + def test_invalid_token(self, authtest, id_token, error): + with pytest.raises(error): + authtest.verify_id_token(id_token) + + def test_project_id_env_var(self, non_cert_app): + id_token = get_id_token() + gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) + try: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = 'mg-test-1210' + claims = auth.verify_id_token(id_token, non_cert_app) + assert claims['admin'] is True + assert claims['uid'] == USER + finally: + if gcloud_project: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project + else: + del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] + + def test_no_project_id(self, non_cert_app): + id_token = get_id_token() + gcloud_project = None + if os.environ.has_key(auth.GCLOUD_PROJECT_ENV_VAR): + gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) + del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] + try: + with pytest.raises(ValueError): + auth.verify_id_token(id_token, non_cert_app) + finally: + if gcloud_project: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project + + def test_custom_token(self, authtest): + id_token = authtest.create_custom_token(USER) + with pytest.raises(crypt.AppIdentityError): + authtest.verify_id_token(id_token) + + def test_certificate_request_failure(self, authtest): + id_token = get_id_token() + auth._http = testutils.HttpMock(404, 'not found') + with pytest.raises(client.VerifyJwtTokenError): + authtest.verify_id_token(id_token) From 73e48149e7200b7b6bccae6c7545eeb2945def20 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 20 Mar 2017 13:45:37 -0700 Subject: [PATCH 06/12] Updated documentation --- README.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c82089084..4eb0ce894 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,39 @@ # Firebase Admin Python SDK +## Unit Testing +We use [pytest](http://doc.pytest.org/en/latest/) for writing and executing +unit tests. Download pytest 3.0.6 or higher using pip. + +``` +pip install -U pytest +``` + +All source files containing test code is located in the `tests` +directory. Simply launch pytest from the root of the Git repository, or from +within the `tests` directory to execute all test cases. + +``` +pytest +``` + +Refer the pytest [usage and invocations](http://doc.pytest.org/en/latest/usage.html) +guide to learn how to run a subset of all test cases. + + ## Running Linters -We use [pylint](https://pylint.org/) for verifying source code format, -and enforcing other Python programming best practices. Install pylint -1.6.4 or higher using pip. +We use [pylint](https://pylint.org/) for verifying source code format, and +enforcing other Python programming best practices. Install pylint 1.6.4 or +higher using pip. ``` -sudo pip install pylint +pip install -U pylint ``` Specify a pylint version explicitly if the above command installs an older version. ``` -sudo pip install pylint==1.6.4 +pip install pylint==1.6.4 ``` Once installed, you can check the version of the installed binary by running From 1d9b5a280566d081bee67e757beba56feba442f1 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 20 Mar 2017 13:50:55 -0700 Subject: [PATCH 07/12] Updated documentation --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4eb0ce894..6b7e19ed7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Firebase Admin Python SDK +Firebase Admin Python SDK enables server-side (backend) Python developers +to integrate [Firebase](http://firebase.google.com) into their services +and applications. Currently this SDK provides Firebase custom authentication +support. Other Firebase APIs will be added soon. + + ## Unit Testing We use [pytest](http://doc.pytest.org/en/latest/) for writing and executing unit tests. Download pytest 3.0.6 or higher using pip. From 66993ffc08f56b3fa5ec9dcb5a2dec780760b194 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 21 Mar 2017 17:47:45 -0700 Subject: [PATCH 08/12] Changed max line length to 100; Updated documentation; Using the mock private key from Node SDK tests for testing --- .pylintrc | 2 +- README.md | 32 ++++++++++---------- tests/data/private_key.pem | 53 ++++++++++++++++----------------- tests/data/public_certs.json | 2 +- tests/data/service_account.json | 2 +- 5 files changed, 44 insertions(+), 47 deletions(-) diff --git a/.pylintrc b/.pylintrc index 0c71c9af4..89ad58fd1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -285,7 +285,7 @@ ignore-imports=no [FORMAT] # Maximum number of characters on a single line. -max-line-length=80 +max-line-length=100 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ diff --git a/README.md b/README.md index 6b7e19ed7..aa57b3060 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,71 @@ # Firebase Admin Python SDK -Firebase Admin Python SDK enables server-side (backend) Python developers -to integrate [Firebase](http://firebase.google.com) into their services +The Firebase Admin Python SDK enables server-side (backend) Python developers +to integrate [Firebase](https://firebase.google.com) into their services and applications. Currently this SDK provides Firebase custom authentication -support. Other Firebase APIs will be added soon. +support. ## Unit Testing We use [pytest](http://doc.pytest.org/en/latest/) for writing and executing -unit tests. Download pytest 3.0.6 or higher using pip. +unit tests. Download pytest 3.0.6 or higher using pip: ``` pip install -U pytest ``` -All source files containing test code is located in the `tests` +All source files containing test code is located in the `tests/` directory. Simply launch pytest from the root of the Git repository, or from -within the `tests` directory to execute all test cases. +within the `tests/` directory to execute all test cases. ``` pytest ``` -Refer the pytest [usage and invocations](http://doc.pytest.org/en/latest/usage.html) -guide to learn how to run a subset of all test cases. +Refer to the pytest [usage and invocations guide](http://doc.pytest.org/en/latest/usage.html) +to learn how to run a subset of all test cases. ## Running Linters We use [pylint](https://pylint.org/) for verifying source code format, and enforcing other Python programming best practices. Install pylint 1.6.4 or -higher using pip. +higher using pip: ``` pip install -U pylint ``` Specify a pylint version explicitly if the above command installs an older -version. +version: ``` pip install pylint==1.6.4 ``` Once installed, you can check the version of the installed binary by running -the following command. +the following command: ``` pylint --version ``` There is a pylint configuration file (`.pylintrc`) at the root of this Git -repository. This enables you to invoke pylint directly from the command line. +repository. This enables you to invoke pylint directly from the command line: ``` pylint firebase ``` However, it is recommended that you use the `lint.sh` bash script to invoke -pylint. This script will run the linter on both firebase and the corresponding -tests module. It suprresses some of the noisy warnings that get generated +pylint. This script will run the linter on both `firebase` and the corresponding +`tests` module. It suprresses some of the noisy warnings that get generated when running pylint on test code. Note that by default `lint.sh` will only validate the locally modified source files. To validate all source files, pass `all` as an argument. ``` -./lint.sh -./lint.sh all +./lint.sh # Lint locally modified source files +./lint.sh all # Lint all source files ``` Ideally you should not see any pylint errors or warnings when you run the diff --git a/tests/data/private_key.pem b/tests/data/private_key.pem index e55eb262e..636af7a3e 100644 --- a/tests/data/private_key.pem +++ b/tests/data/private_key.pem @@ -1,28 +1,25 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCQiozUlHeHrUsN -Q34VpMMWfBCStUMb2oPLnhA1LESXGAQ9HIaFzgoDPtxFMxJZ+gJZiRhpb+dmSpN9 -aWGxrBlBTWgrymPEmFRAdilMhIDgNMubPlHieD2ZUYyplZoKziiI+92LPKvG6zpE -h1DltFtnKGXinOfbcTy2zMLGcVfgpid7K8u1txegS9J7OGbqfrJPhs3NJmC4rE5J -lTdc0U+ZwFWNg82MDZpbMTni67S974msFWRGr/dcH86+8nKMii93GfMX3Vgnkq6Q -ckyFmqdBEhzdVw2PHn5bBJAoTaSVHvAHQCwAizSXEDjDEf/4RQIczZLmV+syJ3Pt -w5eYyZ4nAgMBAAECggEAaeBzhIw0enggD9kulKAaH3BFm5GXVOHdxNtFuA1zONJo -2HL6vyzL/NCg/TeJ68rMydU4wpMsB6v9GdKFs2YDIeiXs+wO5MVIgeeMHPa6iIFj -25Xs2V2GkbZAuUBSlDOrUZxdDk8k7RMFnYkZYwmYIppe935EAGSUOrsGueHwoycz -58otdgE/f08Jtwwtlmg1eAdB8YTpG+8g5GgyqPX9Pmsl/1QPkkoya4yHuu6ytkMA -nPqdqpNxczFSD9BcckmmvDPl/gMbvrX2TwQ29LCm0uwpzAVLODlF6z/IglDFJ12a -qCloWR+0GT32rJbJMKAzSm8j0iM8TKZK7tr5aDElwQKBgQDn53tc4GPekaBXoqU4 -0zTqdZGSArzFcspRFFf+F77pQheskFSvQDVuynGT0RNTxepRqvJM95tyxOUqpeR2 -kL58BJWT1EjJbGiPJWD4ZRiCX2xFPuyBa0V2IFw08ZA3k/MyRhRGgDvzro+QX2pM -wXmpqB/ByDiFQN9quO0EB7O3cQKBgQCfj0M8Df80O818dLzACEMHuY2Dajei10K6 -3ITjmYQ601/mJDgHnrGnZCxSVstY/ygyRvpQZOKEyjy8QmxW0RdYP7Q8srfGmNsQ -2tZsk+jM7Txbt/ml8+TRCz28eywtbYQVAkhx4ttjtv5nCjJ3x36Ak0v+Oaen+5zp -tJ67OZLTFwKBgCOVWVB+/dQA9GF+C2wUvGHdeGC3GtARNQoL3RSYACs6gPdxjgz2 -BTziw1qzEgwgqjutx1AYDjomDCPnII8w1omhCnKMeD6v67tLOP3kRUZ77dkSNqgF -Fbtya7OT/VUJ1p84MZQ/yPMzLcQxX9Y3ObvWmEjbuBB6S83MYlHj/KeBAoGACBSi -TA1NamDI9E+ZK4R/mImOICSl8qpCJ+J5HGmu56fCyI33BHPF/Xs2P2lD3Rr29yzf -CmlBi4YOc15NzEvEieSYBSbr5bPiDEV47IDFHnO5Rc/YZc4nPWr7UmtOfnJ4aPP3 -pUTe5XrkAWXjzmsc/ff3tkVHN1unw7IxA7xTsjsCgYAma2fYRuBGnOu3BP7AQ2xa -BdbTyLndP1N3e6BfVufqPM7yWrDCLn97xxscShQg4TYjz0BcATnP1SSsjKtUkQqk -vAHiB4Q3zWSUFFkaW6iAOJuYZI1jbI+J59C/NRWM+kCRYLLwcsJMP0++QdmKZq3E -8lhVy3sgqT14vVKsiy+LdQ== ------END PRIVATE KEY----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwJENcRev+eXZKvhhWLiV3Lz2MvO+naQRHo59g3vaNQnbgyduN/L4krlr +J5c6FiikXdtJNb/QrsAHSyJWCu8j3T9CruiwbidGAk2W0RuViTVspjHUTsIHExx9euWM0Uom +GvYkoqXahdhPL/zViVSJt+Rt8bHLsMvpb8RquTIb9iKY3SMV2tCofNmyCSgVbghq/y7lKORt +V/IRguWs6R22fbkb0r2MCYoNAbZ9dqnbRIFNZBC7itYtUoTEresRWcyFMh0zfAIJycWOJlVL +DLqkY2SmIx8u7fuysCg1wcoSZoStuDq02nZEMw1dx8HGzE0hynpHlloRLByuIuOAfMCCYwID +AQABAoIBADFtihu7TspAO0wSUTpqttzgC/nsIsNn95T2UjVLtyjiDNxPZLUrwq42tdCFur0x +VW9Z+CK5x6DzXWvltlw8IeKKeF1ZEOBVaFzy+YFXKTz835SROcO1fgdjyrme7lRSShGlmKW/ +GKY+baUNquoDLw5qreXaE0SgMp0jt5ktyYuVxvhLDeV4omw2u6waoGkifsGm8lYivg5l3VR7 +w2IVOvYZTt4BuSYVwOM+qjwaS1vtL7gv0SUjrj85Ja6zERRdFiITDhZw6nsvacr9/+/aut9E +aL/koSSb62g5fntQMEwoT4hRnjPnAedmorM9Rhddh2TB3ZKTBbMN1tUk3fJxOuECgYEA+z6l +eSaAcZ3qvwpntcXSpwwJ0SSmzLTH2RJNf+Ld3eBHiSvLTG53dWB7lJtF4R1KcIwf+KGcOFJv +snepzcZBylRvT8RrAAkV0s9OiVm1lXZyaepbLg4GGFJBPi8A6VIAj7zYknToRApdW0s1x/XX +ChewfJDckqsevTMovdbg8YkCgYEAxDYX+3mfvv/opo6HNNY3SfVunM+4vVJL+n8gWZ2w9kz3 +Q9Ub9YbRmI7iQaiVkO5xNuoG1n9bM+3Mnm84aQ1YeNT01YqeyQsipP5Wi+um0PzYTaBw9RO+ +8Gh6992OwlJiRtFk5WjalNWOxY4MU0ImnJwIfKQlUODvLmcixm68NYsCgYEAuAqI3jkk55Vd +KvotREsX5wP7gPePM+7NYiZ1HNQL4Ab1f/bTojZdTV8Sx6YCR0fUiqMqnE+OBvfkGGBtw22S +Lesx6sWf99Ov58+x4Q0U5dpxL0Lb7d2Z+2Dtp+Z4jXFjNeeI4ae/qG/LOR/b0pE0J5F415ap +7Mpq5v89vepUtrkCgYAjMXytu4v+q1Ikhc4UmRPDrUUQ1WVSd+9u19yKlnFGTFnRjej86hiw +H3jPxBhHra0a53EgiilmsBGSnWpl1WH4EmJz5vBCKUAmjgQiBrueIqv9iHiaTNdjsanUyaWw +jyxXfXl2eI80QPXh02+8g1H/pzESgjK7Rg1AqnkfVH9nrwKBgQDJVxKBPTw9pigYMVt9iHrR +iCl9zQVjRMbWiPOc0J56+/5FZYm/AOGl9rfhQ9vGxXZYZiOP5FsNkwt05Y1UoAAH4B4VQwbL +qod71qOcI0ywgZiIR87CYw40gzRfjWnN+YEEW1qfyoNLilEwJB8iB/T+ZePHGmJ4MmQ/cTn9 +xpdLXA== +-----END RSA PRIVATE KEY----- diff --git a/tests/data/public_certs.json b/tests/data/public_certs.json index 76c08f074..40a4ecfd7 100644 --- a/tests/data/public_certs.json +++ b/tests/data/public_certs.json @@ -1,5 +1,5 @@ { - "d98d290613ae1468f7e5f5cf604ead38ca9c8358": "-----BEGIN CERTIFICATE-----\nMIIDKjCCAhKgAwIBAgIIMOpmoq9fpOIwDQYJKoZIhvcNAQEFBQAwODE2MDQGA1UE\nAxMtdGVzdC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29t\nMB4XDTE2MDMxODE4MDY1NFoXDTE2MDMyMDA3MDY1NFowODE2MDQGA1UEAxMtdGVz\ndC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwc1MDUEpn4riMUvY1eJ4kjGifzHX\neNKNuwph0bAvUJqGAcRkXl4YWdRZ0mhj9AnBzC5FdynUYgaMphZmfIrd9cXvbXNT\nljkpY77+h5TKGUbTVKnEP8Yzqm+BfGnzQ+vg5wAlD7Xz/k0yr+k6rw2vBtIBYSZw\nMCXvuQp/zwHLcm2RQgihjhUmDVaouqWRWKo8y4iXNEZUqwYi+iEUjzG2aNvb/DIC\nISbIMgpH8V0vh2BdZNY6x2UML0mC4ZGUdNCl39RFhngDVa3WIAObYD+EoxJRXlXc\nMOpyeQdjykovG2jarsRmIT/ef07fUU/U1IP2cA62+VM57z4XYENtyEbqTQIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEASRz/2FRGOqvjKjKE9l1zhiwT\nkNOm2ioYYgVrG/LFa+Jy2GcnaOW+7Al520NHGYwBCtsX6NmJrunHrVNRabPmUUVJ\nkWH1x+iD63kFJrv3AJb7+FxZBLNVuu0IjsmGlsHgnmUcRe3bHRbTmjpRfq38mbGn\nbBdAM9gOfZteAXrkfA078oDNeUdMlT0sfY8YiiZDASV/h9nfh7KSZQ+LmyKoVAs2\nP/YlvQlNfYbw9yOguxhunEnbixwprra8TYMFmXxh2nLNBqGEGzeF2bijWUGbCWRS\nUH5NSnW0LtZdkbUpZoxfMXAW5kuPebi0zpAbYLx/OhJ/i4XNKGfxOpv6xh747w==\n-----END CERTIFICATE-----\n", + "d98d290613ae1468f7e5f5cf604ead38ca9c8358": "-----BEGIN CERTIFICATE-----\nMIIEFTCCAv2gAwIBAgIJALLYfi2oN8cPMA0GCSqGSIb3DQEBCwUAMIGgMQswCQYD\nVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxDzAN\nBgNVBAoMBkdvb2dsZTERMA8GA1UECwwIRmlyZWJhc2UxHDAaBgNVBAMME2ZpcmVi\nYXNlLmdvb2dsZS5jb20xKjAoBgkqhkiG9w0BCQEWG3N1cHBvcnRAZmlyZWJhc2Uu\nZ29vZ2xlLmNvbTAeFw0xNzAzMjIwMDM4MzRaFw0yNzAzMjAwMDM4MzRaMIGgMQsw\nCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcx\nDzANBgNVBAoMBkdvb2dsZTERMA8GA1UECwwIRmlyZWJhc2UxHDAaBgNVBAMME2Zp\ncmViYXNlLmdvb2dsZS5jb20xKjAoBgkqhkiG9w0BCQEWG3N1cHBvcnRAZmlyZWJh\nc2UuZ29vZ2xlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMCR\nDXEXr/nl2Sr4YVi4ldy89jLzvp2kER6OfYN72jUJ24Mnbjfy+JK5ayeXOhYopF3b\nSTW/0K7AB0siVgrvI90/Qq7osG4nRgJNltEblYk1bKYx1E7CBxMcfXrljNFKJhr2\nJKKl2oXYTy/81YlUibfkbfGxy7DL6W/EarkyG/YimN0jFdrQqHzZsgkoFW4Iav8u\n5SjkbVfyEYLlrOkdtn25G9K9jAmKDQG2fXap20SBTWQQu4rWLVKExK3rEVnMhTId\nM3wCCcnFjiZVSwy6pGNkpiMfLu37srAoNcHKEmaErbg6tNp2RDMNXcfBxsxNIcp6\nR5ZaESwcriLjgHzAgmMCAwEAAaNQME4wHQYDVR0OBBYEFGmG5dc2YEEDbFA2+SBS\nA13S5l4VMB8GA1UdIwQYMBaAFGmG5dc2YEEDbFA2+SBSA13S5l4VMAwGA1UdEwQF\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAEmICKB6kq/Y++JKHZg88JS4nlWzIFh\nNBrfyCnMQiL9mmllEXQIhK25xleQwQGsBF2odDj+8H9CG/lwWLmyC5+TryFjWrhn\nHlt8QJb8E4dIZkYAxDL/ii6tXfFTjvrXsTcY2moD6ZoOoxahVOjVfwkHup0ONn2v\nsCL/11FneR0jhgruXKoqrKspgNVuYp+t4IKnnePpeGJb/I3SyS9GUXlScV/uWyRw\nLdIoR2teEWcWeNrMLmth0NSa3AF3gd9+HTaGpESsusG4qPamqiSM7+INAeTo4k8b\nlbqLwo3Ju6cNGGlDSsDXIUahpCdKnqxBALytITmIcHwsR4vYaDP4iOE=\n-----END CERTIFICATE-----", "525a87bdd5d50522922e6ed2c0216fc442e83e54": "-----BEGIN CERTIFICATE-----\nMIIDKjCCAhKgAwIBAgIIBIUnv7pTIx8wDQYJKoZIhvcNAQEFBQAwODE2MDQGA1UE\nAxMtdGVzdC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29t\nMB4XDTE2MDMxOTE3NTE1NFoXDTE2MDMyMTA2NTE1NFowODE2MDQGA1UEAxMtdGVz\ndC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7beJFmTrA/T4AeMWk/IjxUlGpaxH\n6D1CYbfxEBJUqzuIe7ujaxh76ik/FPQV5WxlL1GOjW0/f5CsmrNaFmTmQbsK4BY3\n3cCd3gM8LcEtmF1I9NxxpXxrZihlfuwbEpb5NpjGPkCC+fG3gTY7qtjuO6e8pGb2\nVQQguOGXKw/YZLZRZXZ41xkQRYrs+tFw48+4YkjMsYJIxyBMiL5Q/HNAQ2IUyZwr\nuc+CMcWyPLNcnsRNXgnPXQD/GKZQnjjJ5KzQAU1vnDcufL9V5KRhb0kRxTTUjE7D\nJl3x4+J6+hbAheZFu9Fntrxie9TvQuQbEBm/437QFYZphfQli0fDjlPHSwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAQzlUGQiWiHgeBZyUsetuoTiQ\nsxzU7B1qw3la/FQrG+jRFr9GE3yjOOxi9JvX16U/ebwSHLUip8UFf/Ir6AJlt/tt\nIjBA6TOd8DysAtr4PCZrAP/m43H9w4lBWdWl1XJE2YfYQgZnorveAMUZqTo0P0pd\nFo3IsYBSTMflKv2Vqz91PPiHgyu2fk+8TYwJT57rnnkS6VzdORTIf+9ZB+J1ye9i\nQN5IgdZ/eqFiJPD8qT5jOcXelWSWqHHdGrNjQNp+z8jgMusY5/ZAlZUe55eo3I0m\nuDSPImLNkDwqY0+bBW6Fp5xi/4O+gJg3cQ+/PeIHzoFqKAlSpxQZSCziPpGfAA==\n-----END CERTIFICATE-----\n", "d2d687bf7d14cb8a54fbd0f36bcc9c4f32fa84cf": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAeKgAwIBAgIIRKlYUHIlbRkwDQYJKoZIhvcNAQEFBQAwIDEeMBwGA1UE\nAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMB4XDTE2MDIxMDAxMzI1N1oXDTI2MDIw\nNzAxMzI1N1owIDEeMBwGA1UEAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkIqM1JR3h61LDUN+FaTDFnwQkrVD\nG9qDy54QNSxElxgEPRyGhc4KAz7cRTMSWfoCWYkYaW/nZkqTfWlhsawZQU1oK8pj\nxJhUQHYpTISA4DTLmz5R4ng9mVGMqZWaCs4oiPvdizyrxus6RIdQ5bRbZyhl4pzn\n23E8tszCxnFX4KYneyvLtbcXoEvSezhm6n6yT4bNzSZguKxOSZU3XNFPmcBVjYPN\njA2aWzE54uu0ve+JrBVkRq/3XB/OvvJyjIovdxnzF91YJ5KukHJMhZqnQRIc3VcN\njx5+WwSQKE2klR7wB0AsAIs0lxA4wxH/+EUCHM2S5lfrMidz7cOXmMmeJwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAF+V8kmeJQnvpPKlFT74BROi0\n1Eple2mSsyQbtm1kL7FJpl1AXZ4sLXXTVj3ql0LsqVawDCVtUSvDXBSHtejnh0bi\nZ0WUyEEJ38XPfXRilIaTrYP408ezowDaXxrfLhho1EjoMOPgXjksu1FyhBFoHmif\ndLJoxyA4f+8DZ8jj7ew6ZIVEmvONYgctpU72uUh36Vyl84oc9D2GByq/zYDXvVvl\nSKWYZ5+86/eGocO4sosB5QrsEdVGT2Im6mz2DUIewSyIvrDgZ5r3XyL4RXpdi8+8\n9re/meIh5pnhimU4pX9weQia8bqSPf0oZhh0uAWxO5ES7k1GwblnJfxeCZ0xDQ==\n-----END CERTIFICATE-----\n" } diff --git a/tests/data/service_account.json b/tests/data/service_account.json index 99d89ace2..7922e24da 100644 --- a/tests/data/service_account.json +++ b/tests/data/service_account.json @@ -2,7 +2,7 @@ "type": "service_account", "project_id": "mg-test-1210", "private_key_id": "d5dedce38b8a8d20679c33a0838d954ae9c2553c", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCQiozUlHeHrUsN\nQ34VpMMWfBCStUMb2oPLnhA1LESXGAQ9HIaFzgoDPtxFMxJZ+gJZiRhpb+dmSpN9\naWGxrBlBTWgrymPEmFRAdilMhIDgNMubPlHieD2ZUYyplZoKziiI+92LPKvG6zpE\nh1DltFtnKGXinOfbcTy2zMLGcVfgpid7K8u1txegS9J7OGbqfrJPhs3NJmC4rE5J\nlTdc0U+ZwFWNg82MDZpbMTni67S974msFWRGr/dcH86+8nKMii93GfMX3Vgnkq6Q\nckyFmqdBEhzdVw2PHn5bBJAoTaSVHvAHQCwAizSXEDjDEf/4RQIczZLmV+syJ3Pt\nw5eYyZ4nAgMBAAECggEAaeBzhIw0enggD9kulKAaH3BFm5GXVOHdxNtFuA1zONJo\n2HL6vyzL/NCg/TeJ68rMydU4wpMsB6v9GdKFs2YDIeiXs+wO5MVIgeeMHPa6iIFj\n25Xs2V2GkbZAuUBSlDOrUZxdDk8k7RMFnYkZYwmYIppe935EAGSUOrsGueHwoycz\n58otdgE/f08Jtwwtlmg1eAdB8YTpG+8g5GgyqPX9Pmsl/1QPkkoya4yHuu6ytkMA\nnPqdqpNxczFSD9BcckmmvDPl/gMbvrX2TwQ29LCm0uwpzAVLODlF6z/IglDFJ12a\nqCloWR+0GT32rJbJMKAzSm8j0iM8TKZK7tr5aDElwQKBgQDn53tc4GPekaBXoqU4\n0zTqdZGSArzFcspRFFf+F77pQheskFSvQDVuynGT0RNTxepRqvJM95tyxOUqpeR2\nkL58BJWT1EjJbGiPJWD4ZRiCX2xFPuyBa0V2IFw08ZA3k/MyRhRGgDvzro+QX2pM\nwXmpqB/ByDiFQN9quO0EB7O3cQKBgQCfj0M8Df80O818dLzACEMHuY2Dajei10K6\n3ITjmYQ601/mJDgHnrGnZCxSVstY/ygyRvpQZOKEyjy8QmxW0RdYP7Q8srfGmNsQ\n2tZsk+jM7Txbt/ml8+TRCz28eywtbYQVAkhx4ttjtv5nCjJ3x36Ak0v+Oaen+5zp\ntJ67OZLTFwKBgCOVWVB+/dQA9GF+C2wUvGHdeGC3GtARNQoL3RSYACs6gPdxjgz2\nBTziw1qzEgwgqjutx1AYDjomDCPnII8w1omhCnKMeD6v67tLOP3kRUZ77dkSNqgF\nFbtya7OT/VUJ1p84MZQ/yPMzLcQxX9Y3ObvWmEjbuBB6S83MYlHj/KeBAoGACBSi\nTA1NamDI9E+ZK4R/mImOICSl8qpCJ+J5HGmu56fCyI33BHPF/Xs2P2lD3Rr29yzf\nCmlBi4YOc15NzEvEieSYBSbr5bPiDEV47IDFHnO5Rc/YZc4nPWr7UmtOfnJ4aPP3\npUTe5XrkAWXjzmsc/ff3tkVHN1unw7IxA7xTsjsCgYAma2fYRuBGnOu3BP7AQ2xa\nBdbTyLndP1N3e6BfVufqPM7yWrDCLn97xxscShQg4TYjz0BcATnP1SSsjKtUkQqk\nvAHiB4Q3zWSUFFkaW6iAOJuYZI1jbI+J59C/NRWM+kCRYLLwcsJMP0++QdmKZq3E\n8lhVy3sgqT14vVKsiy+LdQ==\n-----END PRIVATE KEY-----\n", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwJENcRev+eXZKvhhWLiV3Lz2MvO+naQRHo59g3vaNQnbgyduN/L4krlr\nJ5c6FiikXdtJNb/QrsAHSyJWCu8j3T9CruiwbidGAk2W0RuViTVspjHUTsIHExx9euWM0Uom\nGvYkoqXahdhPL/zViVSJt+Rt8bHLsMvpb8RquTIb9iKY3SMV2tCofNmyCSgVbghq/y7lKORt\nV/IRguWs6R22fbkb0r2MCYoNAbZ9dqnbRIFNZBC7itYtUoTEresRWcyFMh0zfAIJycWOJlVL\nDLqkY2SmIx8u7fuysCg1wcoSZoStuDq02nZEMw1dx8HGzE0hynpHlloRLByuIuOAfMCCYwID\nAQABAoIBADFtihu7TspAO0wSUTpqttzgC/nsIsNn95T2UjVLtyjiDNxPZLUrwq42tdCFur0x\nVW9Z+CK5x6DzXWvltlw8IeKKeF1ZEOBVaFzy+YFXKTz835SROcO1fgdjyrme7lRSShGlmKW/\nGKY+baUNquoDLw5qreXaE0SgMp0jt5ktyYuVxvhLDeV4omw2u6waoGkifsGm8lYivg5l3VR7\nw2IVOvYZTt4BuSYVwOM+qjwaS1vtL7gv0SUjrj85Ja6zERRdFiITDhZw6nsvacr9/+/aut9E\naL/koSSb62g5fntQMEwoT4hRnjPnAedmorM9Rhddh2TB3ZKTBbMN1tUk3fJxOuECgYEA+z6l\neSaAcZ3qvwpntcXSpwwJ0SSmzLTH2RJNf+Ld3eBHiSvLTG53dWB7lJtF4R1KcIwf+KGcOFJv\nsnepzcZBylRvT8RrAAkV0s9OiVm1lXZyaepbLg4GGFJBPi8A6VIAj7zYknToRApdW0s1x/XX\nChewfJDckqsevTMovdbg8YkCgYEAxDYX+3mfvv/opo6HNNY3SfVunM+4vVJL+n8gWZ2w9kz3\nQ9Ub9YbRmI7iQaiVkO5xNuoG1n9bM+3Mnm84aQ1YeNT01YqeyQsipP5Wi+um0PzYTaBw9RO+\n8Gh6992OwlJiRtFk5WjalNWOxY4MU0ImnJwIfKQlUODvLmcixm68NYsCgYEAuAqI3jkk55Vd\nKvotREsX5wP7gPePM+7NYiZ1HNQL4Ab1f/bTojZdTV8Sx6YCR0fUiqMqnE+OBvfkGGBtw22S\nLesx6sWf99Ov58+x4Q0U5dpxL0Lb7d2Z+2Dtp+Z4jXFjNeeI4ae/qG/LOR/b0pE0J5F415ap\n7Mpq5v89vepUtrkCgYAjMXytu4v+q1Ikhc4UmRPDrUUQ1WVSd+9u19yKlnFGTFnRjej86hiw\nH3jPxBhHra0a53EgiilmsBGSnWpl1WH4EmJz5vBCKUAmjgQiBrueIqv9iHiaTNdjsanUyaWw\njyxXfXl2eI80QPXh02+8g1H/pzESgjK7Rg1AqnkfVH9nrwKBgQDJVxKBPTw9pigYMVt9iHrR\niCl9zQVjRMbWiPOc0J56+/5FZYm/AOGl9rfhQ9vGxXZYZiOP5FsNkwt05Y1UoAAH4B4VQwbL\nqod71qOcI0ywgZiIR87CYw40gzRfjWnN+YEEW1qfyoNLilEwJB8iB/T+ZePHGmJ4MmQ/cTn9\nxpdLXA==\n-----END RSA PRIVATE KEY-----", "client_email": "test-484@mg-test-1210.iam.gserviceaccount.com", "client_id": "100772742249150578262", "auth_uri": "https://accounts.google.com/o/oauth2/auth", From 3ce270c3da0e9a2b04611a42abd03a8955c6537c Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 22 Mar 2017 13:47:27 -0700 Subject: [PATCH 09/12] Changing mock crypto field names; Implementing other suggested improvements in PR --- tests/data/public_certs.json | 6 +-- tests/data/service_account.json | 10 ++-- tests/test_app.py | 2 +- tests/test_auth.py | 81 +++++++++++++++++---------------- 4 files changed, 52 insertions(+), 47 deletions(-) diff --git a/tests/data/public_certs.json b/tests/data/public_certs.json index 40a4ecfd7..d23f458fc 100644 --- a/tests/data/public_certs.json +++ b/tests/data/public_certs.json @@ -1,5 +1,5 @@ { - "d98d290613ae1468f7e5f5cf604ead38ca9c8358": "-----BEGIN CERTIFICATE-----\nMIIEFTCCAv2gAwIBAgIJALLYfi2oN8cPMA0GCSqGSIb3DQEBCwUAMIGgMQswCQYD\nVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxDzAN\nBgNVBAoMBkdvb2dsZTERMA8GA1UECwwIRmlyZWJhc2UxHDAaBgNVBAMME2ZpcmVi\nYXNlLmdvb2dsZS5jb20xKjAoBgkqhkiG9w0BCQEWG3N1cHBvcnRAZmlyZWJhc2Uu\nZ29vZ2xlLmNvbTAeFw0xNzAzMjIwMDM4MzRaFw0yNzAzMjAwMDM4MzRaMIGgMQsw\nCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcx\nDzANBgNVBAoMBkdvb2dsZTERMA8GA1UECwwIRmlyZWJhc2UxHDAaBgNVBAMME2Zp\ncmViYXNlLmdvb2dsZS5jb20xKjAoBgkqhkiG9w0BCQEWG3N1cHBvcnRAZmlyZWJh\nc2UuZ29vZ2xlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMCR\nDXEXr/nl2Sr4YVi4ldy89jLzvp2kER6OfYN72jUJ24Mnbjfy+JK5ayeXOhYopF3b\nSTW/0K7AB0siVgrvI90/Qq7osG4nRgJNltEblYk1bKYx1E7CBxMcfXrljNFKJhr2\nJKKl2oXYTy/81YlUibfkbfGxy7DL6W/EarkyG/YimN0jFdrQqHzZsgkoFW4Iav8u\n5SjkbVfyEYLlrOkdtn25G9K9jAmKDQG2fXap20SBTWQQu4rWLVKExK3rEVnMhTId\nM3wCCcnFjiZVSwy6pGNkpiMfLu37srAoNcHKEmaErbg6tNp2RDMNXcfBxsxNIcp6\nR5ZaESwcriLjgHzAgmMCAwEAAaNQME4wHQYDVR0OBBYEFGmG5dc2YEEDbFA2+SBS\nA13S5l4VMB8GA1UdIwQYMBaAFGmG5dc2YEEDbFA2+SBSA13S5l4VMAwGA1UdEwQF\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAEmICKB6kq/Y++JKHZg88JS4nlWzIFh\nNBrfyCnMQiL9mmllEXQIhK25xleQwQGsBF2odDj+8H9CG/lwWLmyC5+TryFjWrhn\nHlt8QJb8E4dIZkYAxDL/ii6tXfFTjvrXsTcY2moD6ZoOoxahVOjVfwkHup0ONn2v\nsCL/11FneR0jhgruXKoqrKspgNVuYp+t4IKnnePpeGJb/I3SyS9GUXlScV/uWyRw\nLdIoR2teEWcWeNrMLmth0NSa3AF3gd9+HTaGpESsusG4qPamqiSM7+INAeTo4k8b\nlbqLwo3Ju6cNGGlDSsDXIUahpCdKnqxBALytITmIcHwsR4vYaDP4iOE=\n-----END CERTIFICATE-----", - "525a87bdd5d50522922e6ed2c0216fc442e83e54": "-----BEGIN CERTIFICATE-----\nMIIDKjCCAhKgAwIBAgIIBIUnv7pTIx8wDQYJKoZIhvcNAQEFBQAwODE2MDQGA1UE\nAxMtdGVzdC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29t\nMB4XDTE2MDMxOTE3NTE1NFoXDTE2MDMyMTA2NTE1NFowODE2MDQGA1UEAxMtdGVz\ndC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7beJFmTrA/T4AeMWk/IjxUlGpaxH\n6D1CYbfxEBJUqzuIe7ujaxh76ik/FPQV5WxlL1GOjW0/f5CsmrNaFmTmQbsK4BY3\n3cCd3gM8LcEtmF1I9NxxpXxrZihlfuwbEpb5NpjGPkCC+fG3gTY7qtjuO6e8pGb2\nVQQguOGXKw/YZLZRZXZ41xkQRYrs+tFw48+4YkjMsYJIxyBMiL5Q/HNAQ2IUyZwr\nuc+CMcWyPLNcnsRNXgnPXQD/GKZQnjjJ5KzQAU1vnDcufL9V5KRhb0kRxTTUjE7D\nJl3x4+J6+hbAheZFu9Fntrxie9TvQuQbEBm/437QFYZphfQli0fDjlPHSwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAQzlUGQiWiHgeBZyUsetuoTiQ\nsxzU7B1qw3la/FQrG+jRFr9GE3yjOOxi9JvX16U/ebwSHLUip8UFf/Ir6AJlt/tt\nIjBA6TOd8DysAtr4PCZrAP/m43H9w4lBWdWl1XJE2YfYQgZnorveAMUZqTo0P0pd\nFo3IsYBSTMflKv2Vqz91PPiHgyu2fk+8TYwJT57rnnkS6VzdORTIf+9ZB+J1ye9i\nQN5IgdZ/eqFiJPD8qT5jOcXelWSWqHHdGrNjQNp+z8jgMusY5/ZAlZUe55eo3I0m\nuDSPImLNkDwqY0+bBW6Fp5xi/4O+gJg3cQ+/PeIHzoFqKAlSpxQZSCziPpGfAA==\n-----END CERTIFICATE-----\n", - "d2d687bf7d14cb8a54fbd0f36bcc9c4f32fa84cf": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAeKgAwIBAgIIRKlYUHIlbRkwDQYJKoZIhvcNAQEFBQAwIDEeMBwGA1UE\nAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMB4XDTE2MDIxMDAxMzI1N1oXDTI2MDIw\nNzAxMzI1N1owIDEeMBwGA1UEAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkIqM1JR3h61LDUN+FaTDFnwQkrVD\nG9qDy54QNSxElxgEPRyGhc4KAz7cRTMSWfoCWYkYaW/nZkqTfWlhsawZQU1oK8pj\nxJhUQHYpTISA4DTLmz5R4ng9mVGMqZWaCs4oiPvdizyrxus6RIdQ5bRbZyhl4pzn\n23E8tszCxnFX4KYneyvLtbcXoEvSezhm6n6yT4bNzSZguKxOSZU3XNFPmcBVjYPN\njA2aWzE54uu0ve+JrBVkRq/3XB/OvvJyjIovdxnzF91YJ5KukHJMhZqnQRIc3VcN\njx5+WwSQKE2klR7wB0AsAIs0lxA4wxH/+EUCHM2S5lfrMidz7cOXmMmeJwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAF+V8kmeJQnvpPKlFT74BROi0\n1Eple2mSsyQbtm1kL7FJpl1AXZ4sLXXTVj3ql0LsqVawDCVtUSvDXBSHtejnh0bi\nZ0WUyEEJ38XPfXRilIaTrYP408ezowDaXxrfLhho1EjoMOPgXjksu1FyhBFoHmif\ndLJoxyA4f+8DZ8jj7ew6ZIVEmvONYgctpU72uUh36Vyl84oc9D2GByq/zYDXvVvl\nSKWYZ5+86/eGocO4sosB5QrsEdVGT2Im6mz2DUIewSyIvrDgZ5r3XyL4RXpdi8+8\n9re/meIh5pnhimU4pX9weQia8bqSPf0oZhh0uAWxO5ES7k1GwblnJfxeCZ0xDQ==\n-----END CERTIFICATE-----\n" + "mock-key-id-1": "-----BEGIN CERTIFICATE-----\nMIIEFTCCAv2gAwIBAgIJALLYfi2oN8cPMA0GCSqGSIb3DQEBCwUAMIGgMQswCQYD\nVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxDzAN\nBgNVBAoMBkdvb2dsZTERMA8GA1UECwwIRmlyZWJhc2UxHDAaBgNVBAMME2ZpcmVi\nYXNlLmdvb2dsZS5jb20xKjAoBgkqhkiG9w0BCQEWG3N1cHBvcnRAZmlyZWJhc2Uu\nZ29vZ2xlLmNvbTAeFw0xNzAzMjIwMDM4MzRaFw0yNzAzMjAwMDM4MzRaMIGgMQsw\nCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcx\nDzANBgNVBAoMBkdvb2dsZTERMA8GA1UECwwIRmlyZWJhc2UxHDAaBgNVBAMME2Zp\ncmViYXNlLmdvb2dsZS5jb20xKjAoBgkqhkiG9w0BCQEWG3N1cHBvcnRAZmlyZWJh\nc2UuZ29vZ2xlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMCR\nDXEXr/nl2Sr4YVi4ldy89jLzvp2kER6OfYN72jUJ24Mnbjfy+JK5ayeXOhYopF3b\nSTW/0K7AB0siVgrvI90/Qq7osG4nRgJNltEblYk1bKYx1E7CBxMcfXrljNFKJhr2\nJKKl2oXYTy/81YlUibfkbfGxy7DL6W/EarkyG/YimN0jFdrQqHzZsgkoFW4Iav8u\n5SjkbVfyEYLlrOkdtn25G9K9jAmKDQG2fXap20SBTWQQu4rWLVKExK3rEVnMhTId\nM3wCCcnFjiZVSwy6pGNkpiMfLu37srAoNcHKEmaErbg6tNp2RDMNXcfBxsxNIcp6\nR5ZaESwcriLjgHzAgmMCAwEAAaNQME4wHQYDVR0OBBYEFGmG5dc2YEEDbFA2+SBS\nA13S5l4VMB8GA1UdIwQYMBaAFGmG5dc2YEEDbFA2+SBSA13S5l4VMAwGA1UdEwQF\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAEmICKB6kq/Y++JKHZg88JS4nlWzIFh\nNBrfyCnMQiL9mmllEXQIhK25xleQwQGsBF2odDj+8H9CG/lwWLmyC5+TryFjWrhn\nHlt8QJb8E4dIZkYAxDL/ii6tXfFTjvrXsTcY2moD6ZoOoxahVOjVfwkHup0ONn2v\nsCL/11FneR0jhgruXKoqrKspgNVuYp+t4IKnnePpeGJb/I3SyS9GUXlScV/uWyRw\nLdIoR2teEWcWeNrMLmth0NSa3AF3gd9+HTaGpESsusG4qPamqiSM7+INAeTo4k8b\nlbqLwo3Ju6cNGGlDSsDXIUahpCdKnqxBALytITmIcHwsR4vYaDP4iOE=\n-----END CERTIFICATE-----", + "mock-key-id-2": "-----BEGIN CERTIFICATE-----\nMIIDKjCCAhKgAwIBAgIIBIUnv7pTIx8wDQYJKoZIhvcNAQEFBQAwODE2MDQGA1UE\nAxMtdGVzdC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29t\nMB4XDTE2MDMxOTE3NTE1NFoXDTE2MDMyMTA2NTE1NFowODE2MDQGA1UEAxMtdGVz\ndC00ODQubWctdGVzdC0xMjEwLmlhbS5nc2VydmljZWFjY291bnQuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7beJFmTrA/T4AeMWk/IjxUlGpaxH\n6D1CYbfxEBJUqzuIe7ujaxh76ik/FPQV5WxlL1GOjW0/f5CsmrNaFmTmQbsK4BY3\n3cCd3gM8LcEtmF1I9NxxpXxrZihlfuwbEpb5NpjGPkCC+fG3gTY7qtjuO6e8pGb2\nVQQguOGXKw/YZLZRZXZ41xkQRYrs+tFw48+4YkjMsYJIxyBMiL5Q/HNAQ2IUyZwr\nuc+CMcWyPLNcnsRNXgnPXQD/GKZQnjjJ5KzQAU1vnDcufL9V5KRhb0kRxTTUjE7D\nJl3x4+J6+hbAheZFu9Fntrxie9TvQuQbEBm/437QFYZphfQli0fDjlPHSwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAQzlUGQiWiHgeBZyUsetuoTiQ\nsxzU7B1qw3la/FQrG+jRFr9GE3yjOOxi9JvX16U/ebwSHLUip8UFf/Ir6AJlt/tt\nIjBA6TOd8DysAtr4PCZrAP/m43H9w4lBWdWl1XJE2YfYQgZnorveAMUZqTo0P0pd\nFo3IsYBSTMflKv2Vqz91PPiHgyu2fk+8TYwJT57rnnkS6VzdORTIf+9ZB+J1ye9i\nQN5IgdZ/eqFiJPD8qT5jOcXelWSWqHHdGrNjQNp+z8jgMusY5/ZAlZUe55eo3I0m\nuDSPImLNkDwqY0+bBW6Fp5xi/4O+gJg3cQ+/PeIHzoFqKAlSpxQZSCziPpGfAA==\n-----END CERTIFICATE-----\n", + "mock-key-id-3": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAeKgAwIBAgIIRKlYUHIlbRkwDQYJKoZIhvcNAQEFBQAwIDEeMBwGA1UE\nAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMB4XDTE2MDIxMDAxMzI1N1oXDTI2MDIw\nNzAxMzI1N1owIDEeMBwGA1UEAxMVMTAwNzcyNzQyMjQ5MTUwNTc4MjYyMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkIqM1JR3h61LDUN+FaTDFnwQkrVD\nG9qDy54QNSxElxgEPRyGhc4KAz7cRTMSWfoCWYkYaW/nZkqTfWlhsawZQU1oK8pj\nxJhUQHYpTISA4DTLmz5R4ng9mVGMqZWaCs4oiPvdizyrxus6RIdQ5bRbZyhl4pzn\n23E8tszCxnFX4KYneyvLtbcXoEvSezhm6n6yT4bNzSZguKxOSZU3XNFPmcBVjYPN\njA2aWzE54uu0ve+JrBVkRq/3XB/OvvJyjIovdxnzF91YJ5KukHJMhZqnQRIc3VcN\njx5+WwSQKE2klR7wB0AsAIs0lxA4wxH/+EUCHM2S5lfrMidz7cOXmMmeJwIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAF+V8kmeJQnvpPKlFT74BROi0\n1Eple2mSsyQbtm1kL7FJpl1AXZ4sLXXTVj3ql0LsqVawDCVtUSvDXBSHtejnh0bi\nZ0WUyEEJ38XPfXRilIaTrYP408ezowDaXxrfLhho1EjoMOPgXjksu1FyhBFoHmif\ndLJoxyA4f+8DZ8jj7ew6ZIVEmvONYgctpU72uUh36Vyl84oc9D2GByq/zYDXvVvl\nSKWYZ5+86/eGocO4sosB5QrsEdVGT2Im6mz2DUIewSyIvrDgZ5r3XyL4RXpdi8+8\n9re/meIh5pnhimU4pX9weQia8bqSPf0oZhh0uAWxO5ES7k1GwblnJfxeCZ0xDQ==\n-----END CERTIFICATE-----\n" } diff --git a/tests/data/service_account.json b/tests/data/service_account.json index 7922e24da..ee8357f86 100644 --- a/tests/data/service_account.json +++ b/tests/data/service_account.json @@ -1,12 +1,12 @@ { "type": "service_account", - "project_id": "mg-test-1210", - "private_key_id": "d5dedce38b8a8d20679c33a0838d954ae9c2553c", + "project_id": "mock-project-id", + "private_key_id": "mock-key-id-1", "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwJENcRev+eXZKvhhWLiV3Lz2MvO+naQRHo59g3vaNQnbgyduN/L4krlr\nJ5c6FiikXdtJNb/QrsAHSyJWCu8j3T9CruiwbidGAk2W0RuViTVspjHUTsIHExx9euWM0Uom\nGvYkoqXahdhPL/zViVSJt+Rt8bHLsMvpb8RquTIb9iKY3SMV2tCofNmyCSgVbghq/y7lKORt\nV/IRguWs6R22fbkb0r2MCYoNAbZ9dqnbRIFNZBC7itYtUoTEresRWcyFMh0zfAIJycWOJlVL\nDLqkY2SmIx8u7fuysCg1wcoSZoStuDq02nZEMw1dx8HGzE0hynpHlloRLByuIuOAfMCCYwID\nAQABAoIBADFtihu7TspAO0wSUTpqttzgC/nsIsNn95T2UjVLtyjiDNxPZLUrwq42tdCFur0x\nVW9Z+CK5x6DzXWvltlw8IeKKeF1ZEOBVaFzy+YFXKTz835SROcO1fgdjyrme7lRSShGlmKW/\nGKY+baUNquoDLw5qreXaE0SgMp0jt5ktyYuVxvhLDeV4omw2u6waoGkifsGm8lYivg5l3VR7\nw2IVOvYZTt4BuSYVwOM+qjwaS1vtL7gv0SUjrj85Ja6zERRdFiITDhZw6nsvacr9/+/aut9E\naL/koSSb62g5fntQMEwoT4hRnjPnAedmorM9Rhddh2TB3ZKTBbMN1tUk3fJxOuECgYEA+z6l\neSaAcZ3qvwpntcXSpwwJ0SSmzLTH2RJNf+Ld3eBHiSvLTG53dWB7lJtF4R1KcIwf+KGcOFJv\nsnepzcZBylRvT8RrAAkV0s9OiVm1lXZyaepbLg4GGFJBPi8A6VIAj7zYknToRApdW0s1x/XX\nChewfJDckqsevTMovdbg8YkCgYEAxDYX+3mfvv/opo6HNNY3SfVunM+4vVJL+n8gWZ2w9kz3\nQ9Ub9YbRmI7iQaiVkO5xNuoG1n9bM+3Mnm84aQ1YeNT01YqeyQsipP5Wi+um0PzYTaBw9RO+\n8Gh6992OwlJiRtFk5WjalNWOxY4MU0ImnJwIfKQlUODvLmcixm68NYsCgYEAuAqI3jkk55Vd\nKvotREsX5wP7gPePM+7NYiZ1HNQL4Ab1f/bTojZdTV8Sx6YCR0fUiqMqnE+OBvfkGGBtw22S\nLesx6sWf99Ov58+x4Q0U5dpxL0Lb7d2Z+2Dtp+Z4jXFjNeeI4ae/qG/LOR/b0pE0J5F415ap\n7Mpq5v89vepUtrkCgYAjMXytu4v+q1Ikhc4UmRPDrUUQ1WVSd+9u19yKlnFGTFnRjej86hiw\nH3jPxBhHra0a53EgiilmsBGSnWpl1WH4EmJz5vBCKUAmjgQiBrueIqv9iHiaTNdjsanUyaWw\njyxXfXl2eI80QPXh02+8g1H/pzESgjK7Rg1AqnkfVH9nrwKBgQDJVxKBPTw9pigYMVt9iHrR\niCl9zQVjRMbWiPOc0J56+/5FZYm/AOGl9rfhQ9vGxXZYZiOP5FsNkwt05Y1UoAAH4B4VQwbL\nqod71qOcI0ywgZiIR87CYw40gzRfjWnN+YEEW1qfyoNLilEwJB8iB/T+ZePHGmJ4MmQ/cTn9\nxpdLXA==\n-----END RSA PRIVATE KEY-----", - "client_email": "test-484@mg-test-1210.iam.gserviceaccount.com", - "client_id": "100772742249150578262", + "client_email": "mock-email@mock-project.iam.gserviceaccount.com", + "client_id": "1234567890", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-484%40mg-test-1210.iam.gserviceaccount.com" + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/mock-project-id.iam.gserviceaccount.com" } diff --git a/tests/test_app.py b/tests/test_app.py index 30dbce47a..3c11c0845 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -69,4 +69,4 @@ def test_non_existing_app_get(self, args): @pytest.mark.parametrize('name', invalid_names) def test_app_get_with_invalid_name(self, name): with pytest.raises(ValueError): - firebase.initialize_app(OPTIONS, name) + firebase.get_app(name) diff --git a/tests/test_auth.py b/tests/test_auth.py index 05577f87d..e47d0fa7f 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -12,18 +12,15 @@ from tests import testutils -SERVICE_ACCOUNT_EMAIL = 'test-484@mg-test-1210.iam.gserviceaccount.com' -CLIENT_CERT_URL = ('https://www.googleapis.com/robot/v1/metadata/x509/' - 'test-484%40mg-test-1210.iam.gserviceaccount.com') - FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis.com/' 'google.identity.identitytoolkit.v1.IdentityToolkit') -USER = 'user1' -CREDENTIAL = auth.CertificateCredential( +MOCK_UID = 'user1' +MOCK_CREDENTIAL = auth.CertificateCredential( testutils.resource_filename('service_account.json')) -PUBLIC_CERTS = testutils.resource('public_certs.json') -PRIVATE_KEY = testutils.resource('private_key.pem') +MOCK_PUBLIC_CERTS = testutils.resource('public_certs.json') +MOCK_PRIVATE_KEY = testutils.resource('private_key.pem') +MOCK_SERVICE_ACCOUNT_EMAIL = MOCK_CREDENTIAL.service_account_email class AuthFixture(object): @@ -46,8 +43,8 @@ def verify_id_token(self, *args): return auth.verify_id_token(*args) def setup_module(): - firebase.initialize_app({'credential': CREDENTIAL}) - firebase.initialize_app({'credential': CREDENTIAL}, 'testApp') + firebase.initialize_app({'credential': MOCK_CREDENTIAL}) + firebase.initialize_app({'credential': MOCK_CREDENTIAL}, 'testApp') def teardown_module(): firebase.delete_app('[DEFAULT]') @@ -55,10 +52,23 @@ def teardown_module(): @pytest.fixture(params=[None, 'testApp'], ids=['DefaultApp', 'CustomApp']) def authtest(request): + """Returns an AuthFixture instance. + + Instances returned by this fixture are parameterized to use either the defult App instance, + or a custom App instance named 'testApp'. Due to this parameterization, each test case that + depends on this fixture will get executed twice (as two test cases); once with the default + App, and once with the custom App. + """ return AuthFixture(request.param) @pytest.fixture def non_cert_app(): + """Returns an App instance initialized with a mock non-cert credential. + + The lines of code following the yield statement are guaranteed to run after each test case + that depends on this fixture. This ensures the proper cleanup of the App instance after + tests. + """ app = firebase.initialize_app( {'credential': auth.Credential()}, 'non-cert-app') yield app @@ -69,11 +79,10 @@ def verify_custom_token(custom_token, expected_claims): token = client.verify_id_token( custom_token, FIREBASE_AUDIENCE, - http=testutils.HttpMock(200, PUBLIC_CERTS), - cert_uri=CLIENT_CERT_URL) - assert token['uid'] == USER - assert token['iss'] == SERVICE_ACCOUNT_EMAIL - assert token['sub'] == SERVICE_ACCOUNT_EMAIL + http=testutils.HttpMock(200, MOCK_PUBLIC_CERTS)) + assert token['uid'] == MOCK_UID + assert token['iss'] == MOCK_SERVICE_ACCOUNT_EMAIL + assert token['sub'] == MOCK_SERVICE_ACCOUNT_EMAIL header, _ = jwt.decode(custom_token) assert header.get('typ') == 'JWT' assert header.get('alg') == 'RS256' @@ -89,17 +98,16 @@ def _merge_jwt_claims(defaults, overrides): return defaults def get_id_token(payload_overrides=None, header_overrides=None): - signer = crypt.Signer.from_string(PRIVATE_KEY) + signer = crypt.Signer.from_string(MOCK_PRIVATE_KEY) headers = { - 'kid': 'd98d290613ae1468f7e5f5cf604ead38ca9c8358' + 'kid': 'mock-key-id-1' } payload = { - 'aud': 'mg-test-1210', - 'iss': 'https://securetoken.google.com/mg-test-1210', + 'aud': MOCK_CREDENTIAL.project_id, + 'iss': 'https://securetoken.google.com/' + MOCK_CREDENTIAL.project_id, 'iat': int(time.time()) - 100, 'exp': int(time.time()) + 3600, 'sub': '1234567890', - 'uid': USER, 'admin': True, } if header_overrides: @@ -112,9 +120,9 @@ def get_id_token(payload_overrides=None, header_overrides=None): class TestCreateCustomToken(object): valid_args = { - 'Basic': (USER, {'one': 2, 'three': 'four'}), - 'NoDevClaims': (USER, None), - 'EmptyDevClaims': (USER, {}), + 'Basic': (MOCK_UID, {'one': 2, 'three': 'four'}), + 'NoDevClaims': (MOCK_UID, None), + 'EmptyDevClaims': (MOCK_UID, {}), } invalid_args = { @@ -126,12 +134,12 @@ class TestCreateCustomToken(object): 'ListUid': ([], None, ValueError), 'EmptyDictUid': ({}, None, ValueError), 'NonEmptyDictUid': ({'a':1}, None, ValueError), - 'BoolClaims': (USER, True, ValueError), - 'IntClaims': (USER, 1, ValueError), - 'StrClaims': (USER, 'foo', ValueError), - 'ListClaims': (USER, [], ValueError), - 'TupleClaims': (USER, (1, 2), ValueError), - 'ReservedClaims': (USER, {'sub':'1234'}, ValueError), + 'BoolClaims': (MOCK_UID, True, ValueError), + 'IntClaims': (MOCK_UID, 1, ValueError), + 'StrClaims': (MOCK_UID, 'foo', ValueError), + 'ListClaims': (MOCK_UID, [], ValueError), + 'TupleClaims': (MOCK_UID, (1, 2), ValueError), + 'ReservedClaims': (MOCK_UID, {'sub':'1234'}, ValueError), } @pytest.mark.parametrize('user,claims', valid_args.values(), @@ -147,7 +155,7 @@ def test_invalid_params(self, authtest, user, claims, error): def test_noncert_credential(self, non_cert_app): with pytest.raises(ValueError): - auth.create_custom_token(USER, app=non_cert_app) + auth.create_custom_token(MOCK_UID, app=non_cert_app) class TestVerifyIdToken(object): @@ -187,13 +195,12 @@ class TestVerifyIdToken(object): } def setup_method(self): - auth._http = testutils.HttpMock(200, PUBLIC_CERTS) + auth._http = testutils.HttpMock(200, MOCK_PUBLIC_CERTS) def test_valid_token(self, authtest): id_token = get_id_token() claims = authtest.verify_id_token(id_token) assert claims['admin'] is True - assert claims['uid'] == USER @pytest.mark.parametrize('id_token,error', invalid_tokens.values(), ids=invalid_tokens.keys()) @@ -205,10 +212,9 @@ def test_project_id_env_var(self, non_cert_app): id_token = get_id_token() gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) try: - os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = 'mg-test-1210' + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = MOCK_CREDENTIAL.project_id claims = auth.verify_id_token(id_token, non_cert_app) assert claims['admin'] is True - assert claims['uid'] == USER finally: if gcloud_project: os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project @@ -217,9 +223,8 @@ def test_project_id_env_var(self, non_cert_app): def test_no_project_id(self, non_cert_app): id_token = get_id_token() - gcloud_project = None - if os.environ.has_key(auth.GCLOUD_PROJECT_ENV_VAR): - gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) + gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) + if gcloud_project: del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] try: with pytest.raises(ValueError): @@ -229,7 +234,7 @@ def test_no_project_id(self, non_cert_app): os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project def test_custom_token(self, authtest): - id_token = authtest.create_custom_token(USER) + id_token = authtest.create_custom_token(MOCK_UID) with pytest.raises(crypt.AppIdentityError): authtest.verify_id_token(id_token) From aa80ca9330fc8350e94577524a107e3bf00fd1c6 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 22 Mar 2017 13:58:57 -0700 Subject: [PATCH 10/12] Reformatted file to get rid of unnecessary whitespace characters --- tests/test_auth.py | 490 ++++++++++++++++++++++----------------------- 1 file changed, 245 insertions(+), 245 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index e47d0fa7f..3bdf26f61 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,245 +1,245 @@ -"""Test cases for firebase.auth module.""" -import os -import time - -from oauth2client import client -from oauth2client import crypt -import pytest - -import firebase -from firebase import auth -from firebase import jwt -from tests import testutils - - -FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis.com/' - 'google.identity.identitytoolkit.v1.IdentityToolkit') - -MOCK_UID = 'user1' -MOCK_CREDENTIAL = auth.CertificateCredential( - testutils.resource_filename('service_account.json')) -MOCK_PUBLIC_CERTS = testutils.resource('public_certs.json') -MOCK_PRIVATE_KEY = testutils.resource('private_key.pem') -MOCK_SERVICE_ACCOUNT_EMAIL = MOCK_CREDENTIAL.service_account_email - - -class AuthFixture(object): - def __init__(self, name=None): - if name: - self.app = firebase.get_app(name) - else: - self.app = None - - def create_custom_token(self, *args): - if self.app: - return auth.create_custom_token(*args, app=self.app) - else: - return auth.create_custom_token(*args) - - def verify_id_token(self, *args): - if self.app: - return auth.verify_id_token(*args, app=self.app) - else: - return auth.verify_id_token(*args) - -def setup_module(): - firebase.initialize_app({'credential': MOCK_CREDENTIAL}) - firebase.initialize_app({'credential': MOCK_CREDENTIAL}, 'testApp') - -def teardown_module(): - firebase.delete_app('[DEFAULT]') - firebase.delete_app('testApp') - -@pytest.fixture(params=[None, 'testApp'], ids=['DefaultApp', 'CustomApp']) -def authtest(request): - """Returns an AuthFixture instance. - - Instances returned by this fixture are parameterized to use either the defult App instance, - or a custom App instance named 'testApp'. Due to this parameterization, each test case that - depends on this fixture will get executed twice (as two test cases); once with the default - App, and once with the custom App. - """ - return AuthFixture(request.param) - -@pytest.fixture -def non_cert_app(): - """Returns an App instance initialized with a mock non-cert credential. - - The lines of code following the yield statement are guaranteed to run after each test case - that depends on this fixture. This ensures the proper cleanup of the App instance after - tests. - """ - app = firebase.initialize_app( - {'credential': auth.Credential()}, 'non-cert-app') - yield app - firebase.delete_app(app.name) - -def verify_custom_token(custom_token, expected_claims): - assert isinstance(custom_token, basestring) - token = client.verify_id_token( - custom_token, - FIREBASE_AUDIENCE, - http=testutils.HttpMock(200, MOCK_PUBLIC_CERTS)) - assert token['uid'] == MOCK_UID - assert token['iss'] == MOCK_SERVICE_ACCOUNT_EMAIL - assert token['sub'] == MOCK_SERVICE_ACCOUNT_EMAIL - header, _ = jwt.decode(custom_token) - assert header.get('typ') == 'JWT' - assert header.get('alg') == 'RS256' - if expected_claims: - for key, value in expected_claims.items(): - assert value == token['claims'][key] - -def _merge_jwt_claims(defaults, overrides): - defaults.update(overrides) - for key, value in overrides.items(): - if value is None: - del defaults[key] - return defaults - -def get_id_token(payload_overrides=None, header_overrides=None): - signer = crypt.Signer.from_string(MOCK_PRIVATE_KEY) - headers = { - 'kid': 'mock-key-id-1' - } - payload = { - 'aud': MOCK_CREDENTIAL.project_id, - 'iss': 'https://securetoken.google.com/' + MOCK_CREDENTIAL.project_id, - 'iat': int(time.time()) - 100, - 'exp': int(time.time()) + 3600, - 'sub': '1234567890', - 'admin': True, - } - if header_overrides: - headers = _merge_jwt_claims(headers, header_overrides) - if payload_overrides: - payload = _merge_jwt_claims(payload, payload_overrides) - return jwt.encode(payload, signer, headers=headers) - - -class TestCreateCustomToken(object): - - valid_args = { - 'Basic': (MOCK_UID, {'one': 2, 'three': 'four'}), - 'NoDevClaims': (MOCK_UID, None), - 'EmptyDevClaims': (MOCK_UID, {}), - } - - invalid_args = { - 'NoUid': (None, None, ValueError), - 'EmptyUid': ('', None, ValueError), - 'LongUid': ('x'*129, None, ValueError), - 'BoolUid': (True, None, ValueError), - 'IntUid': (1, None, ValueError), - 'ListUid': ([], None, ValueError), - 'EmptyDictUid': ({}, None, ValueError), - 'NonEmptyDictUid': ({'a':1}, None, ValueError), - 'BoolClaims': (MOCK_UID, True, ValueError), - 'IntClaims': (MOCK_UID, 1, ValueError), - 'StrClaims': (MOCK_UID, 'foo', ValueError), - 'ListClaims': (MOCK_UID, [], ValueError), - 'TupleClaims': (MOCK_UID, (1, 2), ValueError), - 'ReservedClaims': (MOCK_UID, {'sub':'1234'}, ValueError), - } - - @pytest.mark.parametrize('user,claims', valid_args.values(), - ids=valid_args.keys()) - def test_valid_params(self, authtest, user, claims): - verify_custom_token(authtest.create_custom_token(user, claims), claims) - - @pytest.mark.parametrize('user,claims,error', invalid_args.values(), - ids=invalid_args.keys()) - def test_invalid_params(self, authtest, user, claims, error): - with pytest.raises(error): - authtest.create_custom_token(user, claims) - - def test_noncert_credential(self, non_cert_app): - with pytest.raises(ValueError): - auth.create_custom_token(MOCK_UID, app=non_cert_app) - - -class TestVerifyIdToken(object): - - invalid_tokens = { - 'NoKid': (get_id_token(header_overrides={'kid': None}), - crypt.AppIdentityError), - 'WrongKid': (get_id_token(header_overrides={'kid': 'foo'}), - client.VerifyJwtTokenError), - 'WrongAlg': (get_id_token(header_overrides={'alg': 'HS256'}), - crypt.AppIdentityError), - 'BadAudience': (get_id_token({'aud': 'bad-audience'}), - crypt.AppIdentityError), - 'BadIssuer': (get_id_token({ - 'iss': 'https://securetoken.google.com/wrong-issuer' - }), crypt.AppIdentityError), - 'EmptySubject': (get_id_token({'sub': ''}), - crypt.AppIdentityError), - 'IntSubject': (get_id_token({'sub': 10}), - crypt.AppIdentityError), - 'LongStrSubject': (get_id_token({'sub': 'a' * 129}), - crypt.AppIdentityError), - 'FutureToken': (get_id_token({'iat': int(time.time()) + 1000}), - crypt.AppIdentityError), - 'ExpiredToken': (get_id_token({ - 'iat': int(time.time()) - 10000, - 'exp': int(time.time()) - 3600 - }), crypt.AppIdentityError), - 'NoneToken': (None, ValueError), - 'EmptyToken': ('', ValueError), - 'BoolToken': (True, ValueError), - 'IntToken': (1, ValueError), - 'ListToken': ([], ValueError), - 'EmptyDictToken': ({}, ValueError), - 'NonEmptyDictToken': ({'a': 1}, ValueError), - 'BadFormatToken': ('foobar', crypt.AppIdentityError) - } - - def setup_method(self): - auth._http = testutils.HttpMock(200, MOCK_PUBLIC_CERTS) - - def test_valid_token(self, authtest): - id_token = get_id_token() - claims = authtest.verify_id_token(id_token) - assert claims['admin'] is True - - @pytest.mark.parametrize('id_token,error', invalid_tokens.values(), - ids=invalid_tokens.keys()) - def test_invalid_token(self, authtest, id_token, error): - with pytest.raises(error): - authtest.verify_id_token(id_token) - - def test_project_id_env_var(self, non_cert_app): - id_token = get_id_token() - gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) - try: - os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = MOCK_CREDENTIAL.project_id - claims = auth.verify_id_token(id_token, non_cert_app) - assert claims['admin'] is True - finally: - if gcloud_project: - os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project - else: - del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] - - def test_no_project_id(self, non_cert_app): - id_token = get_id_token() - gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) - if gcloud_project: - del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] - try: - with pytest.raises(ValueError): - auth.verify_id_token(id_token, non_cert_app) - finally: - if gcloud_project: - os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project - - def test_custom_token(self, authtest): - id_token = authtest.create_custom_token(MOCK_UID) - with pytest.raises(crypt.AppIdentityError): - authtest.verify_id_token(id_token) - - def test_certificate_request_failure(self, authtest): - id_token = get_id_token() - auth._http = testutils.HttpMock(404, 'not found') - with pytest.raises(client.VerifyJwtTokenError): - authtest.verify_id_token(id_token) +"""Test cases for firebase.auth module.""" +import os +import time + +from oauth2client import client +from oauth2client import crypt +import pytest + +import firebase +from firebase import auth +from firebase import jwt +from tests import testutils + + +FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis.com/' + 'google.identity.identitytoolkit.v1.IdentityToolkit') + +MOCK_UID = 'user1' +MOCK_CREDENTIAL = auth.CertificateCredential( + testutils.resource_filename('service_account.json')) +MOCK_PUBLIC_CERTS = testutils.resource('public_certs.json') +MOCK_PRIVATE_KEY = testutils.resource('private_key.pem') +MOCK_SERVICE_ACCOUNT_EMAIL = MOCK_CREDENTIAL.service_account_email + + +class AuthFixture(object): + def __init__(self, name=None): + if name: + self.app = firebase.get_app(name) + else: + self.app = None + + def create_custom_token(self, *args): + if self.app: + return auth.create_custom_token(*args, app=self.app) + else: + return auth.create_custom_token(*args) + + def verify_id_token(self, *args): + if self.app: + return auth.verify_id_token(*args, app=self.app) + else: + return auth.verify_id_token(*args) + +def setup_module(): + firebase.initialize_app({'credential': MOCK_CREDENTIAL}) + firebase.initialize_app({'credential': MOCK_CREDENTIAL}, 'testApp') + +def teardown_module(): + firebase.delete_app('[DEFAULT]') + firebase.delete_app('testApp') + +@pytest.fixture(params=[None, 'testApp'], ids=['DefaultApp', 'CustomApp']) +def authtest(request): + """Returns an AuthFixture instance. + + Instances returned by this fixture are parameterized to use either the defult App instance, + or a custom App instance named 'testApp'. Due to this parameterization, each test case that + depends on this fixture will get executed twice (as two test cases); once with the default + App, and once with the custom App. + """ + return AuthFixture(request.param) + +@pytest.fixture +def non_cert_app(): + """Returns an App instance initialized with a mock non-cert credential. + + The lines of code following the yield statement are guaranteed to run after each test case + that depends on this fixture. This ensures the proper cleanup of the App instance after + tests. + """ + app = firebase.initialize_app( + {'credential': auth.Credential()}, 'non-cert-app') + yield app + firebase.delete_app(app.name) + +def verify_custom_token(custom_token, expected_claims): + assert isinstance(custom_token, basestring) + token = client.verify_id_token( + custom_token, + FIREBASE_AUDIENCE, + http=testutils.HttpMock(200, MOCK_PUBLIC_CERTS)) + assert token['uid'] == MOCK_UID + assert token['iss'] == MOCK_SERVICE_ACCOUNT_EMAIL + assert token['sub'] == MOCK_SERVICE_ACCOUNT_EMAIL + header, _ = jwt.decode(custom_token) + assert header.get('typ') == 'JWT' + assert header.get('alg') == 'RS256' + if expected_claims: + for key, value in expected_claims.items(): + assert value == token['claims'][key] + +def _merge_jwt_claims(defaults, overrides): + defaults.update(overrides) + for key, value in overrides.items(): + if value is None: + del defaults[key] + return defaults + +def get_id_token(payload_overrides=None, header_overrides=None): + signer = crypt.Signer.from_string(MOCK_PRIVATE_KEY) + headers = { + 'kid': 'mock-key-id-1' + } + payload = { + 'aud': MOCK_CREDENTIAL.project_id, + 'iss': 'https://securetoken.google.com/' + MOCK_CREDENTIAL.project_id, + 'iat': int(time.time()) - 100, + 'exp': int(time.time()) + 3600, + 'sub': '1234567890', + 'admin': True, + } + if header_overrides: + headers = _merge_jwt_claims(headers, header_overrides) + if payload_overrides: + payload = _merge_jwt_claims(payload, payload_overrides) + return jwt.encode(payload, signer, headers=headers) + + +class TestCreateCustomToken(object): + + valid_args = { + 'Basic': (MOCK_UID, {'one': 2, 'three': 'four'}), + 'NoDevClaims': (MOCK_UID, None), + 'EmptyDevClaims': (MOCK_UID, {}), + } + + invalid_args = { + 'NoUid': (None, None, ValueError), + 'EmptyUid': ('', None, ValueError), + 'LongUid': ('x'*129, None, ValueError), + 'BoolUid': (True, None, ValueError), + 'IntUid': (1, None, ValueError), + 'ListUid': ([], None, ValueError), + 'EmptyDictUid': ({}, None, ValueError), + 'NonEmptyDictUid': ({'a':1}, None, ValueError), + 'BoolClaims': (MOCK_UID, True, ValueError), + 'IntClaims': (MOCK_UID, 1, ValueError), + 'StrClaims': (MOCK_UID, 'foo', ValueError), + 'ListClaims': (MOCK_UID, [], ValueError), + 'TupleClaims': (MOCK_UID, (1, 2), ValueError), + 'ReservedClaims': (MOCK_UID, {'sub':'1234'}, ValueError), + } + + @pytest.mark.parametrize('user,claims', valid_args.values(), + ids=valid_args.keys()) + def test_valid_params(self, authtest, user, claims): + verify_custom_token(authtest.create_custom_token(user, claims), claims) + + @pytest.mark.parametrize('user,claims,error', invalid_args.values(), + ids=invalid_args.keys()) + def test_invalid_params(self, authtest, user, claims, error): + with pytest.raises(error): + authtest.create_custom_token(user, claims) + + def test_noncert_credential(self, non_cert_app): + with pytest.raises(ValueError): + auth.create_custom_token(MOCK_UID, app=non_cert_app) + + +class TestVerifyIdToken(object): + + invalid_tokens = { + 'NoKid': (get_id_token(header_overrides={'kid': None}), + crypt.AppIdentityError), + 'WrongKid': (get_id_token(header_overrides={'kid': 'foo'}), + client.VerifyJwtTokenError), + 'WrongAlg': (get_id_token(header_overrides={'alg': 'HS256'}), + crypt.AppIdentityError), + 'BadAudience': (get_id_token({'aud': 'bad-audience'}), + crypt.AppIdentityError), + 'BadIssuer': (get_id_token({ + 'iss': 'https://securetoken.google.com/wrong-issuer' + }), crypt.AppIdentityError), + 'EmptySubject': (get_id_token({'sub': ''}), + crypt.AppIdentityError), + 'IntSubject': (get_id_token({'sub': 10}), + crypt.AppIdentityError), + 'LongStrSubject': (get_id_token({'sub': 'a' * 129}), + crypt.AppIdentityError), + 'FutureToken': (get_id_token({'iat': int(time.time()) + 1000}), + crypt.AppIdentityError), + 'ExpiredToken': (get_id_token({ + 'iat': int(time.time()) - 10000, + 'exp': int(time.time()) - 3600 + }), crypt.AppIdentityError), + 'NoneToken': (None, ValueError), + 'EmptyToken': ('', ValueError), + 'BoolToken': (True, ValueError), + 'IntToken': (1, ValueError), + 'ListToken': ([], ValueError), + 'EmptyDictToken': ({}, ValueError), + 'NonEmptyDictToken': ({'a': 1}, ValueError), + 'BadFormatToken': ('foobar', crypt.AppIdentityError) + } + + def setup_method(self): + auth._http = testutils.HttpMock(200, MOCK_PUBLIC_CERTS) + + def test_valid_token(self, authtest): + id_token = get_id_token() + claims = authtest.verify_id_token(id_token) + assert claims['admin'] is True + + @pytest.mark.parametrize('id_token,error', invalid_tokens.values(), + ids=invalid_tokens.keys()) + def test_invalid_token(self, authtest, id_token, error): + with pytest.raises(error): + authtest.verify_id_token(id_token) + + def test_project_id_env_var(self, non_cert_app): + id_token = get_id_token() + gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) + try: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = MOCK_CREDENTIAL.project_id + claims = auth.verify_id_token(id_token, non_cert_app) + assert claims['admin'] is True + finally: + if gcloud_project: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project + else: + del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] + + def test_no_project_id(self, non_cert_app): + id_token = get_id_token() + gcloud_project = os.environ.get(auth.GCLOUD_PROJECT_ENV_VAR) + if gcloud_project: + del os.environ[auth.GCLOUD_PROJECT_ENV_VAR] + try: + with pytest.raises(ValueError): + auth.verify_id_token(id_token, non_cert_app) + finally: + if gcloud_project: + os.environ[auth.GCLOUD_PROJECT_ENV_VAR] = gcloud_project + + def test_custom_token(self, authtest): + id_token = authtest.create_custom_token(MOCK_UID) + with pytest.raises(crypt.AppIdentityError): + authtest.verify_id_token(id_token) + + def test_certificate_request_failure(self, authtest): + id_token = get_id_token() + auth._http = testutils.HttpMock(404, 'not found') + with pytest.raises(client.VerifyJwtTokenError): + authtest.verify_id_token(id_token) From 5edcd32cee13c2fe51a9dc833140902c3a304b18 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 23 Mar 2017 13:28:56 -0700 Subject: [PATCH 11/12] Added initial tox configuration; Added setup.py for packaging releases; Added contribution guide --- .github/CONTRIBUTING.md | 191 ++++++++++++++++++++++++++++++++++++++++ README.md | 91 ++++--------------- setup.py | 95 ++++++++++++++++++++ tox.ini | 13 +++ 4 files changed, 318 insertions(+), 72 deletions(-) create mode 100644 .github/CONTRIBUTING.md create mode 100644 setup.py create mode 100644 tox.ini diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..c25ce6dd2 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,191 @@ +# Contributing | Firebase Admin Python SDK + +Thank you for contributing to the Firebase community! + + - [Have a usage question?](#question) + - [Think you found a bug?](#issue) + - [Have a feature request?](#feature) + - [Want to submit a pull request?](#submit) + - [Need to get set up locally?](#local-setup) + + +## Have a usage question? + +We get lots of those and we love helping you, but GitHub is not the best place for them. Issues +which just ask about usage will be closed. Here are some resources to get help: + +- Go through the [guides](https://firebase.google.com/docs/admin/setup/) +- Read the full [API reference](https://firebase.google.com/docs/reference/admin/node/) + +If the official documentation doesn't help, try asking a question on the +[Firebase Google Group](https://groups.google.com/forum/#!forum/firebase-talk/) or one of our +other [official support channels](https://firebase.google.com/support/). + +**Please avoid double posting across multiple channels!** + + +## Think you found a bug? + +Yeah, we're definitely not perfect! + +Search through [old issues](https://github.com/firebase/firebase-admin-python/issues) before +submitting a new issue as your question may have already been answered. + +If your issue appears to be a bug, and hasn't been reported, +[open a new issue](https://github.com/firebase/firebase-admin-python/issues/new). Please use the +provided bug report template and include a minimal repro. + +If you are up to the challenge, [submit a pull request](#submit) with a fix! + + +## Have a feature request? + +Great, we love hearing how we can improve our products! Share you idea through our +[feature request support channel](https://firebase.google.com/support/contact/bugs-features/). + + +## Want to submit a pull request? + +Sweet, we'd love to accept your contribution! +[Open a new pull request](https://github.com/firebase/firebase-admin-python/pull/new/master) and fill +out the provided template. + +**If you want to implement a new feature, please open an issue with a proposal first so that we can +figure out if the feature makes sense and how it will work.** + +Make sure your changes pass our linter and the tests all pass on your local machine. +Most non-trivial changes should include some extra test coverage. If you aren't sure how to add +tests, feel free to submit regardless and ask us for some advice. + +Finally, you will need to sign our +[Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +before we can accept your pull request. + + +## Need to get set up locally? + + +### Initial Setup + +Run the following commands from the command line to get your local environment set up: + +```bash +$ git clone https://github.com/firebase/firebase-admin-python.git +$ cd firebase-admin-python # go to the firebase-admin-python directory +$ pip install -U pytest # globally install pytest test framework and executor +$ pip install -U pylint # globally install pylint code quality checker +``` + +### Running Linters + +We use [pylint](https://pylint.org/) for verifying source code format, and +enforcing other Python programming best practices. Install pylint 1.6.4 or +higher using pip: + +``` +pip install -U pylint +``` + +Specify a pylint version explicitly if the above command installs an older +version: + +``` +pip install pylint==1.6.4 +``` + +Once installed, you can check the version of the installed binary by running +the following command: + +``` +pylint --version +``` + +There is a pylint configuration file (`.pylintrc`) at the root of this Git +repository. This enables you to invoke pylint directly from the command line: + +``` +pylint firebase +``` + +However, it is recommended that you use the `lint.sh` bash script to invoke +pylint. This script will run the linter on both `firebase` and the corresponding +`tests` module. It suprresses some of the noisy warnings that get generated +when running pylint on test code. Note that by default `lint.sh` will only +validate the locally modified source files. To validate all source files, +pass `all` as an argument. + +``` +./lint.sh # Lint locally modified source files +./lint.sh all # Lint all source files +``` + +Ideally you should not see any pylint errors or warnings when you run the +linter. This means source files are properly formatted, and the linter has +not found any issues. If you do observe any errors, fix them before +committing or sending a pull request. Details on how to interpret pylint +errors are available +[here](https://pylint.readthedocs.io/en/latest/user_guide/output.html). + +Our configuration files suppress the verbose reports usually generated +by pylint, and only output the detected issues. If you wish to obtain the +comprehensive reports, run pylint from command-line with the `-r` flag. + +``` +pylint -r yes firebase +``` + +### Unit Testing + +We use [pytest](http://doc.pytest.org/en/latest/) for writing and executing +unit tests. Download pytest 3.0.6 or higher using pip: + +``` +pip install -U pytest +``` + +All source files containing test code is located in the `tests/` +directory. Simply launch pytest from the root of the Git repository, or from +within the `tests/` directory to execute all test cases. + +``` +pytest +``` +Refer to the pytest [usage and invocations guide](http://doc.pytest.org/en/latest/usage.html) +to learn how to run a subset of all test cases. + +### Testing on Different Platforms + +Sometimes we may want to run unit tests in multiple environments (e.g. different +Python versions), and ensure that the SDK works as expected in each of them. +We use [tox](https://tox.readthedocs.io/en/latest/) for this purpose. Install +the latest version of tox using pip: + +``` +pip install -U tox +``` + +Now you can execute the following command from the root of the repository: + +``` +tox +``` + +This command will read a list of target environments from the `tox.ini` file +in the Git repository, and execute test cases in each of those environments. +We currently define the following target environments in `tox.ini`: + + * python 2.7 + + +### Repo Organization + +Here are some highlights of the directory structure and notable source files + +* `firebase/` - Source directory for the `firebase` module. +* `tests/` - Unit tests. + * `data/` - Provides mocks for several variables as well as mock service account keys. +* `.github/` - Contribution instructions as well as issue and pull request templates. +* `lint.sh` - Runs pylint to check for code quality. +* `.pylintrc` - Default configuration for pylint. +* `setup.py` - Python setup script for building distribution artifacts. +* `tox.ini` - Tox configuration for running tests on different environments. diff --git a/README.md b/README.md index aa57b3060..dc450aacc 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,31 @@ # Firebase Admin Python SDK -The Firebase Admin Python SDK enables server-side (backend) Python developers -to integrate [Firebase](https://firebase.google.com) into their services -and applications. Currently this SDK provides Firebase custom authentication -support. +## Table of Contents + * [Overview](#overview) + * [Documentation](#documentation) + * [License and Terms](#license-and-terms) -## Unit Testing -We use [pytest](http://doc.pytest.org/en/latest/) for writing and executing -unit tests. Download pytest 3.0.6 or higher using pip: +## Overview -``` -pip install -U pytest -``` +[Firebase](https://firebase.google.com) provides the tools and infrastructure +you need to develop apps, grow your user base, and earn money. The Firebase +Admin Python SDK enables server-side (backend) Python developers to integrate +Firebase into their services and applications. Currently this SDK provides +Firebase custom authentication support. -All source files containing test code is located in the `tests/` -directory. Simply launch pytest from the root of the Git repository, or from -within the `tests/` directory to execute all test cases. +For more information, visit the +[Firebase Admin SDK setup guide](https://firebase.google.com/docs/admin/setup/). -``` -pytest -``` -Refer to the pytest [usage and invocations guide](http://doc.pytest.org/en/latest/usage.html) -to learn how to run a subset of all test cases. +## Documentation +* [Setup Guide](https://firebase.google.com/docs/admin/setup/) +* [Database Guide](https://firebase.google.com/docs/database/admin/start/) +* [Authentication Guide](https://firebase.google.com/docs/auth/admin/) -## Running Linters -We use [pylint](https://pylint.org/) for verifying source code format, and -enforcing other Python programming best practices. Install pylint 1.6.4 or -higher using pip: -``` -pip install -U pylint -``` +## License and Terms -Specify a pylint version explicitly if the above command installs an older -version: - -``` -pip install pylint==1.6.4 -``` - -Once installed, you can check the version of the installed binary by running -the following command: - -``` -pylint --version -``` - -There is a pylint configuration file (`.pylintrc`) at the root of this Git -repository. This enables you to invoke pylint directly from the command line: - -``` -pylint firebase -``` - -However, it is recommended that you use the `lint.sh` bash script to invoke -pylint. This script will run the linter on both `firebase` and the corresponding -`tests` module. It suprresses some of the noisy warnings that get generated -when running pylint on test code. Note that by default `lint.sh` will only -validate the locally modified source files. To validate all source files, -pass `all` as an argument. - -``` -./lint.sh # Lint locally modified source files -./lint.sh all # Lint all source files -``` - -Ideally you should not see any pylint errors or warnings when you run the -linter. This means source files are properly formatted, and the linter has -not found any issues. If you do observe any errors, fix them before -committing or sending a pull request. Details on how to interpret pylint -errors are available -[here](https://pylint.readthedocs.io/en/latest/user_guide/output.html). - -Our configuration files suppress the verbose reports usually generated -by pylint, and only output the detected issues. If you wish to obtain the -comprehensive reports, run pylint from command-line with the `-r` flag. - -``` -pylint -r yes firebase -``` +Your use of Firebase is governed by the +[Terms of Service for Firebase Services](https://firebase.google.com/terms/). diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..f0837edf5 --- /dev/null +++ b/setup.py @@ -0,0 +1,95 @@ +"""Setup file for distribution artifacts.""" +from os import path +from setuptools import setup, find_packages + + +here = path.abspath(path.dirname(__file__)) + +long_description = ('The Firebase Admin Python SDK enables server-side (backend) Python developers ' + 'to integrate Firebase into their services and applications. Currently this ' + 'SDK provides Firebase custom authentication support.') + +setup( + name='firebase', + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version='0.0.1', + + description='Firebase Admin Python SDK', + long_description=long_description, + + # The project's main homepage. + url='https://github.com/pypa/sampleproject', + + # Author details + author='Firebase Team', + author_email='firebase-talk@googlegroups.com', + + # Choose your license + license='Apache Software License 2.0', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 3 - Alpha', + + # Indicate who your project is intended for + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: Apache Software License', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + ], + + # What does your project relate to? + keywords='firebase cloud development', + + packages=find_packages(exclude=['tests']), + + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=['oauth2client'], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + #extras_require={ + # 'dev': ['check-manifest'], + # 'test': ['coverage'], + #}, + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + #package_data={ + # 'sample': ['package_data.dat'], + #}, + + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. See: + # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa + # In this case, 'data_file' will be installed into '/my_data' + #data_files=[('my_data', ['data/data_file'])], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + #entry_points={ + # 'console_scripts': [ + # 'sample=sample:main', + # ], + #}, +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..b27c99c4a --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +# Tox (https://tox.readthedocs.io/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py27 + +[testenv] +commands = pytest +deps = + pytest + oauth2client From 4e1d002a08d35136f9740a6a4e4fa47b29b88079 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 23 Mar 2017 13:30:32 -0700 Subject: [PATCH 12/12] Updated readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index dc450aacc..a99e7713e 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,6 @@ For more information, visit the ## Documentation * [Setup Guide](https://firebase.google.com/docs/admin/setup/) -* [Database Guide](https://firebase.google.com/docs/database/admin/start/) -* [Authentication Guide](https://firebase.google.com/docs/auth/admin/) ## License and Terms