From 2e5a94bc0b6c40d536e2c09b02f8fafcc0ef034c Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Wed, 31 Aug 2016 14:31:43 -0400 Subject: [PATCH] create key server data interface --- data/interfaces/key_server.py | 122 ++++++++++++++++++ .../{key_server.py => keyserver/__init__.py} | 31 ++--- test/test_endpoints.py | 6 +- web.py | 2 +- 4 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 data/interfaces/key_server.py rename endpoints/{key_server.py => keyserver/__init__.py} (84%) diff --git a/data/interfaces/key_server.py b/data/interfaces/key_server.py new file mode 100644 index 000000000..81cf43312 --- /dev/null +++ b/data/interfaces/key_server.py @@ -0,0 +1,122 @@ +from collections import namedtuple + +import data.model + + + +class ServiceKey(namedtuple('ServiceKey', ['name', 'kid', 'service', 'jwk', 'metadata', + 'created_date', 'expiration_date', 'rotation_duration', + 'approval'])): + """ + Service Key represents a public key (JWK) being used by an instance of a particular service to + authenticate with other services. + """ + pass + + +class ServiceKeyException(Exception): + pass + + +class ServiceKeyDoesNotExist(ServiceKeyException): + pass + + +# TODO(jzelinskie): make this interface support superuser API +class KeyServerDataInterface(object): + """ + Interface that represents all data store interactions required by a JWT key service. + """ + + @classmethod + def list_service_keys(cls, service): + """ + Returns a list of service keys or an empty list if the service does not exist. + """ + raise NotImplementedError() + + @classmethod + def get_service_key(cls, signer_kid, service=None, alive_only=None, approved_only=None): + """ + Returns a service kid with the given kid or raises ServiceKeyNotFound. + """ + raise NotImplementedError() + + @classmethod + def create_service_key(cls, name, kid, service, jwk, metadata, expiration_date, + rotation_duration=None): + """ + Stores a service key. + """ + raise NotImplementedError() + + @classmethod + def replace_service_key(cls, old_kid, kid, jwk, metadata, expiration_date): + """ + Replaces a service with a new key or raises ServiceKeyNotFound. + """ + raise NotImplementedError() + + @classmethod + def delete_service_key(cls, kid): + """ + Deletes and returns a service key with the given kid or raises ServiceKeyNotFound. + """ + raise NotImplementedError() + + +class PreOCIModel(KeyServerDataInterface): + """ + PreOCIModel implements the data model for JWT key service using a database schema before it was + changed to support the OCI specification. + """ + @classmethod + def _db_key_to_servicekey(cls, key): + """ + Converts the database model of a service key into a ServiceKey. + """ + return ServiceKey( + name=key.name, + kid=key.kid, + service=key.service, + jwk=key.jwk, + metadata=key.metadata, + created_date=key.created_date, + expiration_date=key.expiration_date, + rotation_duration=key.rotation_duration, + approval=key.approval, + ) + + @classmethod + def list_service_keys(cls, service): + return data.model.service_keys.list_service_keys(service) + + @classmethod + def get_service_key(cls, signer_kid, service=None, alive_only=True, approved_only=True): + try: + key = data.model.service_keys.get_service_key(signer_kid, service, alive_only, approved_only) + return cls._db_key_to_servicekey(key) + except data.model.ServiceKeyDoesNotExist: + raise ServiceKeyDoesNotExist() + + @classmethod + def create_service_key(cls, name, kid, service, jwk, metadata, expiration_date, + rotation_duration=None): + key = data.model.service_keys.create_service_key(name, kid, service, jwk, metadata, + expiration_date, rotation_duration) + return cls._db_key_to_servicekey(key) + + @classmethod + def replace_service_key(cls, old_kid, kid, jwk, metadata, expiration_date): + try: + data.model.service_keys.replace_service_key(old_kid, kid, jwk, metadata, expiration_date) + except data.model.ServiceKeyDoesNotExist: + raise ServiceKeyDoesNotExist() + + @classmethod + def delete_service_key(cls, kid): + try: + key = data.model.service_keys.delete_service_key(kid) + return cls._db_key_to_servicekey(key) + except data.model.ServiceKeyDoesNotExist: + raise ServiceKeyDoesNotExist() diff --git a/endpoints/key_server.py b/endpoints/keyserver/__init__.py similarity index 84% rename from endpoints/key_server.py rename to endpoints/keyserver/__init__.py index 70c0da0eb..b5e74b171 100644 --- a/endpoints/key_server.py +++ b/endpoints/keyserver/__init__.py @@ -4,11 +4,9 @@ from datetime import datetime, timedelta from flask import Blueprint, jsonify, abort, request, make_response from jwt import get_unverified_header -import data.model -import data.model.service_keys -from data.model.log import log_action - from app import app +from data.interfaces.key_server import PreOCIModel as model, ServiceKeyDoesNotExist +from data.model.log import log_action from util.security import jwtutil @@ -38,7 +36,7 @@ def _validate_jwt(encoded_jwt, jwk, service): try: jwtutil.decode(encoded_jwt, public_key, algorithms=['RS256'], - audience=JWT_AUDIENCE, issuer=service) + audience=JWT_AUDIENCE, issuer=service) except jwtutil.InvalidTokenError: logger.exception('JWT validation failure') abort(400) @@ -55,23 +53,22 @@ def _signer_kid(encoded_jwt, allow_none=False): def _lookup_service_key(service, signer_kid, approved_only=True): try: - return data.model.service_keys.get_service_key(signer_kid, service=service, - approved_only=approved_only) - except data.model.ServiceKeyDoesNotExist: + return model.get_service_key(signer_kid, service=service, approved_only=approved_only) + except ServiceKeyDoesNotExist: abort(403) @key_server.route('/services//keys', methods=['GET']) def list_service_keys(service): - keys = data.model.service_keys.list_service_keys(service) + keys = model.list_service_keys(service) return jsonify({'keys': [key.jwk for key in keys]}) @key_server.route('/services//keys/', methods=['GET']) def get_service_key(service, kid): try: - key = data.model.service_keys.get_service_key(kid, alive_only=False, approved_only=False) - except data.model.ServiceKeyDoesNotExist: + key = model.get_service_key(kid, alive_only=False, approved_only=False) + except ServiceKeyDoesNotExist: abort(404) if key.approval is None: @@ -119,8 +116,8 @@ def put_service_key(service, kid): if kid == signer_kid or signer_kid is None: # The key is self-signed. Create a new instance and await approval. _validate_jwt(encoded_jwt, jwk, service) - data.model.service_keys.create_service_key('', kid, service, jwk, metadata, expiration_date, - rotation_duration=rotation_duration) + model.create_service_key('', kid, service, jwk, metadata, expiration_date, + rotation_duration=rotation_duration) key_log_metadata = { 'kid': kid, @@ -143,8 +140,8 @@ def put_service_key(service, kid): _validate_jwt(encoded_jwt, signer_jwk, service) try: - data.model.service_keys.replace_service_key(signer_key.kid, kid, jwk, metadata, expiration_date) - except data.model.ServiceKeyDoesNotExist: + model.replace_service_key(signer_key.kid, kid, jwk, metadata, expiration_date) + except ServiceKeyDoesNotExist: abort(404) key_log_metadata = { @@ -180,8 +177,8 @@ def delete_service_key(service, kid): _validate_jwt(encoded_jwt, signer_key.jwk, service) try: - data.model.service_keys.delete_service_key(kid) - except data.model.ServiceKeyDoesNotExist: + model.delete_service_key(kid) + except ServiceKeyDoesNotExist: abort(404) key_log_metadata = { diff --git a/test/test_endpoints.py b/test/test_endpoints.py index d8430375b..a139ee575 100644 --- a/test/test_endpoints.py +++ b/test/test_endpoints.py @@ -18,7 +18,7 @@ from jwkest.jwk import RSAKey from app import app from data import model from data.database import ServiceKeyApprovalType -from endpoints import key_server +from endpoints import keyserver from endpoints.api import api, api_bp from endpoints.api.user import Signin from endpoints.web import web as web_bp @@ -28,7 +28,7 @@ from test.helpers import assert_action_logged try: app.register_blueprint(web_bp, url_prefix='') - app.register_blueprint(key_server.key_server, url_prefix='') + app.register_blueprint(keyserver.key_server, url_prefix='') except ValueError: # This blueprint was already registered pass @@ -355,7 +355,7 @@ class KeyServerTestCase(EndpointTestCase): def _get_test_jwt_payload(self): return { 'iss': 'sample_service', - 'aud': key_server.JWT_AUDIENCE, + 'aud': keyserver.JWT_AUDIENCE, 'exp': int(time.time()) + 60, 'iat': int(time.time()), 'nbf': int(time.time()), diff --git a/web.py b/web.py index b33c76383..237829c0f 100644 --- a/web.py +++ b/web.py @@ -7,7 +7,7 @@ from endpoints.api import api_bp from endpoints.bitbuckettrigger import bitbuckettrigger from endpoints.githubtrigger import githubtrigger from endpoints.gitlabtrigger import gitlabtrigger -from endpoints.key_server import key_server +from endpoints.keyserver import key_server from endpoints.oauthlogin import oauthlogin from endpoints.realtime import realtime from endpoints.web import web