Add support for full logging of all actions in Quay, and the ability to view and filter these logs in the org’s admin view

This commit is contained in:
Joseph Schorr 2013-11-27 02:29:31 -05:00
parent d5c0f768c2
commit cca5daf097
16 changed files with 25024 additions and 16 deletions

View file

@ -17,6 +17,7 @@ from app import app
from util.email import send_confirmation_email, send_recovery_email
from util.names import parse_repository_name, format_robot_username
from util.gravatar import compute_hash
from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission,
AdministerRepositoryPermission,
@ -34,6 +35,11 @@ user_files = app.config['USERFILES']
logger = logging.getLogger(__name__)
def log_action(kind, user_or_orgname, description=None, metadata={}, repo=None):
parent = current_user.db_user()
model.log_action(kind, user_or_orgname, performer = parent, ip = request.remote_addr,
description = description, metadata = metadata, repository = repo)
def api_login_required(f):
@wraps(f)
def decorated_view(*args, **kwargs):
@ -142,6 +148,7 @@ def convert_user_to_organization():
# Convert the user to an organization.
model.convert_user_to_organization(user, model.get_user(admin_username))
log_action('account_convert', user.username, 'Convert account to an organization')
# And finally login with the admin credentials.
return conduct_signin(admin_username, admin_password)
@ -158,6 +165,7 @@ def change_user_details():
try:
if 'password' in user_data:
logger.debug('Changing password for user: %s', user.username)
log_action('account_change_password', user.username, 'Change account password')
model.change_password(user, user_data['password'])
if 'invoice_email' in user_data:
@ -486,14 +494,20 @@ def update_organization_team(orgname, teamname):
org = model.get_organization(orgname)
team = model.create_team(teamname, org, role, description)
log_action('org_create_team', orgname, 'Creation of team {team}', {'team': teamname})
if is_existing:
if 'description' in details:
team.description = details['description']
team.save()
log_action('org_set_team_description', orgname, 'Set description for {team}: {description}',
{'team': teamname, 'description': team.description})
if 'role' in details:
team = model.set_team_org_permission(team, details['role'],
current_user.db_user().username)
log_action('org_set_team_role', orgname, 'Set role for {team} to {role}',
{'team': teamname, 'role': details['role']})
resp = jsonify(team_view(orgname, team))
if not is_existing:
@ -510,6 +524,7 @@ def delete_organization_team(orgname, teamname):
permission = AdministerOrganizationPermission(orgname)
if permission.can():
model.remove_team(orgname, teamname, current_user.db_user().username)
log_action('org_delete_team', orgname, 'Deletion of team {team}', {'team': teamname})
return make_response('Deleted', 204)
abort(403)
@ -560,7 +575,7 @@ def update_organization_team_member(orgname, teamname, membername):
# Add the user to the team.
model.add_user_to_team(user, team)
log_action('org_add_team_member', orgname, 'Add member {member} to team {team}', {'member': membername, 'team': teamname})
return jsonify(member_view(user))
abort(403)
@ -575,6 +590,7 @@ def delete_organization_team_member(orgname, teamname, membername):
# Remote the user from the team.
invoking_user = current_user.db_user().username
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
log_action('org_remove_team_member', orgname, 'Remove member {member} from team {team}', {'member': membername, 'team': teamname})
return make_response('Deleted', 204)
abort(403)
@ -603,6 +619,8 @@ def create_repo_api():
repo.description = req['description']
repo.save()
log_action('create_repo', namespace_name, 'Create repository {repo}',
{'repo': repository_name, 'namespace': namespace_name}, repo=repo)
return jsonify({
'namespace': namespace_name,
'name': repository_name
@ -686,6 +704,9 @@ def update_repo_api(namespace, repository):
values = request.get_json()
repo.description = values['description']
repo.save()
log_action('set_repo_description', namespace, 'Set description of repository {repo}: {description}',
{'repo': repository, 'description': values['description']}, repo=repo)
return jsonify({
'success': True
})
@ -704,6 +725,8 @@ def change_repo_visibility_api(namespace, repository):
if repo:
values = request.get_json()
model.set_repository_visibility(repo, values['visibility'])
log_action('change_repo_visibility', namespace, 'Change visibility of repository {repo}: {visibility}',
{'repo': repository, 'visibility': values['visibility']}, repo=repo)
return jsonify({
'success': True
})
@ -719,6 +742,7 @@ def delete_repository(namespace, repository):
if permission.can():
model.purge_repository(namespace, repository)
registry.delete_repository_storage(namespace, repository)
log_action('delete_repo', namespace, 'Delete repository {repo}', {'repo': repository, 'namespace': namespace})
return make_response('Deleted', 204)
abort(403)
@ -834,6 +858,9 @@ def request_repo_build(namespace, repository):
tag)
dockerfile_build_queue.put(json.dumps({'build_id': build_request.id}))
log_action('build_dockerfile', namespace, 'Build dockerfile and update repository {repo}',
{'repo': repository, 'namespace': namespace, 'fileid': dockerfile_id}, repo=repo)
resp = jsonify({
'started': True
})
@ -862,6 +889,8 @@ def create_webhook(namespace, repository):
repo_string = '%s/%s' % (namespace, repository)
resp.headers['Location'] = url_for('get_webhook', repository=repo_string,
public_id=webhook.public_id)
log_action('add_repo_webhook', namespace, 'Create push webhook {webhook_id} on repo {repo}',
{'repo': repository, 'webhook_id': webhook.public_id}, repo=repo)
return resp
abort(403) # Permissions denied
@ -902,6 +931,9 @@ def delete_webhook(namespace, repository, public_id):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
model.delete_webhook(namespace, repository, public_id)
log_action('delete_repo_webhook', namespace, 'Delete webhook {webhook_id} on repository {repo}',
{'repo': repository, 'webhook_id': public_id},
repo=model.get_repository(namespace, repository))
return make_response('No Content', 204)
abort(403) # Permission denied
@ -1142,6 +1174,9 @@ def change_user_permissions(namespace, repository, username):
error_resp.status_code = 400
return error_resp
log_action('change_repo_permission', namespace, 'Change permissions for user {username} on repository {repo} to {role}',
{'username': username, 'repo': repository, 'role': new_permission['role']},
repo=model.get_repository(namespace, repository))
resp = jsonify(perm_view)
if request.method == 'POST':
@ -1166,6 +1201,10 @@ def change_team_permissions(namespace, repository, teamname):
perm = model.set_team_repo_permission(teamname, namespace, repository,
new_permission['role'])
log_action('change_repo_permission', namespace, 'Change permissions for team {team} on repository {repo} to {role}',
{'team': team, 'repo': repository, 'role': new_permission['role']},
repo=model.get_repository(namespace, repository))
resp = jsonify(role_view(perm))
if request.method == 'POST':
resp.status_code = 201
@ -1189,6 +1228,9 @@ def delete_user_permissions(namespace, repository, username):
})
error_resp.status_code = 400
return error_resp
log_action('delete_repo_permission', namespace, 'Delete permissions for user {username} on repository {repo}',
{'username': username, 'repo': repository}, repo=model.get_repository(namespace, repository))
return make_response('Deleted', 204)
@ -1203,6 +1245,10 @@ def delete_team_permissions(namespace, repository, teamname):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
model.delete_team_permission(teamname, namespace, repository)
log_action('delete_repo_permission', namespace, 'Delete permissions for team {team} on repository {repo}',
{'team': teamname, 'repo': repository}, repo=model.get_repository(namespace, repository))
return make_response('Deleted', 204)
abort(403) # Permission denied
@ -1254,6 +1300,10 @@ def create_token(namespace, repository):
token = model.create_delegate_token(namespace, repository,
token_params['friendlyName'])
log_action('add_repo_accesstoken', namespace, 'Add access token {name} for repository {repo}',
{'repo': repository, 'name': token_params['friendlyName']},
repo = model.get_repository(namespace, repository))
resp = jsonify(token_view(token))
resp.status_code = 201
return resp
@ -1275,6 +1325,9 @@ def change_token(namespace, repository, code):
token = model.set_repo_delegate_token_role(namespace, repository, code,
new_permission['role'])
log_action('change_repo_permission', namespace, 'Change permissions for access token {code} in repository {repo}',
{'repo': repository, 'code': code}, repo = model.get_repository(namespace, repository))
resp = jsonify(token_view(token))
return resp
@ -1289,6 +1342,10 @@ def delete_token(namespace, repository, code):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
model.delete_delegate_token(namespace, repository, code)
log_action('delete_repo_accesstoken', namespace, 'Delete access token {code} in repository {repo}',
{'repo': repository, 'code': code}, repo = model.get_repository(namespace, repository))
return make_response('Deleted', 204)
abort(403) # Permission denied
@ -1326,7 +1383,9 @@ def get_org_card_api(orgname):
def set_user_card_api():
user = current_user.db_user()
token = request.get_json()['token']
return set_card(user, token)
response = set_card(user, token)
log_action('account_change_cc', user.username, 'Change account credit card')
return response
@app.route('/api/organization/<orgname>/card', methods=['POST'])
@ -1336,7 +1395,9 @@ def set_org_card_api(orgname):
if permission.can():
organization = model.get_organization(orgname)
token = request.get_json()['token']
return jsonify(set_card(organization, token))
response = set_card(organization, token)
log_action('account_change_cc', orgname, 'Change organization account credit card')
return response
abort(403)
@ -1425,6 +1486,7 @@ def subscribe(user, plan, token, accepted_plans):
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id
user.save()
log_action('account_change_plan', user.username, 'Change subscription plan', {'plan': plan})
except stripe.CardError as e:
return carderror_response(e)
@ -1440,6 +1502,7 @@ def subscribe(user, plan, token, accepted_plans):
# We only have to cancel the subscription if they actually have one
cus.cancel_subscription()
cus.save()
log_action('account_change_plan', user.username, 'Change subscription plan', {'plan': plan})
else:
# User may have been a previous customer who is resubscribing
@ -1454,6 +1517,7 @@ def subscribe(user, plan, token, accepted_plans):
return carderror_response(e)
response_json = subscription_view(cus.subscription, private_repos)
log_action('account_change_plan', user.username, 'Change subscription plan', {'plan': plan})
resp = jsonify(response_json)
resp.status_code = status_code
@ -1581,6 +1645,7 @@ def create_robot(robot_shortname):
parent = current_user.db_user()
robot, password = model.create_robot(robot_shortname, parent)
resp = jsonify(robot_view(robot.username, password))
log_action('create_robot', parent.username, 'Creation of robot account {robot}', {'robot': robot_shortname})
resp.status_code = 201
return resp
@ -1594,6 +1659,7 @@ def create_org_robot(orgname, robot_shortname):
parent = model.get_organization(orgname)
robot, password = model.create_robot(robot_shortname, parent)
resp = jsonify(robot_view(robot.username, password))
log_action('create_robot', orgname, 'Creation of robot account {robot}', {'robot': robot_shortname})
resp.status_code = 201
return resp
@ -1605,6 +1671,7 @@ def create_org_robot(orgname, robot_shortname):
def delete_robot(robot_shortname):
parent = current_user.db_user()
model.delete_robot(format_robot_username(parent.username, robot_shortname))
log_action('delete_robot', parent.username, 'Deletion of robot account {robot}', {'robot': robot_shortname})
return make_response('No Content', 204)
@ -1615,6 +1682,33 @@ def delete_org_robot(orgname, robot_shortname):
permission = AdministerOrganizationPermission(orgname)
if permission.can():
model.delete_robot(format_robot_username(orgname, robot_shortname))
log_action('delete_robot', orgname, 'Deletion of robot account {robot}', {'robot': robot_shortname})
return make_response('No Content', 204)
abort(403)
@app.route('/api/organization/<orgname>/logs', methods=['GET'])
@api_login_required
def org_logs_api(orgname):
def log_view(log):
return {
'kind': log.kind.name,
'description': log.description,
'metadata': json.loads(log.metadata_json),
'ip': log.ip,
'performer': {
'username': log.performer.username,
'is_robot': log.performer.robot,
},
'datetime': log.datetime,
}
permission = AdministerOrganizationPermission(orgname)
if permission.can():
logs = model.list_logs(orgname)
return jsonify({
'logs': [log_view(log) for log in logs]
})
abort(403)