From a6f6a114c2ad4e8a75e359a0e8a36b667bd9c2d4 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Fri, 29 Apr 2016 10:51:22 -0500 Subject: [PATCH] service key worker to refresh automatic keys --- boot.py | 33 +++++++++++--------- conf/init/service/service_key_worker/log/run | 2 ++ conf/init/service/service_key_worker/run | 8 +++++ config.py | 6 ++-- data/model/service_keys.py | 23 ++++++++++++-- workers/service_key_worker.py | 28 +++++++++++++++++ 6 files changed, 82 insertions(+), 18 deletions(-) create mode 100755 conf/init/service/service_key_worker/log/run create mode 100755 conf/init/service/service_key_worker/run create mode 100644 workers/service_key_worker.py diff --git a/boot.py b/boot.py index 2806b16d0..a97b507cd 100644 --- a/boot.py +++ b/boot.py @@ -1,30 +1,20 @@ #!/usr/bin/env python -from datetime import datetime +from datetime import datetime, timedelta from urlparse import urlunparse from jinja2 import Template +from cachetools import lru_cache import release + from app import app from data.model.release import set_region_release from util.config.database import sync_database_with_config from util.generatepresharedkey import generate_key -def create_quay_service_key(): - """ - Creates a service key for quay to use in the jwtproxy - """ - quay_key, key_id = generate_key('quay', 'quay') - - with open('/conf/quay.pem', mode='w') as f: - f.truncate(0) - f.write(quay_key.exportKey()) - - return key_id - - +@lru_cache(maxsize=1) def get_audience(): audience = app.config.get('JWTPROXY_AUDIENCE') @@ -47,6 +37,21 @@ def get_audience(): return urlunparse((scheme, hostname + ':' + port, '', '', '', '')) +def create_quay_service_key(): + """ + Creates a service key for quay to use in the jwtproxy + """ + minutes_until_expiration = app.config.get('QUAY_SERVICE_KEY_EXPIRATION', 120) + expiration = timedelta(minutes=minutes_until_expiration) + quay_key, key_id = generate_key('quay', get_audience(), datetime.now() + expiration) + + with open('/conf/quay.pem', mode='w') as f: + f.truncate(0) + f.write(quay_key.exportKey()) + + return key_id + + def create_jwtproxy_conf(quay_key_id): """ Generates the jwtproxy conf from the jinja template diff --git a/conf/init/service/service_key_worker/log/run b/conf/init/service/service_key_worker/log/run new file mode 100755 index 000000000..410fabb1a --- /dev/null +++ b/conf/init/service/service_key_worker/log/run @@ -0,0 +1,2 @@ +#!/bin/sh +exec logger -i -t service_key_worker diff --git a/conf/init/service/service_key_worker/run b/conf/init/service/service_key_worker/run new file mode 100755 index 000000000..20b578c24 --- /dev/null +++ b/conf/init/service/service_key_worker/run @@ -0,0 +1,8 @@ +#! /bin/bash + +echo 'Starting service key worker' + +cd / +venv/bin/python -m workers.service_key_worker 2>&1 + +echo 'Service key worker exited' diff --git a/config.py b/config.py index 27593de98..ea9fada91 100644 --- a/config.py +++ b/config.py @@ -320,5 +320,7 @@ class DefaultConfig(object): # lowest user in the database will be used. SERVICE_LOG_ACCOUNT_ID = None - # Quay's service key expiration in seconds - QUAY_SERVICE_KEY_EXPIRATION = 500 + # Quay's service key expiration in minutes + QUAY_SERVICE_KEY_EXPIRATION = 120 + # Number of minutes between expiration refresh in minutes + QUAY_SERVICE_KEY_REFRESH = 60 diff --git a/data/model/service_keys.py b/data/model/service_keys.py index 5de5b0301..7e37908c4 100644 --- a/data/model/service_keys.py +++ b/data/model/service_keys.py @@ -7,7 +7,7 @@ from peewee import JOIN_LEFT_OUTER from Crypto.PublicKey import RSA from jwkest.jwk import RSAKey -from data.database import db_for_update, User, ServiceKey, ServiceKeyApproval +from data.database import db_for_update, User, ServiceKey, ServiceKeyApproval, ServiceKeyApprovalType from data.model import (ServiceKeyDoesNotExist, ServiceKeyAlreadyApproved, ServiceNameInvalid, db_transaction, config) from data.model.notification import create_notification, delete_all_notifications_by_path_prefix @@ -20,9 +20,11 @@ def _expired_keys_clause(service): return ((ServiceKey.service == service) & (ServiceKey.expiration_date <= datetime.utcnow())) + def _stale_expired_keys_service_clause(service): return ((ServiceKey.service == service) & _stale_expired_keys_clause()) + def _stale_expired_keys_clause(): expired_ttl = timedelta(seconds=config.app_config['EXPIRED_SERVICE_KEY_TTL_SEC']) return (ServiceKey.expiration_date <= (datetime.utcnow() - expired_ttl)) @@ -34,6 +36,10 @@ def _stale_unapproved_keys_clause(service): (ServiceKey.created_date <= (datetime.utcnow() - unapproved_ttl))) +def _unexpired_clause(): + return ServiceKey.expiration_date >= datetime.utcnow() + + def _gc_expired(service): ServiceKey.delete().where(_stale_expired_keys_service_clause(service) | _stale_unapproved_keys_clause(service)).execute() @@ -141,6 +147,16 @@ def set_key_expiration(kid, expiration_date): service_key.save() +def refresh_automatic_service_keys(extension): + """ + Finds all unexpired automatic keys and sets their + expiration to `now + extension` + """ + for service_key in list(_list_service_keys_query(approval_type=ServiceKeyApprovalType.AUTOMATIC).where(_unexpired_clause())): + service_key.expiration_date = datetime.now() + extension + service_key.save() + + def approve_service_key(kid, approver, approval_type, notes=''): try: with db_transaction(): @@ -159,12 +175,15 @@ def approve_service_key(kid, approver, approval_type, notes=''): return key -def _list_service_keys_query(kid=None, service=None, approved_only=False): +def _list_service_keys_query(kid=None, service=None, approved_only=False, approval_type=None): query = ServiceKey.select().join(ServiceKeyApproval, JOIN_LEFT_OUTER) if approved_only: query = query.where(~(ServiceKey.approval >> None)) + if approval_type is not None: + query = query.where(ServiceKeyApproval.approval_type == approval_type) + if service is not None: query = query.where(ServiceKey.service == service) query = query.where(~(_expired_keys_clause(service)) | diff --git a/workers/service_key_worker.py b/workers/service_key_worker.py new file mode 100644 index 000000000..6086916d3 --- /dev/null +++ b/workers/service_key_worker.py @@ -0,0 +1,28 @@ +import logging +from datetime import timedelta + +from app import app +from data.model.service_keys import refresh_automatic_service_keys +from workers.worker import Worker + +logger = logging.getLogger(__name__) + +class ServiceKeyWorker(Worker): + def __init__(self): + super(ServiceKeyWorker, self).__init__() + self.add_operation(self._refresh_service_keys, + app.config.get('QUAY_SERVICE_KEY_REFRESH', 60)*60) + + def _refresh_service_keys(self): + """ Refreshes active service keys so they don't get garbage collected. """ + + minutes_until_expiration = app.config.get('QUAY_SERVICE_KEY_EXPIRATION', 120) + expiration = timedelta(minutes=minutes_until_expiration) + + logger.debug('Starting refresh of automatic service keys') + refresh_automatic_service_keys(expiration) + logger.debug('Finished refresh of automatic service keys') + +if __name__ == "__main__": + worker = ServiceKeyWorker() + worker.start()