hooks_base.py
465 lines
| 16.6 KiB
| text/x-python
|
PythonLexer
r1 | # -*- coding: utf-8 -*- | |||
r2487 | # Copyright (C) 2013-2018 RhodeCode GmbH | |||
r1 | # | |||
# 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/ | ||||
""" | ||||
Set of hooks run by RhodeCode Enterprise | ||||
""" | ||||
import os | ||||
import collections | ||||
r1455 | import logging | |||
r1 | ||||
import rhodecode | ||||
r375 | from rhodecode import events | |||
r1 | from rhodecode.lib import helpers as h | |||
r1736 | from rhodecode.lib import audit_logger | |||
r1 | from rhodecode.lib.utils2 import safe_str | |||
r2979 | from rhodecode.lib.exceptions import ( | |||
HTTPLockedRC, HTTPBranchProtected, UserCreationError) | ||||
r1 | from rhodecode.model.db import Repository, User | |||
r1455 | log = logging.getLogger(__name__) | |||
r1 | ||||
HookResponse = collections.namedtuple('HookResponse', ('status', 'output')) | ||||
Martin Bornhold
|
r900 | def is_shadow_repo(extras): | ||
""" | ||||
Returns ``True`` if this is an action executed against a shadow repository. | ||||
""" | ||||
return extras['is_shadow_repo'] | ||||
r1 | def _get_scm_size(alias, root_path): | |||
if not alias.startswith('.'): | ||||
alias += '.' | ||||
size_scm, size_root = 0, 0 | ||||
for path, unused_dirs, files in os.walk(safe_str(root_path)): | ||||
if path.find(alias) != -1: | ||||
for f in files: | ||||
try: | ||||
size_scm += os.path.getsize(os.path.join(path, f)) | ||||
except OSError: | ||||
pass | ||||
else: | ||||
for f in files: | ||||
try: | ||||
size_root += os.path.getsize(os.path.join(path, f)) | ||||
except OSError: | ||||
pass | ||||
size_scm_f = h.format_byte_size_binary(size_scm) | ||||
size_root_f = h.format_byte_size_binary(size_root) | ||||
size_total_f = h.format_byte_size_binary(size_root + size_scm) | ||||
return size_scm_f, size_root_f, size_total_f | ||||
# actual hooks called by Mercurial internally, and GIT by our Python Hooks | ||||
def repo_size(extras): | ||||
"""Present size of repository after push.""" | ||||
repo = Repository.get_by_repo_name(extras.repository) | ||||
vcs_part = safe_str(u'.%s' % repo.repo_type) | ||||
size_vcs, size_root, size_total = _get_scm_size(vcs_part, | ||||
repo.repo_full_path) | ||||
msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n' | ||||
% (repo.repo_name, vcs_part, size_vcs, size_root, size_total)) | ||||
return HookResponse(0, msg) | ||||
def pre_push(extras): | ||||
""" | ||||
Hook executed before pushing code. | ||||
It bans pushing when the repository is locked. | ||||
""" | ||||
r1456 | ||||
r2979 | user = User.get_by_username(extras.username) | |||
r1 | output = '' | |||
r2979 | if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]): | |||
r1 | locked_by = User.get(extras.locked_by[0]).username | |||
reason = extras.locked_by[2] | ||||
# this exception is interpreted in git/hg middlewares and based | ||||
# on that proper return code is server to client | ||||
_http_ret = HTTPLockedRC( | ||||
_locked_by_explanation(extras.repository, locked_by, reason)) | ||||
if str(_http_ret.code).startswith('2'): | ||||
# 2xx Codes don't raise exceptions | ||||
output = _http_ret.title | ||||
else: | ||||
raise _http_ret | ||||
Martin Bornhold
|
r900 | if not is_shadow_repo(extras): | ||
r2979 | if extras.commit_ids and extras.check_branch_perms: | |||
auth_user = user.AuthUser() | ||||
repo = Repository.get_by_repo_name(extras.repository) | ||||
affected_branches = [] | ||||
if repo.repo_type == 'hg': | ||||
for entry in extras.commit_ids: | ||||
if entry['type'] == 'branch': | ||||
is_forced = bool(entry['multiple_heads']) | ||||
affected_branches.append([entry['name'], is_forced]) | ||||
elif repo.repo_type == 'git': | ||||
for entry in extras.commit_ids: | ||||
if entry['type'] == 'heads': | ||||
is_forced = bool(entry['pruned_sha']) | ||||
affected_branches.append([entry['name'], is_forced]) | ||||
for branch_name, is_forced in affected_branches: | ||||
rule, branch_perm = auth_user.get_rule_and_branch_permission( | ||||
extras.repository, branch_name) | ||||
if not branch_perm: | ||||
# no branch permission found for this branch, just keep checking | ||||
continue | ||||
if branch_perm == 'branch.push_force': | ||||
continue | ||||
elif branch_perm == 'branch.push' and is_forced is False: | ||||
continue | ||||
elif branch_perm == 'branch.push' and is_forced is True: | ||||
halt_message = 'Branch `{}` changes rejected by rule {}. ' \ | ||||
'FORCE PUSH FORBIDDEN.'.format(branch_name, rule) | ||||
else: | ||||
halt_message = 'Branch `{}` changes rejected by rule {}.'.format( | ||||
branch_name, rule) | ||||
if halt_message: | ||||
_http_ret = HTTPBranchProtected(halt_message) | ||||
raise _http_ret | ||||
# Propagate to external components. This is done after checking the | ||||
# lock, for consistent behavior. | ||||
Martin Bornhold
|
r900 | pre_push_extension(repo_store_path=Repository.base_path(), **extras) | ||
events.trigger(events.RepoPrePushEvent( | ||||
repo_name=extras.repository, extras=extras)) | ||||
r375 | ||||
r1 | return HookResponse(0, output) | |||
def pre_pull(extras): | ||||
""" | ||||
Hook executed before pulling the code. | ||||
It bans pulling when the repository is locked. | ||||
""" | ||||
output = '' | ||||
if extras.locked_by[0]: | ||||
locked_by = User.get(extras.locked_by[0]).username | ||||
reason = extras.locked_by[2] | ||||
# this exception is interpreted in git/hg middlewares and based | ||||
# on that proper return code is server to client | ||||
_http_ret = HTTPLockedRC( | ||||
_locked_by_explanation(extras.repository, locked_by, reason)) | ||||
if str(_http_ret.code).startswith('2'): | ||||
# 2xx Codes don't raise exceptions | ||||
output = _http_ret.title | ||||
else: | ||||
raise _http_ret | ||||
Martin Bornhold
|
r900 | # Propagate to external components. This is done after checking the | ||
# lock, for consistent behavior. | ||||
if not is_shadow_repo(extras): | ||||
pre_pull_extension(**extras) | ||||
events.trigger(events.RepoPrePullEvent( | ||||
repo_name=extras.repository, extras=extras)) | ||||
r1 | ||||
return HookResponse(0, output) | ||||
def post_pull(extras): | ||||
"""Hook executed after client pulls the code.""" | ||||
r1736 | audit_user = audit_logger.UserWrap( | |||
username=extras.username, | ||||
ip_addr=extras.ip) | ||||
r1737 | repo = audit_logger.RepoWrap(repo_name=extras.repository) | |||
r1736 | audit_logger.store( | |||
r1829 | 'user.pull', action_data={ | |||
r1736 | 'user_agent': extras.user_agent}, | |||
r1737 | user=audit_user, repo=repo, commit=True) | |||
r1736 | ||||
Martin Bornhold
|
r900 | # Propagate to external components. | ||
if not is_shadow_repo(extras): | ||||
post_pull_extension(**extras) | ||||
events.trigger(events.RepoPullEvent( | ||||
repo_name=extras.repository, extras=extras)) | ||||
r1 | ||||
output = '' | ||||
# make lock is a tri state False, True, None. We only make lock on True | ||||
Martin Bornhold
|
r900 | if extras.make_lock is True and not is_shadow_repo(extras): | ||
r1754 | user = User.get_by_username(extras.username) | |||
r1 | Repository.lock(Repository.get_by_repo_name(extras.repository), | |||
user.user_id, | ||||
lock_reason=Repository.LOCK_PULL) | ||||
msg = 'Made lock on repo `%s`' % (extras.repository,) | ||||
output += msg | ||||
if extras.locked_by[0]: | ||||
locked_by = User.get(extras.locked_by[0]).username | ||||
reason = extras.locked_by[2] | ||||
_http_ret = HTTPLockedRC( | ||||
_locked_by_explanation(extras.repository, locked_by, reason)) | ||||
if str(_http_ret.code).startswith('2'): | ||||
# 2xx Codes don't raise exceptions | ||||
output += _http_ret.title | ||||
return HookResponse(0, output) | ||||
def post_push(extras): | ||||
"""Hook executed after user pushes to the repository.""" | ||||
r1754 | commit_ids = extras.commit_ids | |||
r1 | ||||
r1754 | # log the push call | |||
r1736 | audit_user = audit_logger.UserWrap( | |||
r1754 | username=extras.username, ip_addr=extras.ip) | |||
r1736 | repo = audit_logger.RepoWrap(repo_name=extras.repository) | |||
audit_logger.store( | ||||
r1829 | 'user.push', action_data={ | |||
r1736 | 'user_agent': extras.user_agent, | |||
r1964 | 'commit_ids': commit_ids[:400]}, | |||
r1736 | user=audit_user, repo=repo, commit=True) | |||
Martin Bornhold
|
r900 | # Propagate to external components. | ||
if not is_shadow_repo(extras): | ||||
post_push_extension( | ||||
repo_store_path=Repository.base_path(), | ||||
pushed_revs=commit_ids, | ||||
**extras) | ||||
events.trigger(events.RepoPushEvent( | ||||
repo_name=extras.repository, | ||||
pushed_commit_ids=commit_ids, | ||||
extras=extras)) | ||||
r1 | ||||
output = '' | ||||
# make lock is a tri state False, True, None. We only release lock on False | ||||
Martin Bornhold
|
r900 | if extras.make_lock is False and not is_shadow_repo(extras): | ||
r1 | Repository.unlock(Repository.get_by_repo_name(extras.repository)) | |||
msg = 'Released lock on repo `%s`\n' % extras.repository | ||||
output += msg | ||||
if extras.locked_by[0]: | ||||
locked_by = User.get(extras.locked_by[0]).username | ||||
reason = extras.locked_by[2] | ||||
_http_ret = HTTPLockedRC( | ||||
_locked_by_explanation(extras.repository, locked_by, reason)) | ||||
# TODO: johbo: if not? | ||||
if str(_http_ret.code).startswith('2'): | ||||
# 2xx Codes don't raise exceptions | ||||
output += _http_ret.title | ||||
r1755 | if extras.new_refs: | |||
tmpl = \ | ||||
extras.server_url + '/' + \ | ||||
extras.repository + \ | ||||
"/pull-request/new?{ref_type}={ref_name}" | ||||
for branch_name in extras.new_refs['branches']: | ||||
output += 'RhodeCode: open pull request link: {}\n'.format( | ||||
tmpl.format(ref_type='branch', ref_name=branch_name)) | ||||
for book_name in extras.new_refs['bookmarks']: | ||||
output += 'RhodeCode: open pull request link: {}\n'.format( | ||||
tmpl.format(ref_type='bookmark', ref_name=book_name)) | ||||
r1 | output += 'RhodeCode: push completed\n' | |||
return HookResponse(0, output) | ||||
def _locked_by_explanation(repo_name, user_name, reason): | ||||
message = ( | ||||
'Repository `%s` locked by user `%s`. Reason:`%s`' | ||||
% (repo_name, user_name, reason)) | ||||
return message | ||||
def check_allowed_create_user(user_dict, created_by, **kwargs): | ||||
# pre create hooks | ||||
if pre_create_user.is_active(): | ||||
allowed, reason = pre_create_user(created_by=created_by, **user_dict) | ||||
if not allowed: | ||||
raise UserCreationError(reason) | ||||
class ExtensionCallback(object): | ||||
""" | ||||
Forwards a given call to rcextensions, sanitizes keyword arguments. | ||||
Does check if there is an extension active for that hook. If it is | ||||
there, it will forward all `kwargs_keys` keyword arguments to the | ||||
extension callback. | ||||
""" | ||||
def __init__(self, hook_name, kwargs_keys): | ||||
self._hook_name = hook_name | ||||
self._kwargs_keys = set(kwargs_keys) | ||||
def __call__(self, *args, **kwargs): | ||||
r1455 | log.debug('Calling extension callback for %s', self._hook_name) | |||
r1 | kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys) | |||
r1482 | # backward compat for removed api_key for old hooks. THis was it works | |||
# with older rcextensions that require api_key present | ||||
if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']: | ||||
kwargs_to_pass['api_key'] = '_DEPRECATED_' | ||||
r1 | callback = self._get_callback() | |||
if callback: | ||||
return callback(**kwargs_to_pass) | ||||
r1455 | else: | |||
log.debug('extensions callback not found skipping...') | ||||
r1 | ||||
def is_active(self): | ||||
return hasattr(rhodecode.EXTENSIONS, self._hook_name) | ||||
def _get_callback(self): | ||||
return getattr(rhodecode.EXTENSIONS, self._hook_name, None) | ||||
pre_pull_extension = ExtensionCallback( | ||||
hook_name='PRE_PULL_HOOK', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
'repository')) | ||||
post_pull_extension = ExtensionCallback( | ||||
hook_name='PULL_HOOK', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
'repository')) | ||||
pre_push_extension = ExtensionCallback( | ||||
hook_name='PRE_PUSH_HOOK', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
r1456 | 'repository', 'repo_store_path', 'commit_ids')) | |||
r1 | ||||
post_push_extension = ExtensionCallback( | ||||
hook_name='PUSH_HOOK', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
'repository', 'repo_store_path', 'pushed_revs')) | ||||
pre_create_user = ExtensionCallback( | ||||
hook_name='PRE_CREATE_USER_HOOK', | ||||
kwargs_keys=( | ||||
'username', 'password', 'email', 'firstname', 'lastname', 'active', | ||||
'admin', 'created_by')) | ||||
log_create_pull_request = ExtensionCallback( | ||||
hook_name='CREATE_PULL_REQUEST', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
'repository', 'pull_request_id', 'url', 'title', 'description', | ||||
'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', | ||||
'mergeable', 'source', 'target', 'author', 'reviewers')) | ||||
log_merge_pull_request = ExtensionCallback( | ||||
hook_name='MERGE_PULL_REQUEST', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
'repository', 'pull_request_id', 'url', 'title', 'description', | ||||
'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', | ||||
'mergeable', 'source', 'target', 'author', 'reviewers')) | ||||
log_close_pull_request = ExtensionCallback( | ||||
hook_name='CLOSE_PULL_REQUEST', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
'repository', 'pull_request_id', 'url', 'title', 'description', | ||||
'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', | ||||
'mergeable', 'source', 'target', 'author', 'reviewers')) | ||||
log_review_pull_request = ExtensionCallback( | ||||
hook_name='REVIEW_PULL_REQUEST', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
'repository', 'pull_request_id', 'url', 'title', 'description', | ||||
'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', | ||||
'mergeable', 'source', 'target', 'author', 'reviewers')) | ||||
log_update_pull_request = ExtensionCallback( | ||||
hook_name='UPDATE_PULL_REQUEST', | ||||
kwargs_keys=( | ||||
'server_url', 'config', 'scm', 'username', 'ip', 'action', | ||||
'repository', 'pull_request_id', 'url', 'title', 'description', | ||||
'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', | ||||
'mergeable', 'source', 'target', 'author', 'reviewers')) | ||||
log_create_user = ExtensionCallback( | ||||
hook_name='CREATE_USER_HOOK', | ||||
kwargs_keys=( | ||||
'username', 'full_name_or_username', 'full_contact', 'user_id', | ||||
'name', 'firstname', 'short_contact', 'admin', 'lastname', | ||||
'ip_addresses', 'extern_type', 'extern_name', | ||||
r1482 | 'email', 'api_keys', 'last_login', | |||
r1 | 'full_name', 'active', 'password', 'emails', | |||
'inherit_default_permissions', 'created_by', 'created_on')) | ||||
log_delete_user = ExtensionCallback( | ||||
hook_name='DELETE_USER_HOOK', | ||||
kwargs_keys=( | ||||
'username', 'full_name_or_username', 'full_contact', 'user_id', | ||||
'name', 'firstname', 'short_contact', 'admin', 'lastname', | ||||
'ip_addresses', | ||||
r1482 | 'email', 'last_login', | |||
r1 | 'full_name', 'active', 'password', 'emails', | |||
'inherit_default_permissions', 'deleted_by')) | ||||
log_create_repository = ExtensionCallback( | ||||
hook_name='CREATE_REPO_HOOK', | ||||
kwargs_keys=( | ||||
'repo_name', 'repo_type', 'description', 'private', 'created_on', | ||||
'enable_downloads', 'repo_id', 'user_id', 'enable_statistics', | ||||
'clone_uri', 'fork_id', 'group_id', 'created_by')) | ||||
log_delete_repository = ExtensionCallback( | ||||
hook_name='DELETE_REPO_HOOK', | ||||
kwargs_keys=( | ||||
'repo_name', 'repo_type', 'description', 'private', 'created_on', | ||||
'enable_downloads', 'repo_id', 'user_id', 'enable_statistics', | ||||
'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on')) | ||||
log_create_repository_group = ExtensionCallback( | ||||
hook_name='CREATE_REPO_GROUP_HOOK', | ||||
kwargs_keys=( | ||||
'group_name', 'group_parent_id', 'group_description', | ||||
'group_id', 'user_id', 'created_by', 'created_on', | ||||
'enable_locking')) | ||||