keys ui WIP
This commit is contained in:
parent
dc593c0197
commit
11ff3e9b59
25 changed files with 1154 additions and 74 deletions
|
@ -5,6 +5,7 @@ import logging
|
|||
import os
|
||||
import string
|
||||
|
||||
from calendar import timegm
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from random import SystemRandom
|
||||
|
@ -22,7 +23,7 @@ from auth.permissions import SuperUserPermission
|
|||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
|
||||
internal_only, require_scope, show_if, parse_args,
|
||||
query_param, abort, require_fresh_login, path_param, verify_not_prod,
|
||||
page_support)
|
||||
page_support, log_action)
|
||||
from endpoints.api.logs import get_logs, get_aggregate_logs
|
||||
from data import model
|
||||
from data.database import ServiceKeyApprovalType
|
||||
|
@ -148,6 +149,8 @@ def org_view(org):
|
|||
|
||||
def user_view(user, password=None):
|
||||
user_data = {
|
||||
'kind': 'user',
|
||||
'name': user.username,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'verified': user.verified,
|
||||
|
@ -506,7 +509,7 @@ class SuperUserServiceKeyManagement(ApiResource):
|
|||
""" Resource for managing service keys."""
|
||||
schemas = {
|
||||
'CreateServiceKey': {
|
||||
'id': 'PutServiceKey',
|
||||
'id': 'CreateServiceKey',
|
||||
'type': 'object',
|
||||
'description': 'Description of creation of a service key',
|
||||
'required': ['service', 'expiration'],
|
||||
|
@ -523,9 +526,13 @@ class SuperUserServiceKeyManagement(ApiResource):
|
|||
'type': 'object',
|
||||
'description': 'The key/value pairs of this key\'s metadata',
|
||||
},
|
||||
'notes': {
|
||||
'type': 'string',
|
||||
'description': 'If specified, the extra notes for the key',
|
||||
},
|
||||
'expiration': {
|
||||
'description': 'The expiration date as a unix timestamp',
|
||||
'anyOf': [{'type': 'string'}, {'type': 'null'}],
|
||||
'anyOf': [{'type': 'number'}, {'type': 'null'}],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -536,46 +543,72 @@ class SuperUserServiceKeyManagement(ApiResource):
|
|||
@require_scope(scopes.SUPERUSER)
|
||||
def get(self):
|
||||
if SuperUserPermission().can():
|
||||
keys = model.service_keys.list_keys()
|
||||
return jsonify({'keys': [key_view(key) for key in keys]})
|
||||
keys = model.service_keys.list_all_keys()
|
||||
|
||||
return jsonify({
|
||||
'keys': [key_view(key) for key in keys],
|
||||
})
|
||||
|
||||
abort(403)
|
||||
|
||||
@require_fresh_login
|
||||
@verify_not_prod
|
||||
@nickname('createServiceKey')
|
||||
@require_scope(scopes.SUPERUSER)
|
||||
@validate_json_request('PutServiceKey')
|
||||
@validate_json_request('CreateServiceKey')
|
||||
def post(self):
|
||||
if SuperUserPermission().can():
|
||||
body = request.get_json()
|
||||
|
||||
# Ensure we have a valid expiration date if specified.
|
||||
expiration_date = body.get('expiration', None)
|
||||
if expiration_date == '':
|
||||
expiration_date = None
|
||||
if expiration_date is not None:
|
||||
try:
|
||||
expiration_date = datetime.utcfromtimestamp(float(expiration_date))
|
||||
except ValueError:
|
||||
abort(400)
|
||||
|
||||
|
||||
# Create the metadata for the key.
|
||||
user = get_authenticated_user()
|
||||
metadata = body.get('metadata', {})
|
||||
metadata.update({
|
||||
'created_by': 'Quay SuperUser Panel',
|
||||
'created_by': 'Quay Superuser Panel',
|
||||
'creator': user.username,
|
||||
'ip': request.remote_addr,
|
||||
})
|
||||
|
||||
# Generate the private key but *do not save it on the server anywhere*.
|
||||
private_key = RSA.generate(2048)
|
||||
jwk = RSAKey(key=private_key.publickey()).serialize()
|
||||
kid = sha256(json.dumps(canonicalize(jwk), separators=(',', ':'))).hexdigest()
|
||||
|
||||
# Create the service key.
|
||||
model.service_keys.create_service_key(body.get('name', ''), kid, body['service'], jwk,
|
||||
metadata, expiration_date)
|
||||
model.service_keys.approve_service_key(kid, user, ServiceKeyApprovalType.SUPERUSER)
|
||||
|
||||
return jsonify({'public_key': private_key.publickey().exportKey('PEM'),
|
||||
'private_key': private_key.exportKey('PEM')})
|
||||
# Auto-approve the service key.
|
||||
model.service_keys.approve_service_key(kid, user, ServiceKeyApprovalType.SUPERUSER,
|
||||
notes=body.get('notes', ''))
|
||||
|
||||
# Log the creation and auto-approval of the service key.
|
||||
key_log_metadata = {
|
||||
'kid': kid,
|
||||
'preshared': True,
|
||||
'service': body['service'],
|
||||
'name': body.get('name', ''),
|
||||
'expiration_date': expiration_date,
|
||||
'auto_approved': True,
|
||||
}
|
||||
|
||||
log_action('service_key_create', user.username, key_log_metadata)
|
||||
log_action('service_key_approve', user.username, key_log_metadata)
|
||||
|
||||
return jsonify({
|
||||
'kid': kid,
|
||||
'name': body.get('name', ''),
|
||||
'public_key': private_key.publickey().exportKey('PEM'),
|
||||
'private_key': private_key.exportKey('PEM'),
|
||||
})
|
||||
|
||||
abort(403)
|
||||
|
||||
|
@ -590,7 +623,6 @@ class SuperUserServiceKeyUpdater(ApiResource):
|
|||
'id': 'PutServiceKey',
|
||||
'type': 'object',
|
||||
'description': 'Description of updates for a service key',
|
||||
'required': ['name', 'metadata', 'expiration'],
|
||||
'properties': {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
|
@ -602,28 +634,78 @@ class SuperUserServiceKeyUpdater(ApiResource):
|
|||
},
|
||||
'expiration': {
|
||||
'description': 'The expiration date as a unix timestamp',
|
||||
'anyOf': [{'type': 'string'}, {'type': 'null'}],
|
||||
'anyOf': [{'type': 'number'}, {'type': 'null'}],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@require_fresh_login
|
||||
@verify_not_prod
|
||||
@nickname('putServiceKey')
|
||||
@nickname('updateServiceKey')
|
||||
@require_scope(scopes.SUPERUSER)
|
||||
@validate_json_request('PutServiceKey')
|
||||
def put(self, kid):
|
||||
if SuperUserPermission().can():
|
||||
body = request.get_json()
|
||||
try:
|
||||
key = model.service_keys.get_service_key(kid)
|
||||
except model.service_keys.ServiceKeyDoesNotExist:
|
||||
abort(404)
|
||||
|
||||
expiration_date = body['expiration']
|
||||
if expiration_date is not None and expiration_date != '':
|
||||
try:
|
||||
expiration_date = datetime.utcfromtimestamp(float(expiration_date))
|
||||
except ValueError:
|
||||
abort(400)
|
||||
user = get_authenticated_user()
|
||||
|
||||
model.service_keys.update_service_key(body['name'], kid, body['metadata'], expiration_date)
|
||||
key_log_metadata = {
|
||||
'kid': key.kid,
|
||||
'service': key.service,
|
||||
'name': body.get('name', key.name),
|
||||
'expiration_date': key.expiration_date,
|
||||
}
|
||||
|
||||
if 'expiration' in body:
|
||||
expiration_date = body['expiration']
|
||||
if expiration_date is not None and expiration_date != '':
|
||||
try:
|
||||
expiration_date = datetime.utcfromtimestamp(float(expiration_date))
|
||||
except ValueError:
|
||||
abort(400)
|
||||
|
||||
key_log_metadata.update({
|
||||
'old_expiration_date': key.expiration_date,
|
||||
'expiration_date': expiration_date,
|
||||
})
|
||||
|
||||
log_action('service_key_extend', user.username, key_log_metadata)
|
||||
model.service_keys.set_key_expiration(kid, expiration_date)
|
||||
|
||||
|
||||
if 'name' in body or 'metadata' in body:
|
||||
model.service_keys.update_service_key(kid, body.get('name'), body.get('metadata'))
|
||||
log_action('service_key_modify', user.username, key_log_metadata)
|
||||
|
||||
return jsonify(key_view(model.service_keys.get_service_key(kid)))
|
||||
|
||||
abort(403)
|
||||
|
||||
@require_fresh_login
|
||||
@verify_not_prod
|
||||
@nickname('deleteServiceKey')
|
||||
@require_scope(scopes.SUPERUSER)
|
||||
def delete(self, kid):
|
||||
if SuperUserPermission().can():
|
||||
key = model.service_keys.delete_service_key(kid)
|
||||
|
||||
key_log_metadata = {
|
||||
'kid': kid,
|
||||
'service': key.service,
|
||||
'name': key.name,
|
||||
'created_date': key.created_date,
|
||||
'expiration_date': key.expiration_date,
|
||||
}
|
||||
|
||||
user = get_authenticated_user()
|
||||
log_action('service_key_delete', user.username, key_log_metadata)
|
||||
return make_response('', 201)
|
||||
|
||||
abort(403)
|
||||
|
||||
|
@ -634,19 +716,36 @@ class SuperUserServiceKeyUpdater(ApiResource):
|
|||
class SuperUserServiceKeyApproval(ApiResource):
|
||||
""" Resource for approving service keys. """
|
||||
|
||||
schemas = {
|
||||
'ApproveServiceKey': {
|
||||
'id': 'ApproveServiceKey',
|
||||
'type': 'object',
|
||||
'description': 'Information for approving service keys',
|
||||
'properties': {
|
||||
'notes': {
|
||||
'type': 'string',
|
||||
'description': 'Optional approval notes',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@require_fresh_login
|
||||
@verify_not_prod
|
||||
@nickname('approveServiceKey')
|
||||
@require_scope(scopes.SUPERUSER)
|
||||
def put(self, kid):
|
||||
if SuperUserPermission().can():
|
||||
notes = request.get_json().get('notes', '')
|
||||
approver = get_authenticated_user()
|
||||
try:
|
||||
model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER)
|
||||
model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER,
|
||||
notes=notes)
|
||||
except model.ServiceKeyDoesNotExist:
|
||||
abort(404)
|
||||
except model.ServiceKeyAlreadyApproved:
|
||||
pass
|
||||
|
||||
return make_response('', 200)
|
||||
return make_response('', 201)
|
||||
|
||||
abort(403)
|
||||
|
|
|
@ -12,6 +12,7 @@ from jwkest.jwk import keyrep, RSAKey, ECKey
|
|||
|
||||
import data.model
|
||||
import data.model.service_keys
|
||||
from data.model.log import log_action
|
||||
|
||||
from app import app
|
||||
from auth.registry_jwt_auth import TOKEN_REGEX
|
||||
|
@ -59,13 +60,14 @@ def _validate_jwt(encoded_jwt, jwk, service):
|
|||
strictjwt.decode(encoded_jwt, public_key, algorithms=['RS256'],
|
||||
audience=JWT_AUDIENCE, issuer=service)
|
||||
except strictjwt.InvalidTokenError:
|
||||
logger.exception('JWT validation failure')
|
||||
abort(400)
|
||||
|
||||
|
||||
def _signer_kid(encoded_jwt):
|
||||
decoded_jwt = jwt.decode(encoded_jwt, verify=False)
|
||||
logger.debug(decoded_jwt)
|
||||
return decoded_jwt.get('signer_kid', None)
|
||||
return decoded_jwt.get('kid', None)
|
||||
|
||||
|
||||
def _signer_key(service, signer_kid):
|
||||
|
@ -104,6 +106,7 @@ def put_service_key(service, kid):
|
|||
try:
|
||||
expiration_date = datetime.utcfromtimestamp(float(expiration_date))
|
||||
except ValueError:
|
||||
logger.exception('Error parsing expiration date on key')
|
||||
abort(400)
|
||||
|
||||
rotation_ttl = request.args.get('rotation', None)
|
||||
|
@ -113,6 +116,7 @@ def put_service_key(service, kid):
|
|||
try:
|
||||
jwk = request.get_json()
|
||||
except ValueError:
|
||||
logger.exception('Error parsing JWK')
|
||||
abort(400)
|
||||
|
||||
logger.debug(jwk)
|
||||
|
@ -120,7 +124,9 @@ def put_service_key(service, kid):
|
|||
jwt_header = request.headers.get(JWT_HEADER_NAME, '')
|
||||
match = TOKEN_REGEX.match(jwt_header)
|
||||
if match is None:
|
||||
logger.error('Could not find matching bearer token')
|
||||
abort(400)
|
||||
|
||||
encoded_jwt = match.group(1)
|
||||
|
||||
_validate_jwk(jwk, kid)
|
||||
|
@ -131,6 +137,18 @@ def put_service_key(service, kid):
|
|||
# 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)
|
||||
|
||||
key_log_metadata = {
|
||||
'kid': kid,
|
||||
'preshared': False,
|
||||
'service': service,
|
||||
'name': '',
|
||||
'expiration_date': expiration_date,
|
||||
'user_agent': request.headers.get('User-Agent'),
|
||||
'ip': request.remote_addr,
|
||||
}
|
||||
|
||||
log_action('service_key_create', None, metadata=key_log_metadata, ip=request.remote_addr)
|
||||
return make_response('', 202)
|
||||
|
||||
metadata.update({'created_by': 'Key Rotation'})
|
||||
|
@ -146,6 +164,17 @@ def put_service_key(service, kid):
|
|||
except data.model.ServiceKeyDoesNotExist:
|
||||
abort(404)
|
||||
|
||||
key_log_metadata = {
|
||||
'kid': kid,
|
||||
'signer_kid': signer_key.kid,
|
||||
'service': service,
|
||||
'name': signer_key.name,
|
||||
'expiration_date': expiration_date,
|
||||
'user_agent': request.headers.get('User-Agent'),
|
||||
'ip': request.remote_addr,
|
||||
}
|
||||
|
||||
log_action('service_key_rotate', None, metadata=key_log_metadata, ip=request.remote_addr)
|
||||
return make_response('', 200)
|
||||
|
||||
|
||||
|
|
Reference in a new issue