""" Access usage logs for organizations or repositories. """ import json from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args, RepositoryParamResource, require_repo_admin, related_user_resource, format_date, require_user_admin, path_param, require_scope, page_support) from endpoints.exception import Unauthorized, NotFound from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from data import model, database from auth import scopes from app import avatar from tzlocal import get_localzone LOGS_PER_PAGE = 20 SERVICE_LEVEL_LOG_KINDS = set(['service_key_create', 'service_key_approve', 'service_key_delete', 'service_key_modify', 'service_key_extend', 'service_key_rotate']) def log_view(log, kinds, include_namespace): view = { 'kind': kinds[log.kind_id], 'metadata': json.loads(log.metadata_json), 'ip': log.ip, 'datetime': format_date(log.datetime), } if log.performer and log.performer.username: view['performer'] = { 'kind': 'user', 'name': log.performer.username, 'is_robot': log.performer.robot, 'avatar': avatar.get_data_for_user(log.performer), } if include_namespace: if log.account and log.account.username: if log.account.organization: view['namespace'] = { 'kind': 'org', 'name': log.account.username, 'avatar': avatar.get_data_for_org(log.account), } else: view['namespace'] = { 'kind': 'user', 'name': log.account.username, 'avatar': avatar.get_data_for_user(log.account), } return view def aggregated_log_view(log, kinds, start_time): # Because we aggregate based on the day of the month in SQL, we only have that information. # Therefore, create a synthetic date based on the day and the month of the start time. # Logs are allowed for a maximum period of one week, so this calculation should always work. synthetic_date = datetime(start_time.year, start_time.month, int(log.day), tzinfo=get_localzone()) if synthetic_date.day < start_time.day: synthetic_date = synthetic_date + relativedelta(months=1) view = { 'kind': kinds[log.kind_id], 'count': log.count, 'datetime': format_date(synthetic_date), } return view def _validate_logs_arguments(start_time, end_time, performer_name): performer = None if performer_name: performer = model.user.get_user(performer_name) if start_time: try: start_time = datetime.strptime(start_time + ' UTC', '%m/%d/%Y %Z') except ValueError: start_time = None if not start_time: start_time = datetime.today() - timedelta(7) # One week if end_time: try: end_time = datetime.strptime(end_time + ' UTC', '%m/%d/%Y %Z') end_time = end_time + timedelta(days=1) except ValueError: end_time = None if not end_time: end_time = datetime.today() return (start_time, end_time, performer) def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None, page_token=None, ignore=None): (start_time, end_time, performer) = _validate_logs_arguments(start_time, end_time, performer_name) kinds = model.log.get_log_entry_kinds() logs_query = model.log.get_logs_query(start_time, end_time, performer=performer, repository=repository, namespace=namespace, ignore=ignore) logs, next_page_token = model.modelutil.paginate(logs_query, database.LogEntry, descending=True, page_token=page_token, limit=LOGS_PER_PAGE) include_namespace = namespace is None and repository is None return { 'start_time': format_date(start_time), 'end_time': format_date(end_time), 'logs': [log_view(log, kinds, include_namespace) for log in logs], }, next_page_token def get_aggregate_logs(start_time, end_time, performer_name=None, repository=None, namespace=None, ignore=None): (start_time, end_time, performer) = _validate_logs_arguments(start_time, end_time, performer_name) kinds = model.log.get_log_entry_kinds() aggregated_logs = model.log.get_aggregated_logs(start_time, end_time, performer=performer, repository=repository, namespace=namespace, ignore=ignore) return { 'aggregated': [aggregated_log_view(log, kinds, start_time) for log in aggregated_logs] } @resource('/v1/repository//logs') @path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryLogs(RepositoryParamResource): """ Resource for fetching logs for the specific repository. """ @require_repo_admin @nickname('listRepoLogs') @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() def get(self, namespace, repository, page_token, parsed_args): """ List the logs for the specified repository. """ repo = model.repository.get_repository(namespace, repository) if not repo: raise NotFound() start_time = parsed_args['starttime'] end_time = parsed_args['endtime'] return get_logs(start_time, end_time, repository=repo, page_token=page_token, ignore=SERVICE_LEVEL_LOG_KINDS) @resource('/v1/user/logs') class UserLogs(ApiResource): """ Resource for fetching logs for the current user. """ @require_user_admin @nickname('listUserLogs') @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('performer', 'Username for which to filter logs.', type=str) @page_support() def get(self, parsed_args, page_token): """ List the logs for the current user. """ performer_name = parsed_args['performer'] start_time = parsed_args['starttime'] end_time = parsed_args['endtime'] user = get_authenticated_user() return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username, page_token=page_token, ignore=SERVICE_LEVEL_LOG_KINDS) @resource('/v1/organization//logs') @path_param('orgname', 'The name of the organization') @related_user_resource(UserLogs) class OrgLogs(ApiResource): """ Resource for fetching logs for the entire organization. """ @nickname('listOrgLogs') @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('performer', 'Username for which to filter logs.', type=str) @query_param('page', 'The page number for the logs', type=int, default=1) @page_support() @require_scope(scopes.ORG_ADMIN) def get(self, orgname, page_token, parsed_args): """ List the logs for the specified organization. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): performer_name = parsed_args['performer'] start_time = parsed_args['starttime'] end_time = parsed_args['endtime'] return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name, page_token=page_token, ignore=SERVICE_LEVEL_LOG_KINDS) raise Unauthorized() @resource('/v1/repository//aggregatelogs') @path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryAggregateLogs(RepositoryParamResource): """ Resource for fetching aggregated logs for the specific repository. """ @require_repo_admin @nickname('getAggregateRepoLogs') @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, namespace, repository, parsed_args): """ Returns the aggregated logs for the specified repository. """ repo = model.repository.get_repository(namespace, repository) if not repo: raise NotFound() start_time = parsed_args['starttime'] end_time = parsed_args['endtime'] return get_aggregate_logs(start_time, end_time, repository=repo, ignore=SERVICE_LEVEL_LOG_KINDS) @resource('/v1/user/aggregatelogs') class UserAggregateLogs(ApiResource): """ Resource for fetching aggregated logs for the current user. """ @require_user_admin @nickname('getAggregateUserLogs') @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('performer', 'Username for which to filter logs.', type=str) def get(self, parsed_args): """ Returns the aggregated logs for the current user. """ performer_name = parsed_args['performer'] start_time = parsed_args['starttime'] end_time = parsed_args['endtime'] user = get_authenticated_user() return get_aggregate_logs(start_time, end_time, performer_name=performer_name, namespace=user.username, ignore=SERVICE_LEVEL_LOG_KINDS) @resource('/v1/organization//aggregatelogs') @path_param('orgname', 'The name of the organization') @related_user_resource(UserLogs) class OrgAggregateLogs(ApiResource): """ Resource for fetching aggregate logs for the entire organization. """ @nickname('getAggregateOrgLogs') @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('performer', 'Username for which to filter logs.', type=str) @require_scope(scopes.ORG_ADMIN) def get(self, orgname, parsed_args): """ Gets the aggregated logs for the specified organization. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): performer_name = parsed_args['performer'] start_time = parsed_args['starttime'] end_time = parsed_args['endtime'] return get_aggregate_logs(start_time, end_time, namespace=orgname, performer_name=performer_name, ignore=SERVICE_LEVEL_LOG_KINDS) raise Unauthorized()