rework superuser api

This commit is contained in:
Jimmy Zelinskie 2016-03-25 18:44:11 -04:00 committed by Jimmy Zelinskie
parent 4079dba167
commit 35ed73e195
4 changed files with 205 additions and 60 deletions

View file

@ -12,6 +12,7 @@ from random import SystemRandom
import resumablehashlib
import toposort
from enum import Enum
from peewee import *
from sqlalchemy.engine.url import make_url
@ -869,6 +870,9 @@ class TorrentInfo(BaseModel):
)
class ServiceKeyApprovalType(Enum):
SUPERUSER = 'Super User API'
_ServiceKeyApproverProxy = Proxy()
class ServiceKeyApproval(BaseModel):
approver = ForeignKeyField(_ServiceKeyApproverProxy, null=True)

View file

@ -22,7 +22,29 @@ def _gc_expired(service):
# TODO ACTION_LOGS for keys
def upsert_service_key(name, kid, service, jwk, metadata, expiration_date):
def create_service_key(name, kid, service, jwk, metadata, expiration_date):
_gc_expired(service)
sk = ServiceKey.create(name=name, kid=kid, service=service, jwk=jwk, metadata=metadata,
expiration_date=expiration_date)
superusers = User.select().where(User.username << app.config['SUPER_USERS'])
for superuser in superusers:
# TODO(jzelinskie): create notification type in the database migration
# I already put it in initdb
create_notification('service_key_submitted', superuser, {
'name': name,
'kid': kid,
'service': service,
'jwk': jwk,
'metadata': metadata,
'created_date': sk.created_date,
'expiration_date': expiration_date,
})
def update_service_key(name, kid, metadata, expiration_date):
_gc_expired(service)
try:
@ -33,29 +55,23 @@ def upsert_service_key(name, kid, service, jwk, metadata, expiration_date):
key.expiration_date = expiration_date
key.save()
except ServiceKey.DoesNotExist:
sk = ServiceKey.create(name=name, kid=kid, service=service, jwk=jwk, metadata=metadata,
expiration_date=expiration_date)
superusers = User.select().where(User.username << app.config['SUPER_USERS'])
for superuser in superusers:
# TODO(jzelinskie): create notification type in the database migration
create_notification('service_key_submitted', superuser, {
'name': name,
'kid': kid,
'service': service,
'jwk': jwk,
'metadata': metadata,
'created_date': sk.created_date,
'expiration_date': expiration_date,
})
raise ServiceKeyDoesNotExist
def get_service_keys(service, kid=None):
def get_service_keys(approved_only, kid=None, service=None):
_gc_expired(service)
query = ServiceKey.select().where(ServiceKey.service == service,
~(ServiceKey.approval >> None))
if kid:
query = ServiceKey.select()
if service is not None:
query = query.where(ServiceKey.service == service)
if kid is not None:
query.where(ServiceKey.kid == kid)
if approved_only:
query = query.where(~(ServiceKey.approval >> None))
return query
@ -69,11 +85,10 @@ def delete_service_key(service, kid):
raise ServiceKeyDoesNotExist()
def approve_service_key(service, kid, approver, approval_type):
def approve_service_key(kid, approver, approval_type):
try:
with db_transaction():
key = db_for_update(ServiceKey.select().where(ServiceKey.service == service,
ServiceKey.kid == kid)).get()
key = db_for_update(ServiceKey.select().where(ServiceKey.kid == kid)).get()
if key.approval is not None:
raise ServiceKeyAlreadyApproved

View file

@ -4,8 +4,10 @@ import string
import logging
import os
from datetime import datetime
from random import SystemRandom
from flask import request, make_response
from flask import request, make_response, jsonify
import features
@ -469,38 +471,154 @@ class SuperUserOrganizationManagement(ApiResource):
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')
@resource('/v1/superuser/keys')
@show_if(features.SUPER_USERS)
class SuperUserServiceKeyManagement(ApiResource):
""" Resource for managing service keys. """
""" Resource for managing service keys."""
schemas = {
'ApproveServiceKey': {
'id': 'ApproveServiceKey',
'CreateServiceKey': {
'id': 'PutServiceKey',
'type': 'object',
'description': 'Description of approved keys for a service',
'description': 'Description of creation of a service key',
'required': ['service', 'expiration'],
'properties': {
'kid': {
'service': {
'type': 'string',
'description': 'The key being approved for service authentication usage.',
'description': 'The service authenticating with this key',
},
'name': {
'type': 'string',
'description': 'The friendly name of a service key',
},
'metadata': {
'type': 'object',
'description': 'The key/value pairs of this key\'s metadata',
},
'expiration': {
'description': 'The expiration date as a unix timestamp',
'anyOf': [{'type': 'string'}, {'type': 'null'}],
},
},
},
}
@verify_not_prod
@nickname('approveServiceKey')
@validate_json_request('ApproveServiceKey')
@nickname('getServiceKeys')
@require_scope(scopes.SUPERUSER)
def put(self, service, kid):
def get():
if SuperUserPermission().can():
return jsonify(list(model.service_keys.get_service_keys(False)))
abort(403)
@verify_not_prod
@nickname('createServiceKey')
@require_scope(scopes.SUPERUSER)
@validate_json_request('PutServiceKey')
def post(self, service):
if SuperUserPermission().can():
body = request.get_json()
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_authenticate_user()
metadata = body.get('metadata', {})
metadata.update({
'created_by': 'Quay SuperUser Panel',
'creator': user.username,
'superuser ip': request.remote_addr,
})
key = RSAKey(key=RSA.generate(2048))
key.serialize()
jwk = json.dumps(key.to_dict(), "enc")
public_key, private_key = generate_key_pair()
jwk = jwk(private_key)
kid = kid(private_key)
model.service_keys.create_service_key(body['name'] or '', kid, jwk, metadata, expiration_date)
model.service_keys.approve_service_key(kid, user,
return jsonify({'public_key': public_key, 'private_key': private_key})
abort(403)
@resource('/v1/superuser/keys/<kid>')
@path_param('kid', 'The unique identifier for a service key')
@show_if(features.SUPER_USERS)
class SuperUserServiceKeyManagement(ApiResource):
""" Resource for managing service keys. """
schemas = {
'PutServiceKey': {
'id': 'PutServiceKey',
'type': 'object',
'description': 'Description of updates for a service key',
'required': ['name', 'metadata', 'expiration'],
'properties': {
'name': {
'type': 'string',
'description': 'The friendly name of a service key',
},
'metadata': {
'type': 'object',
'description': 'The key/value pairs of this key\'s metadata',
},
'expiration': {
'description': 'The expiration date as a unix timestamp',
'anyOf': [{'type': 'string'}, {'type': 'null'}],
},
},
},
}
@verify_not_prod
@nickname('putServiceKey')
@require_scope(scopes.SUPERUSER)
@validate_json_request('PutServiceKey')
def put(self, kid):
if SuperUserPermission().can():
body = request.get_json()
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)
model.service_keys.update_service_key(body['name'], kid, body['metadata'], expiration_date)
abort(403)
@resource('/v1/superuser/approvedkeys/<kid>')
@path_param('kid', 'The unique identifier for a service key')
@show_if(features.SUPER_USERS)
class SuperUserServiceKeyApproval(ApiResource):
""" Resource for approving service keys. """
@verify_not_prod
@nickname('approveServiceKey')
@require_scope(scopes.SUPERUSER)
@validate_json_request('ApproveServiceKey')
def put(self, kid):
if SuperUserPermission().can():
approver = get_authenticated_user()
try:
model.service_keys.approve_service_key(service, kid, approver, 'Quay SuperUser API')
model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER)
except model.ServiceKeyDoesNotExist:
abort(404)
except model.ServiceKeyAlreadyApproved:
pass
make_response('', 200)
abort(403)

View file

@ -18,7 +18,24 @@ JWT_HEADER_NAME = 'Authorization'
JWT_AUDIENCE = 'quay'
def _validate_JWT(encoded_jwt, jwk, service):
def _validate_jwk(jwk, 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)
elif jwk['kty'] == 'RSA':
if 'e' not in jwk or 'n' not in jwk:
abort(400)
else:
abort(400)
def _validate_jwt(encoded_jwt, jwk, service):
public_key = RSAPublicNumbers(e=jwk.e,
n=jwk.n).public_key(default_backend())
@ -47,15 +64,16 @@ def _signer_jwk(encoded_jwt, jwk, service, kid):
@key_server.route('/services/<service>/keys', methods=['GET'])
def get_service_keys(service):
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)
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=['GET'])
def get_service_key(service, kid):
key = data.model.service_keys.get_service_keys(service, kid=kid)
return jsonify(key.jwk)
@key_server.route('/services/<service>/keys/<kid>', methods=['PUT'])
def put_service_keys(service, kid):
expiration_date = request.args.get('expiration', None)
@ -70,27 +88,14 @@ def put_service_keys(service, kid):
except ValueError:
abort(400)
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)
elif jwk['kty'] == 'RSA':
if 'e' not in jwk or 'n' not in jwk:
abort(400)
else:
abort(400)
_validate_jwk(jwk, kid)
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)
_validate_jwt(encoded_jwt, signer_jwk, service)
metadata = {
'ip': request.remote_addr,
@ -98,7 +103,10 @@ def put_service_keys(service, kid):
}
data.model.service_keys.upsert_service_key('', kid, service, jwk, metadata, expiration_date)
try:
data.model.service_keys.update_service_key('', kid, service, metadata, expiration_date)
except data.model.ServiceKeyDoesNotExist:
data.model.service_keys.create_service_key('', kid, service, jwk, metadata, expiration_date)
@key_server.route('/services/<service>/keys/<kid>', methods=['DELETE'])
@ -108,7 +116,7 @@ def delete_service_key(service, kid):
abort(400)
signer_jwk = _signer_jwk(encoded_jwt, None, service, kid)
_validate_JWT(encoded_jwt, signer_jwk, service)
_validate_jwt(encoded_jwt, signer_jwk, service)
try:
data.model.service_keys.delete_service_key(service, kid)