service keys: do all the right stuff

This commit is contained in:
Jimmy Zelinskie 2016-03-23 18:16:03 -04:00 committed by Jimmy Zelinskie
parent 6ecff950ab
commit 4079dba167
7 changed files with 149 additions and 41 deletions

View file

@ -5,19 +5,20 @@ import logging
import os
from random import SystemRandom
from flask import request
from flask import request, make_response
import features
from app import app, avatar, superusers, authentication, config_provider
from auth import scopes
from auth.auth_context import get_authenticated_user
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)
from endpoints.api.logs import get_logs, get_aggregate_logs
from data import model
from auth.permissions import SuperUserPermission
from auth import scopes
from util.useremails import send_confirmation_email, send_recovery_email
@ -467,3 +468,39 @@ class SuperUserOrganizationManagement(ApiResource):
return org_view(org)
abort(403)
@resource('/v1/superuser/services/<service>/keys/<kid>')
@path_param('service', 'The service using the key')
@path_param('kid', 'The unique identifier for a service key')
@show_if(features.SUPER_USERS)
class SuperUserServiceKeyManagement(ApiResource):
""" Resource for managing service keys. """
schemas = {
'ApproveServiceKey': {
'id': 'ApproveServiceKey',
'type': 'object',
'description': 'Description of approved keys for a service',
'properties': {
'kid': {
'type': 'string',
'description': 'The key being approved for service authentication usage.',
},
},
},
}
@verify_not_prod
@nickname('approveServiceKey')
@validate_json_request('ApproveServiceKey')
@require_scope(scopes.SUPERUSER)
def put(self, service, kid):
if SuperUserPermission().can():
approver = get_authenticated_user()
try:
model.service_keys.approve_service_key(service, kid, approver, 'Quay SuperUser API')
except model.ServiceKeyDoesNotExist:
abort(404)
except model.ServiceKeyAlreadyApproved:
pass
make_response('', 200)

View file

@ -1,5 +1,7 @@
from datetime import datetime
import jwt
from flask import Blueprint, jsonify, abort, request, make_response
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.backends import default_backend
@ -16,26 +18,42 @@ JWT_HEADER_NAME = 'Authorization'
JWT_AUDIENCE = 'quay'
def _validate_JWT(jwt, service, kid):
try:
service_key = data.model.service_keys.get_service_keys(service, kid=kid)
except data.model.ServiceKeyDoesNotExist:
abort(404)
public_key = RSAPublicNumbers(e=service_key.jwk.e,
n=service_key.jwk.n).public_key(default_backend())
def _validate_JWT(encoded_jwt, jwk, service):
public_key = RSAPublicNumbers(e=jwk.e,
n=jwk.n).public_key(default_backend())
try:
strictjwt.decode(jwt, public_key, algorithms=['RS256'], audience=JWT_AUDIENCE, issuer=service)
strictjwt.decode(encoded_jwt, public_key, algorithms=['RS256'],
audience=JWT_AUDIENCE, issuer=service)
except strictjwt.InvalidTokenError:
abort(400)
def _signer_jwk(encoded_jwt, jwk, service, kid):
decoded_jwt = jwt.decode(encoded_jwt, verify=False)
signer_kid = decoded_jwt.get('signer_kid', '')
# If we already have our own JWK and it's the signer, short-circuit.
if (signer_kid == kid or signer_kid == '') and jwk is not None:
return jwk
else:
try:
service_key = data.model.service_keys.get_service_keys(service, kid=signer_kid)
except data.model.ServiceKeyDoesNotExist:
abort(404)
return service_key.jwk
@key_server.route('/services/<service>/keys', methods=['GET'])
def get_service_keys(service):
keys = data.model.service_keys.get_service_keys(service)
jwks = [key.jwk for key in keys]
return jsonify({'keys': jwks})
kid = request.args.get('kid', None)
if kid is not None:
keys = data.model.service_keys.get_service_keys(service, kid=kid)
else:
keys = data.model.service_keys.get_service_keys(service)
return jsonify({'keys': [key.jwk for key in keys]})
@key_server.route('/services/<service>/keys/<kid>', methods=['PUT'])
@ -47,11 +65,6 @@ def put_service_keys(service, kid):
except ValueError:
abort(400)
jwt = request.headers.get(JWT_HEADER_NAME, None)
if not jwt:
abort(400)
_validate_JWT(jwt, service, kid)
try:
jwk = request.json()
except ValueError:
@ -60,6 +73,9 @@ def put_service_keys(service, kid):
if 'kty' not in jwk:
abort(400)
if 'kid' not in jwk or jwk['kid'] != kid:
abort(400)
if jwk['kty'] == 'EC':
if 'x' not in jwk or 'y' not in jwk:
abort(400)
@ -69,15 +85,30 @@ def put_service_keys(service, kid):
else:
abort(400)
data.model.service_keys.upsert_service_key(kid, service, jwk, expiration_date)
encoded_jwt = request.headers.get(JWT_HEADER_NAME, None)
if not encoded_jwt:
abort(400)
signer_jwk = _signer_jwk(encoded_jwt, jwk, service, kid)
_validate_JWT(encoded_jwt, signer_jwk, service)
metadata = {
'ip': request.remote_addr,
'signer_jwk': signer_jwk,
}
data.model.service_keys.upsert_service_key('', kid, service, jwk, metadata, expiration_date)
@key_server.route('/services/<service>/keys/<kid>', methods=['DELETE'])
def delete_service_key(service, kid):
jwt = request.headers.get(JWT_HEADER_NAME, None)
if not jwt:
encoded_jwt = request.headers.get(JWT_HEADER_NAME, None)
if not encoded_jwt:
abort(400)
_validate_JWT(jwt, service, kid)
signer_jwk = _signer_jwk(encoded_jwt, None, service, kid)
_validate_JWT(encoded_jwt, signer_jwk, service)
try:
data.model.service_keys.delete_service_key(service, kid)