diff --git a/data/database.py b/data/database.py index 90d02c1e7..ab30d2d22 100644 --- a/data/database.py +++ b/data/database.py @@ -223,7 +223,6 @@ class LogEntry(BaseModel): access_token = ForeignKeyField(AccessToken, null=True) datetime = DateTimeField(default=datetime.now, index=True) ip = CharField(null=True) - description = TextField(null=True) metadata_json = TextField(default='{}') diff --git a/data/model.py b/data/model.py index 976868a26..ac56af88d 100644 --- a/data/model.py +++ b/data/model.py @@ -990,6 +990,7 @@ def set_repo_delegate_token_role(namespace_name, repository_name, code, role): def delete_delegate_token(namespace_name, repository_name, code): token = get_repo_delegate_token(namespace_name, repository_name, code) token.delete_instance() + return token def load_token_data(code): @@ -1055,6 +1056,7 @@ def list_webhooks(namespace_name, repository_name): def delete_webhook(namespace_name, repository_name, public_id): webhook = get_webhook(namespace_name, repository_name, public_id) webhook.delete_instance() + return webhook def list_logs(user_or_organization_name): week_ago = datetime.today() - timedelta(7) # One week @@ -1062,9 +1064,9 @@ def list_logs(user_or_organization_name): return joined.where(User.username == user_or_organization_name, LogEntry.datetime >= week_ago).order_by(LogEntry.datetime.desc()) def log_action(kind_name, user_or_organization_name, performer=None, repository=None, - access_token=None, ip=None, description=None, metadata={}): + access_token=None, ip=None, metadata={}): kind = LogEntryKind.get(LogEntryKind.name == kind_name) account = User.get(User.username == user_or_organization_name) entry = LogEntry.create(kind=kind, account=account, performer=performer, repository=repository, access_token=access_token, ip=ip, - description=description, metadata_json=json.dumps(metadata)) + metadata_json=json.dumps(metadata)) diff --git a/endpoints/api.py b/endpoints/api.py index 8e597f29e..ef733e4ea 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -35,10 +35,10 @@ user_files = app.config['USERFILES'] logger = logging.getLogger(__name__) -def log_action(kind, user_or_orgname, description=None, metadata={}, repo=None): +def log_action(kind, user_or_orgname, metadata={}, repo=None): performer = current_user.db_user() - model.log_action(kind, user_or_orgname, performer=parent, ip=request.remote_addr, - description=description, metadata=metadata, repository=repo) + model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr, + metadata=metadata, repository=repo) def api_login_required(f): @wraps(f) @@ -148,7 +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') + log_action('account_convert', user.username) # And finally login with the admin credentials. return conduct_signin(admin_username, admin_password) @@ -165,7 +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') + log_action('account_change_password', user.username) model.change_password(user, user_data['password']) if 'invoice_email' in user_data: @@ -494,21 +494,21 @@ 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}) + log_action('org_create_team', orgname, {'team': teamname}) if is_existing: - if 'description' in details: + if 'description' in details and team.description != details['description']: team.description = details['description'] team.save() - log_action('org_set_team_description', orgname, 'Set description for {team}: {description}', - {'team': teamname, 'description': team.description}) + log_action('org_set_team_description', orgname, {'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']}) - + role = model.get_team_org_role(team).name + if role != details['role']: + team = model.set_team_org_permission(team, details['role'], + current_user.db_user().username) + log_action('org_set_team_role', orgname, {'team': teamname, 'role': details['role']}) + resp = jsonify(team_view(orgname, team)) if not is_existing: resp.status_code = 201 @@ -524,7 +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}) + log_action('org_delete_team', orgname, {'team': teamname}) return make_response('Deleted', 204) abort(403) @@ -575,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}) + log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname}) return jsonify(member_view(user)) abort(403) @@ -590,7 +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}) + log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname}) return make_response('Deleted', 204) abort(403) @@ -619,8 +619,7 @@ 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) + log_action('create_repo', namespace_name, {'repo': repository_name, 'namespace': namespace_name}, repo=repo) return jsonify({ 'namespace': namespace_name, 'name': repository_name @@ -705,8 +704,8 @@ def update_repo_api(namespace, repository): 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) + log_action('set_repo_description', namespace, {'repo': repository, 'description': values['description']}, + repo=repo) return jsonify({ 'success': True }) @@ -725,8 +724,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) + log_action('change_repo_visibility', namespace, {'repo': repository, 'visibility': values['visibility']}, + repo=repo) return jsonify({ 'success': True }) @@ -742,7 +741,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}) + log_action('delete_repo', namespace, {'repo': repository, 'namespace': namespace}) return make_response('Deleted', 204) abort(403) @@ -858,7 +857,7 @@ 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}', + log_action('build_dockerfile', namespace, {'repo': repository, 'namespace': namespace, 'fileid': dockerfile_id}, repo=repo) resp = jsonify({ @@ -889,7 +888,7 @@ 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}', + log_action('add_repo_webhook', namespace, {'repo': repository, 'webhook_id': webhook.public_id}, repo=repo) return resp @@ -931,7 +930,7 @@ 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}', + log_action('delete_repo_webhook', namespace, {'repo': repository, 'webhook_id': public_id}, repo=model.get_repository(namespace, repository)) return make_response('No Content', 204) @@ -1174,7 +1173,7 @@ 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}', + log_action('change_repo_permission', namespace, {'username': username, 'repo': repository, 'role': new_permission['role']}, repo=model.get_repository(namespace, repository)) @@ -1201,7 +1200,7 @@ 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}', + log_action('change_repo_permission', namespace, {'team': team, 'repo': repository, 'role': new_permission['role']}, repo=model.get_repository(namespace, repository)) @@ -1229,8 +1228,8 @@ 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)) + log_action('delete_repo_permission', namespace, {'username': username, 'repo': repository}, + repo=model.get_repository(namespace, repository)) return make_response('Deleted', 204) @@ -1246,8 +1245,8 @@ def delete_team_permissions(namespace, repository, teamname): 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)) + log_action('delete_repo_permission', namespace, {'team': teamname, 'repo': repository}, + repo=model.get_repository(namespace, repository)) return make_response('Deleted', 204) @@ -1300,8 +1299,7 @@ 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']}, + log_action('add_repo_accesstoken', namespace, {'repo': repository, 'token': token_params['friendlyName']}, repo = model.get_repository(namespace, repository)) resp = jsonify(token_view(token)) @@ -1325,8 +1323,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)) + log_action('change_repo_permission', namespace, + {'repo': repository, 'token': token.friendly_name, 'code': code}, + repo = model.get_repository(namespace, repository)) resp = jsonify(token_view(token)) return resp @@ -1341,10 +1340,11 @@ def change_token(namespace, repository, code): def delete_token(namespace, repository, code): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): - model.delete_delegate_token(namespace, repository, code) + token = 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)) + log_action('delete_repo_accesstoken', namespace, + {'repo': repository, 'token': token.friendly_name, 'code': code}, + repo = model.get_repository(namespace, repository)) return make_response('Deleted', 204) @@ -1384,7 +1384,7 @@ def set_user_card_api(): user = current_user.db_user() token = request.get_json()['token'] response = set_card(user, token) - log_action('account_change_cc', user.username, 'Change account credit card') + log_action('account_change_cc', user.username) return response @@ -1396,7 +1396,7 @@ def set_org_card_api(orgname): organization = model.get_organization(orgname) token = request.get_json()['token'] response = set_card(organization, token) - log_action('account_change_cc', orgname, 'Change organization account credit card') + log_action('account_change_cc', orgname) return response abort(403) @@ -1486,7 +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}) + log_action('account_change_plan', user.username, {'plan': plan}) except stripe.CardError as e: return carderror_response(e) @@ -1502,7 +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}) + log_action('account_change_plan', user.username, {'plan': plan}) else: # User may have been a previous customer who is resubscribing @@ -1517,7 +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}) + log_action('account_change_plan', user.username, {'plan': plan}) resp = jsonify(response_json) resp.status_code = status_code @@ -1645,7 +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}) + log_action('create_robot', parent.username, {'robot': robot_shortname}) resp.status_code = 201 return resp @@ -1659,7 +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}) + log_action('create_robot', orgname, {'robot': robot_shortname}) resp.status_code = 201 return resp @@ -1671,7 +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}) + log_action('delete_robot', parent.username, {'robot': robot_shortname}) return make_response('No Content', 204) @@ -1682,7 +1682,7 @@ 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}) + log_action('delete_robot', orgname, {'robot': robot_shortname}) return make_response('No Content', 204) abort(403) @@ -1694,7 +1694,6 @@ 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': { diff --git a/endpoints/index.py b/endpoints/index.py index a04bdbda4..8b4e90b84 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -179,15 +179,12 @@ def create_repository(namespace, repository): mixpanel.track(get_authenticated_user().username, 'push_repo', extra_params) metadata['username'] = get_authenticated_user().username - description = 'Repository {repo} pushed by user {username}' else: mixpanel.track(get_validated_token().code, 'push_repo', extra_params) metadata['token'] = get_validated_token().friendly_name metadata['token_code'] = get_validated_token().code - description = 'Repository {repo} pushed via access token {token}' model.log_action('push_repo', namespace, performer = get_authenticated_user(), ip = request.remote_addr, - description = description, metadata = metadata, repository = repo) @@ -266,9 +263,17 @@ def get_repository_images(namespace, repository): resp = make_response(json.dumps(all_images), 200) resp.mimetype = 'application/json' - pull_username = 'anonymous' + metadata = { + 'repo': repository, + 'namespace': namespace, + } if get_authenticated_user(): - pull_username = get_authenticated_user().username + metadata['username']= get_authenticated_user().username + elif get_validated_token(): + metadata['token'] = get_validated_token().friendly_name + metadata['token_code'] = get_validated_token().code + else: + metadata['public'] = True extra_params = { 'repository': '%s/%s' % (namespace, repository), @@ -276,14 +281,7 @@ def get_repository_images(namespace, repository): mixpanel.track(pull_username, 'pull_repo', extra_params) model.log_action('pull_repo', namespace, performer = get_authenticated_user(), ip = request.remote_addr, - description = 'Repository {repo} pulled', - metadata = { - 'repo': repository, - 'namespace': namespace, - 'username': pull_username, - 'public': is_public - }, - repository = repo) + metadata = metadata, repository = repo) return resp abort(403) diff --git a/static/js/app.js b/static/js/app.js index 708e9b3c5..3eea068a7 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -686,6 +686,42 @@ quayApp.directive('logsView', function () { $scope.kindsAllowed = null; $scope.chartVisible = true; + var logDescriptions = { + 'account_change_plan': 'Change plan', + 'account_change_cc': 'Update credit card', + 'account_change_password': 'Change password', + 'account_convert': 'Convert account to organization', + 'create_robot': 'Create Robot Account: {robot}', + 'delete_robot': 'Delete Robot Account: {robot}', + 'create_repo': 'Create Repository: {repo}', + 'push_repo': 'Push to repository: {repo}', + 'pull_repo': function(metadata) { + if (metadata.token) { + return 'Pull repository {repo} via token {token}'; + } else if (metadata.username) { + return 'Pull repository {repo} by {username}'; + } else { + return 'Public pull of repository {repo} by {_ip}'; + } + }, + 'delete_repo': 'Delete repository: {repo}', + 'change_repo_permission': 'Change permission for user {username} in repository {repo} to {role}', + 'delete_repo_permission': 'Remove permission for user {username} from repository {repo}', + 'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}', + 'add_repo_accesstoken': 'Create access token {token} in repository {repo}', + 'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}', + 'add_repo_webhook': 'Add webhook in repository {repo}', + 'delete_repo_webhook': 'Delete webhook in repository {repo}', + 'set_repo_description': 'Change description for repository {repo}: {description}', + 'build_dockerfile': 'Build image from Dockerfile for repository {repo}', + 'org_create_team': 'Create team: {team}', + 'org_delete_team': 'Delete team: {team}', + 'org_add_team_member': 'Add member {member} to team {team}', + 'org_remove_team_member': 'Remove member {member} from team {team}', + 'org_set_team_description': 'Change description of team {team}: {description}', + 'org_set_team_role': 'Change permission of team {team} to {role}' + }; + var logKinds = { 'account_change_plan': 'Change plan', 'account_change_cc': 'Update credit card', @@ -697,7 +733,6 @@ quayApp.directive('logsView', function () { 'push_repo': 'Push to repository', 'pull_repo': 'Pull repository', 'delete_repo': 'Delete repository', - 'add_repo_permission': 'Add user permission to repository', 'change_repo_permission': 'Change repository permission', 'delete_repo_permission': 'Remove user permission from repository', 'change_repo_visibility': 'Change repository visibility', @@ -734,6 +769,7 @@ quayApp.directive('logsView', function () { } $scope.chart.draw('bar-chart', resp.logs); + $scope.kindsAllowed = null; $scope.logs = resp.logs; $scope.loading = false; }); @@ -752,7 +788,8 @@ quayApp.directive('logsView', function () { }; $scope.getDescription = function(log) { - var description = log.description; + var description = logDescriptions[log.kind] || logTitles[log.kind] || log.kind; + log.metadata['_ip'] = log.ip; for (var key in log.metadata) { if (log.metadata.hasOwnProperty(key)) { var markedDown = getMarkedDown(log.metadata[key].toString()); @@ -760,7 +797,7 @@ quayApp.directive('logsView', function () { description = description.replace('{' + key + '}', '' + markedDown + ''); } } - return $sce.trustAsHtml(description); + return $sce.trustAsHtml(description); }; $scope.$watch('organization', update);