|
|
# Copyright (C) 2017-2023 RhodeCode GmbH
|
|
|
#
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
# it under the terms of the GNU Affero General Public License, version 3
|
|
|
# (only), as published by the Free Software Foundation.
|
|
|
#
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
# GNU General Public License for more details.
|
|
|
#
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
#
|
|
|
# This program is dual-licensed. If you wish to learn more about the
|
|
|
# RhodeCode Enterprise Edition, including its added features, Support services,
|
|
|
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
|
|
|
|
|
import logging
|
|
|
import datetime
|
|
|
|
|
|
from rhodecode.lib.jsonalchemy import JsonRaw
|
|
|
from rhodecode.model import meta
|
|
|
from rhodecode.model.db import User, UserLog, Repository
|
|
|
from rhodecode.lib.str_utils import safe_str
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
# action as key, and expected action_data as value
|
|
|
ACTIONS_V1 = {
|
|
|
'user.login.success': {'user_agent': ''},
|
|
|
'user.login.failure': {'user_agent': ''},
|
|
|
'user.logout': {'user_agent': ''},
|
|
|
'user.register': {},
|
|
|
'user.password.reset_request': {},
|
|
|
'user.push': {'user_agent': '', 'commit_ids': []},
|
|
|
'user.pull': {'user_agent': ''},
|
|
|
|
|
|
'user.create': {'data': {}},
|
|
|
'user.delete': {'old_data': {}},
|
|
|
'user.edit': {'old_data': {}},
|
|
|
'user.edit.permissions': {},
|
|
|
'user.edit.ip.add': {'ip': {}, 'user': {}},
|
|
|
'user.edit.ip.delete': {'ip': {}, 'user': {}},
|
|
|
'user.edit.token.add': {'token': {}, 'user': {}},
|
|
|
'user.edit.token.delete': {'token': {}, 'user': {}},
|
|
|
'user.edit.email.add': {'email': ''},
|
|
|
'user.edit.email.delete': {'email': ''},
|
|
|
'user.edit.ssh_key.add': {'token': {}, 'user': {}},
|
|
|
'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
|
|
|
'user.edit.password_reset.enabled': {},
|
|
|
'user.edit.password_reset.disabled': {},
|
|
|
|
|
|
'user_group.create': {'data': {}},
|
|
|
'user_group.delete': {'old_data': {}},
|
|
|
'user_group.edit': {'old_data': {}},
|
|
|
'user_group.edit.permissions': {},
|
|
|
'user_group.edit.member.add': {'user': {}},
|
|
|
'user_group.edit.member.delete': {'user': {}},
|
|
|
|
|
|
'repo.create': {'data': {}},
|
|
|
'repo.fork': {'data': {}},
|
|
|
'repo.edit': {'old_data': {}},
|
|
|
'repo.edit.permissions': {},
|
|
|
'repo.edit.permissions.branch': {},
|
|
|
'repo.archive': {'old_data': {}},
|
|
|
'repo.delete': {'old_data': {}},
|
|
|
|
|
|
'repo.archive.download': {'user_agent': '', 'archive_name': '',
|
|
|
'archive_spec': '', 'archive_cached': ''},
|
|
|
|
|
|
'repo.permissions.branch_rule.create': {},
|
|
|
'repo.permissions.branch_rule.edit': {},
|
|
|
'repo.permissions.branch_rule.delete': {},
|
|
|
|
|
|
'repo.pull_request.create': '',
|
|
|
'repo.pull_request.edit': '',
|
|
|
'repo.pull_request.delete': '',
|
|
|
'repo.pull_request.close': '',
|
|
|
'repo.pull_request.merge': '',
|
|
|
'repo.pull_request.vote': '',
|
|
|
'repo.pull_request.comment.create': '',
|
|
|
'repo.pull_request.comment.edit': '',
|
|
|
'repo.pull_request.comment.delete': '',
|
|
|
|
|
|
'repo.pull_request.reviewer.add': '',
|
|
|
'repo.pull_request.reviewer.delete': '',
|
|
|
|
|
|
'repo.pull_request.observer.add': '',
|
|
|
'repo.pull_request.observer.delete': '',
|
|
|
|
|
|
'repo.commit.strip': {'commit_id': ''},
|
|
|
'repo.commit.comment.create': {'data': {}},
|
|
|
'repo.commit.comment.delete': {'data': {}},
|
|
|
'repo.commit.comment.edit': {'data': {}},
|
|
|
'repo.commit.vote': '',
|
|
|
|
|
|
'repo.artifact.add': '',
|
|
|
'repo.artifact.delete': '',
|
|
|
|
|
|
'repo_group.create': {'data': {}},
|
|
|
'repo_group.edit': {'old_data': {}},
|
|
|
'repo_group.edit.permissions': {},
|
|
|
'repo_group.delete': {'old_data': {}},
|
|
|
}
|
|
|
|
|
|
ACTIONS = ACTIONS_V1
|
|
|
|
|
|
SOURCE_WEB = 'source_web'
|
|
|
SOURCE_API = 'source_api'
|
|
|
|
|
|
|
|
|
class UserWrap(object):
|
|
|
"""
|
|
|
Fake object used to imitate AuthUser
|
|
|
"""
|
|
|
|
|
|
def __init__(self, user_id=None, username=None, ip_addr=None):
|
|
|
self.user_id = user_id
|
|
|
self.username = username
|
|
|
self.ip_addr = ip_addr
|
|
|
|
|
|
|
|
|
class RepoWrap(object):
|
|
|
"""
|
|
|
Fake object used to imitate RepoObject that audit logger requires
|
|
|
"""
|
|
|
|
|
|
def __init__(self, repo_id=None, repo_name=None):
|
|
|
self.repo_id = repo_id
|
|
|
self.repo_name = repo_name
|
|
|
|
|
|
|
|
|
def _store_log(action_name, action_data, user_id, username, user_data,
|
|
|
ip_address, repository_id, repository_name):
|
|
|
user_log = UserLog()
|
|
|
user_log.version = UserLog.VERSION_2
|
|
|
|
|
|
user_log.action = action_name
|
|
|
user_log.action_data = action_data or JsonRaw('{}')
|
|
|
|
|
|
user_log.user_ip = ip_address
|
|
|
|
|
|
user_log.user_id = user_id
|
|
|
user_log.username = username
|
|
|
user_log.user_data = user_data or JsonRaw('{}')
|
|
|
|
|
|
user_log.repository_id = repository_id
|
|
|
user_log.repository_name = repository_name
|
|
|
|
|
|
user_log.action_date = datetime.datetime.now()
|
|
|
|
|
|
return user_log
|
|
|
|
|
|
|
|
|
def store_web(*args, **kwargs):
|
|
|
action_data = {}
|
|
|
org_action_data = kwargs.pop('action_data', {})
|
|
|
action_data.update(org_action_data)
|
|
|
action_data['source'] = SOURCE_WEB
|
|
|
kwargs['action_data'] = action_data
|
|
|
|
|
|
return store(*args, **kwargs)
|
|
|
|
|
|
|
|
|
def store_api(*args, **kwargs):
|
|
|
action_data = {}
|
|
|
org_action_data = kwargs.pop('action_data', {})
|
|
|
action_data.update(org_action_data)
|
|
|
action_data['source'] = SOURCE_API
|
|
|
kwargs['action_data'] = action_data
|
|
|
|
|
|
return store(*args, **kwargs)
|
|
|
|
|
|
|
|
|
def store(action, user, action_data=None, user_data=None, ip_addr=None,
|
|
|
repo=None, sa_session=None, commit=False):
|
|
|
"""
|
|
|
Audit logger for various actions made by users, typically this
|
|
|
results in a call such::
|
|
|
|
|
|
from rhodecode.lib import audit_logger
|
|
|
|
|
|
audit_logger.store(
|
|
|
'repo.edit', user=self._rhodecode_user)
|
|
|
audit_logger.store(
|
|
|
'repo.delete', action_data={'data': repo_data},
|
|
|
user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
|
|
|
|
|
|
# repo action
|
|
|
audit_logger.store(
|
|
|
'repo.delete',
|
|
|
user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
|
|
|
repo=audit_logger.RepoWrap(repo_name='some-repo'))
|
|
|
|
|
|
# repo action, when we know and have the repository object already
|
|
|
audit_logger.store(
|
|
|
'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
|
|
|
user=self._rhodecode_user,
|
|
|
repo=repo_object)
|
|
|
|
|
|
# alternative wrapper to the above
|
|
|
audit_logger.store_web(
|
|
|
'repo.delete', action_data={},
|
|
|
user=self._rhodecode_user,
|
|
|
repo=repo_object)
|
|
|
|
|
|
# without an user ?
|
|
|
audit_logger.store(
|
|
|
'user.login.failure',
|
|
|
user=audit_logger.UserWrap(
|
|
|
username=self.request.params.get('username'),
|
|
|
ip_addr=self.request.remote_addr))
|
|
|
|
|
|
"""
|
|
|
from rhodecode.lib.auth import AuthUser
|
|
|
|
|
|
action_spec = ACTIONS.get(action, None)
|
|
|
if action_spec is None:
|
|
|
raise ValueError(f'Action `{action}` is not supported')
|
|
|
|
|
|
if not sa_session:
|
|
|
sa_session = meta.Session()
|
|
|
|
|
|
try:
|
|
|
username = getattr(user, 'username', None)
|
|
|
if not username:
|
|
|
pass
|
|
|
|
|
|
user_id = getattr(user, 'user_id', None)
|
|
|
if not user_id:
|
|
|
# maybe we have username ? Try to figure user_id from username
|
|
|
if username:
|
|
|
user_id = getattr(
|
|
|
User.get_by_username(username), 'user_id', None)
|
|
|
|
|
|
ip_addr = ip_addr or getattr(user, 'ip_addr', None)
|
|
|
if not ip_addr:
|
|
|
pass
|
|
|
|
|
|
if not user_data:
|
|
|
# try to get this from the auth user
|
|
|
if isinstance(user, AuthUser):
|
|
|
user_data = {
|
|
|
'username': user.username,
|
|
|
'email': user.email,
|
|
|
}
|
|
|
|
|
|
repository_name = getattr(repo, 'repo_name', None)
|
|
|
repository_id = getattr(repo, 'repo_id', None)
|
|
|
if not repository_id:
|
|
|
# maybe we have repo_name ? Try to figure repo_id from repo_name
|
|
|
if repository_name:
|
|
|
repository_id = getattr(
|
|
|
Repository.get_by_repo_name(repository_name), 'repo_id', None)
|
|
|
|
|
|
action_name = safe_str(action)
|
|
|
ip_address = safe_str(ip_addr)
|
|
|
|
|
|
with sa_session.no_autoflush:
|
|
|
|
|
|
user_log = _store_log(
|
|
|
action_name=action_name,
|
|
|
action_data=action_data or {},
|
|
|
user_id=user_id,
|
|
|
username=username,
|
|
|
user_data=user_data or {},
|
|
|
ip_address=ip_address,
|
|
|
repository_id=repository_id,
|
|
|
repository_name=repository_name
|
|
|
)
|
|
|
|
|
|
sa_session.add(user_log)
|
|
|
if commit:
|
|
|
sa_session.commit()
|
|
|
entry_id = user_log.entry_id or ''
|
|
|
|
|
|
update_user_last_activity(sa_session, user_id)
|
|
|
|
|
|
if commit:
|
|
|
sa_session.commit()
|
|
|
|
|
|
log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
|
|
|
entry_id, action_name, user_id, username, ip_address,
|
|
|
extra={"entry_id": entry_id, "action": action_name,
|
|
|
"user_id": user_id, "ip": ip_address})
|
|
|
|
|
|
except Exception:
|
|
|
log.exception('AUDIT: failed to store audit log')
|
|
|
|
|
|
|
|
|
def update_user_last_activity(sa_session, user_id):
|
|
|
_last_activity = datetime.datetime.now()
|
|
|
try:
|
|
|
sa_session.query(User).filter(User.user_id == user_id).update(
|
|
|
{"last_activity": _last_activity})
|
|
|
log.debug(
|
|
|
'updated user `%s` last activity to:%s', user_id, _last_activity)
|
|
|
except Exception:
|
|
|
log.exception("Failed last activity update for user_id: %s", user_id)
|
|
|
sa_session.rollback()
|
|
|
|
|
|
|