This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/endpoints/api/superuser.py
2016-04-29 13:38:25 -04:00

652 lines
18 KiB
Python

""" Superuser API. """
import json
import logging
import os
import string
from datetime import datetime
from hashlib import sha256
from random import SystemRandom
from Crypto.PublicKey import RSA
from flask import request, make_response, jsonify
from jwkest.jwk import RSAKey
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 data.database import ServiceKeyApprovalType
from util import canonicalize
from util.useremails import send_confirmation_email, send_recovery_email
logger = logging.getLogger(__name__)
def get_immediate_subdirectories(directory):
return [name for name in os.listdir(directory) if os.path.isdir(os.path.join(directory, name))]
def get_services():
services = set(get_immediate_subdirectories(app.config['SYSTEM_SERVICES_PATH']))
services = services - set(app.config['SYSTEM_SERVICE_BLACKLIST'])
return services
@resource('/v1/superuser/systemlogs/<service>')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserGetLogsForService(ApiResource):
""" Resource for fetching the kinds of system logs in the system. """
@require_fresh_login
@verify_not_prod
@nickname('getSystemLogs')
@require_scope(scopes.SUPERUSER)
def get(self, service):
""" Returns the logs for the specific service. """
if SuperUserPermission().can():
if not service in get_services():
abort(404)
logs = []
try:
with open(app.config['SYSTEM_LOGS_FILE'], 'r') as f:
logs = [line for line in f if line.find(service + '[') >= 0]
except Exception:
logger.exception('Cannot read logs')
abort(400)
return {
'logs': '\n'.join(logs)
}
abort(403)
@resource('/v1/superuser/systemlogs/')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserSystemLogServices(ApiResource):
""" Resource for fetching the kinds of system logs in the system. """
@require_fresh_login
@verify_not_prod
@nickname('listSystemLogServices')
@require_scope(scopes.SUPERUSER)
def get(self):
""" List the system logs for the current system. """
if SuperUserPermission().can():
return {
'services': list(get_services())
}
abort(403)
@resource('/v1/superuser/aggregatelogs')
@internal_only
class SuperUserAggregateLogs(ApiResource):
""" Resource for fetching aggregated logs for the current user. """
@require_fresh_login
@verify_not_prod
@nickname('listAllAggregateLogs')
@parse_args()
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
def get(self, parsed_args):
""" Returns the aggregated logs for the current system. """
if SuperUserPermission().can():
start_time = parsed_args['starttime']
end_time = parsed_args['endtime']
return get_aggregate_logs(start_time, end_time)
abort(403)
@resource('/v1/superuser/logs')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserLogs(ApiResource):
""" Resource for fetching all logs in the system. """
@require_fresh_login
@verify_not_prod
@nickname('listAllLogs')
@parse_args()
@query_param('starttime', 'Earliest time from which to get logs (%m/%d/%Y %Z)', type=str)
@query_param('endtime', 'Latest time to which to get logs (%m/%d/%Y %Z)', type=str)
@query_param('page', 'The page number for the logs', type=int, default=1)
@page_support()
@require_scope(scopes.SUPERUSER)
def get(self, parsed_args, page_token):
""" List the usage logs for the current system. """
if SuperUserPermission().can():
start_time = parsed_args['starttime']
end_time = parsed_args['endtime']
return get_logs(start_time, end_time, page_token=page_token)
abort(403)
def org_view(org):
return {
'name': org.username,
'email': org.email,
'avatar': avatar.get_data_for_org(org),
}
def user_view(user, password=None):
user_data = {
'username': user.username,
'email': user.email,
'verified': user.verified,
'avatar': avatar.get_data_for_user(user),
'super_user': superusers.is_superuser(user.username),
'enabled': user.enabled,
}
if password is not None:
user_data['encrypted_password'] = authentication.encrypt_user_password(password)
return user_data
@resource('/v1/superuser/changelog/')
@internal_only
@show_if(features.SUPER_USERS)
class ChangeLog(ApiResource):
""" Resource for returning the change log for enterprise customers. """
@require_fresh_login
@verify_not_prod
@nickname('getChangeLog')
@require_scope(scopes.SUPERUSER)
def get(self):
""" Returns the change log for this installation. """
if SuperUserPermission().can():
with open('CHANGELOG.md', 'r') as f:
return {
'log': f.read()
}
abort(403)
@resource('/v1/superuser/organizations/')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserOrganizationList(ApiResource):
""" Resource for listing organizations in the system. """
@require_fresh_login
@verify_not_prod
@nickname('listAllOrganizations')
@require_scope(scopes.SUPERUSER)
def get(self):
""" Returns a list of all organizations in the system. """
if SuperUserPermission().can():
orgs = model.organization.get_organizations()
return {
'organizations': [org_view(org) for org in orgs]
}
abort(403)
@resource('/v1/superuser/users/')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserList(ApiResource):
""" Resource for listing users in the system. """
schemas = {
'CreateInstallUser': {
'id': 'CreateInstallUser',
'description': 'Data for creating a user',
'required': ['username', 'email'],
'properties': {
'username': {
'type': 'string',
'description': 'The username of the user being created'
},
'email': {
'type': 'string',
'description': 'The email address of the user being created'
}
}
}
}
@require_fresh_login
@verify_not_prod
@nickname('listAllUsers')
@require_scope(scopes.SUPERUSER)
def get(self):
""" Returns a list of all users in the system. """
if SuperUserPermission().can():
users = model.user.get_active_users()
return {
'users': [user_view(user) for user in users]
}
abort(403)
@require_fresh_login
@verify_not_prod
@nickname('createInstallUser')
@validate_json_request('CreateInstallUser')
@require_scope(scopes.SUPERUSER)
def post(self):
""" Creates a new user. """
# Ensure that we are using database auth.
if app.config['AUTHENTICATION_TYPE'] != 'Database':
abort(400)
user_information = request.get_json()
if SuperUserPermission().can():
username = user_information['username']
email = user_information['email']
# Generate a temporary password for the user.
random = SystemRandom()
password = ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(32)])
# Create the user.
user = model.user.create_user(username, password, email, auto_verify=not features.MAILING)
# If mailing is turned on, send the user a verification email.
if features.MAILING:
confirmation = model.user.create_confirm_email_code(user)
send_confirmation_email(user.username, user.email, confirmation.code)
return {
'username': username,
'email': email,
'password': password,
'encrypted_password': authentication.encrypt_user_password(password),
}
abort(403)
@resource('/v1/superusers/users/<username>/sendrecovery')
@internal_only
@show_if(features.SUPER_USERS)
@show_if(features.MAILING)
class SuperUserSendRecoveryEmail(ApiResource):
""" Resource for sending a recovery user on behalf of a user. """
@require_fresh_login
@verify_not_prod
@nickname('sendInstallUserRecoveryEmail')
@require_scope(scopes.SUPERUSER)
def post(self, username):
# Ensure that we are using database auth.
if app.config['AUTHENTICATION_TYPE'] != 'Database':
abort(400)
if SuperUserPermission().can():
user = model.user.get_nonrobot_user(username)
if not user:
abort(404)
if superusers.is_superuser(username):
abort(403)
code = model.user.create_reset_password_email_code(user.email)
send_recovery_email(user.email, code.code)
return {
'email': user.email
}
abort(403)
@resource('/v1/superuser/users/<username>')
@path_param('username', 'The username of the user being managed')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserManagement(ApiResource):
""" Resource for managing users in the system. """
schemas = {
'UpdateUser': {
'id': 'UpdateUser',
'type': 'object',
'description': 'Description of updates for a user',
'properties': {
'password': {
'type': 'string',
'description': 'The new password for the user',
},
'email': {
'type': 'string',
'description': 'The new e-mail address for the user',
},
'enabled': {
'type': 'boolean',
'description': 'Whether the user is enabled'
}
},
},
}
@require_fresh_login
@verify_not_prod
@nickname('getInstallUser')
@require_scope(scopes.SUPERUSER)
def get(self, username):
""" Returns information about the specified user. """
if SuperUserPermission().can():
user = model.user.get_nonrobot_user(username)
if not user:
abort(404)
return user_view(user)
abort(403)
@require_fresh_login
@verify_not_prod
@nickname('deleteInstallUser')
@require_scope(scopes.SUPERUSER)
def delete(self, username):
""" Deletes the specified user. """
if SuperUserPermission().can():
user = model.user.get_nonrobot_user(username)
if not user:
abort(404)
if superusers.is_superuser(username):
abort(403)
model.user.delete_user(user)
return 'Deleted', 204
abort(403)
@require_fresh_login
@verify_not_prod
@nickname('changeInstallUser')
@validate_json_request('UpdateUser')
@require_scope(scopes.SUPERUSER)
def put(self, username):
""" Updates information about the specified user. """
if SuperUserPermission().can():
user = model.user.get_nonrobot_user(username)
if not user:
abort(404)
if superusers.is_superuser(username):
abort(403)
user_data = request.get_json()
if 'password' in user_data:
# Ensure that we are using database auth.
if app.config['AUTHENTICATION_TYPE'] != 'Database':
abort(400)
model.user.change_password(user, user_data['password'])
if 'email' in user_data:
# Ensure that we are using database auth.
if app.config['AUTHENTICATION_TYPE'] != 'Database':
abort(400)
model.user.update_email(user, user_data['email'], auto_verify=True)
if 'enabled' in user_data:
# Disable/enable the user.
user.enabled = bool(user_data['enabled'])
user.save()
if 'superuser' in user_data:
config_object = config_provider.get_config()
superusers_set = set(config_object['SUPER_USERS'])
if user_data['superuser']:
superusers_set.add(username)
elif username in superusers_set:
superusers_set.remove(username)
config_object['SUPER_USERS'] = list(superusers_set)
config_provider.save_config(config_object)
return user_view(user, password=user_data.get('password'))
abort(403)
@resource('/v1/superuser/organizations/<name>')
@path_param('name', 'The name of the organizaton being managed')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserOrganizationManagement(ApiResource):
""" Resource for managing organizations in the system. """
schemas = {
'UpdateOrg': {
'id': 'UpdateOrg',
'type': 'object',
'description': 'Description of updates for an organization',
'properties': {
'name': {
'type': 'string',
'description': 'The new name for the organization',
}
},
},
}
@require_fresh_login
@verify_not_prod
@nickname('deleteOrganization')
@require_scope(scopes.SUPERUSER)
def delete(self, name):
""" Deletes the specified organization. """
if SuperUserPermission().can():
org = model.organization.get_organization(name)
model.user.delete_user(org)
return 'Deleted', 204
abort(403)
@require_fresh_login
@verify_not_prod
@nickname('changeOrganization')
@validate_json_request('UpdateOrg')
@require_scope(scopes.SUPERUSER)
def put(self, name):
""" Updates information about the specified user. """
if SuperUserPermission().can():
org = model.organization.get_organization(name)
org_data = request.get_json()
if 'name' in org_data:
org = model.user.change_username(org.id, org_data['name'])
return org_view(org)
abort(403)
def key_view(key):
return {
'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,
'approval': approval_view(key.approval) if key.approval is not None else None,
}
def approval_view(approval):
return {
'approver': user_view(approval.approver),
'approval_type': approval.approval_type,
'approved_date': approval.approved_date,
'notes': approval.notes,
}
@resource('/v1/superuser/keys')
@show_if(features.SUPER_USERS)
class SuperUserServiceKeyManagement(ApiResource):
""" Resource for managing service keys."""
schemas = {
'CreateServiceKey': {
'id': 'PutServiceKey',
'type': 'object',
'description': 'Description of creation of a service key',
'required': ['service', 'expiration'],
'properties': {
'service': {
'type': 'string',
'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('getServiceKeys')
@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]})
abort(403)
@verify_not_prod
@nickname('createServiceKey')
@require_scope(scopes.SUPERUSER)
@validate_json_request('PutServiceKey')
def post(self):
if SuperUserPermission().can():
body = request.get_json()
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)
user = get_authenticated_user()
metadata = body.get('metadata', {})
metadata.update({
'created_by': 'Quay SuperUser Panel',
'creator': user.username,
'ip': request.remote_addr,
})
private_key = RSA.generate(2048)
jwk = RSAKey(key=private_key.publickey()).serialize()
kid = sha256(json.dumps(canonicalize(jwk), separators=(',', ':'))).hexdigest()
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')})
abort(403)
@resource('/v1/superuser/keys/<kid>')
@path_param('kid', 'The unique identifier for a service key')
@show_if(features.SUPER_USERS)
class SuperUserServiceKeyUpdater(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)
def put(self, kid):
if SuperUserPermission().can():
approver = get_authenticated_user()
try:
model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER)
except model.ServiceKeyDoesNotExist:
abort(404)
except model.ServiceKeyAlreadyApproved:
pass
return make_response('', 200)
abort(403)