create key server data interface

This commit is contained in:
Jimmy Zelinskie 2016-08-31 14:31:43 -04:00
parent c06d395f96
commit 2e5a94bc0b
4 changed files with 140 additions and 21 deletions

View file

@ -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()

View file

@ -4,11 +4,9 @@ from datetime import datetime, timedelta
from flask import Blueprint, jsonify, abort, request, make_response from flask import Blueprint, jsonify, abort, request, make_response
from jwt import get_unverified_header 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 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 from util.security import jwtutil
@ -38,7 +36,7 @@ def _validate_jwt(encoded_jwt, jwk, service):
try: try:
jwtutil.decode(encoded_jwt, public_key, algorithms=['RS256'], jwtutil.decode(encoded_jwt, public_key, algorithms=['RS256'],
audience=JWT_AUDIENCE, issuer=service) audience=JWT_AUDIENCE, issuer=service)
except jwtutil.InvalidTokenError: except jwtutil.InvalidTokenError:
logger.exception('JWT validation failure') logger.exception('JWT validation failure')
abort(400) abort(400)
@ -55,23 +53,22 @@ def _signer_kid(encoded_jwt, allow_none=False):
def _lookup_service_key(service, signer_kid, approved_only=True): def _lookup_service_key(service, signer_kid, approved_only=True):
try: try:
return data.model.service_keys.get_service_key(signer_kid, service=service, return model.get_service_key(signer_kid, service=service, approved_only=approved_only)
approved_only=approved_only) except ServiceKeyDoesNotExist:
except data.model.ServiceKeyDoesNotExist:
abort(403) abort(403)
@key_server.route('/services/<service>/keys', methods=['GET']) @key_server.route('/services/<service>/keys', methods=['GET'])
def list_service_keys(service): 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]}) return jsonify({'keys': [key.jwk for key in keys]})
@key_server.route('/services/<service>/keys/<kid>', methods=['GET']) @key_server.route('/services/<service>/keys/<kid>', methods=['GET'])
def get_service_key(service, kid): def get_service_key(service, kid):
try: try:
key = data.model.service_keys.get_service_key(kid, alive_only=False, approved_only=False) key = model.get_service_key(kid, alive_only=False, approved_only=False)
except data.model.ServiceKeyDoesNotExist: except ServiceKeyDoesNotExist:
abort(404) abort(404)
if key.approval is None: if key.approval is None:
@ -119,8 +116,8 @@ def put_service_key(service, kid):
if kid == signer_kid or signer_kid is None: if kid == signer_kid or signer_kid is None:
# The key is self-signed. Create a new instance and await approval. # The key is self-signed. Create a new instance and await approval.
_validate_jwt(encoded_jwt, jwk, service) _validate_jwt(encoded_jwt, jwk, service)
data.model.service_keys.create_service_key('', kid, service, jwk, metadata, expiration_date, model.create_service_key('', kid, service, jwk, metadata, expiration_date,
rotation_duration=rotation_duration) rotation_duration=rotation_duration)
key_log_metadata = { key_log_metadata = {
'kid': kid, 'kid': kid,
@ -143,8 +140,8 @@ def put_service_key(service, kid):
_validate_jwt(encoded_jwt, signer_jwk, service) _validate_jwt(encoded_jwt, signer_jwk, service)
try: try:
data.model.service_keys.replace_service_key(signer_key.kid, kid, jwk, metadata, expiration_date) model.replace_service_key(signer_key.kid, kid, jwk, metadata, expiration_date)
except data.model.ServiceKeyDoesNotExist: except ServiceKeyDoesNotExist:
abort(404) abort(404)
key_log_metadata = { key_log_metadata = {
@ -180,8 +177,8 @@ def delete_service_key(service, kid):
_validate_jwt(encoded_jwt, signer_key.jwk, service) _validate_jwt(encoded_jwt, signer_key.jwk, service)
try: try:
data.model.service_keys.delete_service_key(kid) model.delete_service_key(kid)
except data.model.ServiceKeyDoesNotExist: except ServiceKeyDoesNotExist:
abort(404) abort(404)
key_log_metadata = { key_log_metadata = {

View file

@ -18,7 +18,7 @@ from jwkest.jwk import RSAKey
from app import app from app import app
from data import model from data import model
from data.database import ServiceKeyApprovalType from data.database import ServiceKeyApprovalType
from endpoints import key_server from endpoints import keyserver
from endpoints.api import api, api_bp from endpoints.api import api, api_bp
from endpoints.api.user import Signin from endpoints.api.user import Signin
from endpoints.web import web as web_bp from endpoints.web import web as web_bp
@ -28,7 +28,7 @@ from test.helpers import assert_action_logged
try: try:
app.register_blueprint(web_bp, url_prefix='') 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: except ValueError:
# This blueprint was already registered # This blueprint was already registered
pass pass
@ -355,7 +355,7 @@ class KeyServerTestCase(EndpointTestCase):
def _get_test_jwt_payload(self): def _get_test_jwt_payload(self):
return { return {
'iss': 'sample_service', 'iss': 'sample_service',
'aud': key_server.JWT_AUDIENCE, 'aud': keyserver.JWT_AUDIENCE,
'exp': int(time.time()) + 60, 'exp': int(time.time()) + 60,
'iat': int(time.time()), 'iat': int(time.time()),
'nbf': int(time.time()), 'nbf': int(time.time()),

2
web.py
View file

@ -7,7 +7,7 @@ from endpoints.api import api_bp
from endpoints.bitbuckettrigger import bitbuckettrigger from endpoints.bitbuckettrigger import bitbuckettrigger
from endpoints.githubtrigger import githubtrigger from endpoints.githubtrigger import githubtrigger
from endpoints.gitlabtrigger import gitlabtrigger 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.oauthlogin import oauthlogin
from endpoints.realtime import realtime from endpoints.realtime import realtime
from endpoints.web import web from endpoints.web import web