Replace password reset with email OTP verification flow. Add auth_otp module, OTP templates, and email delivery. Expand styleguide CSS with full component library. Rework login, register, and admin views for cookie sessions. Remove legacy 2FA templates and password reset flow. Update SSH wrappers, forms, validators, and middleware.
324 lines
13 KiB
Python
324 lines
13 KiB
Python
# Copyright (C) 2011-2024 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
|
|
|
|
|
|
from pyramid.httpexceptions import HTTPFound
|
|
from packaging.version import Version
|
|
|
|
from rhodecode.apps._base import RepoAppView
|
|
from rhodecode.lib import helpers as h
|
|
from rhodecode.lib import audit_logger
|
|
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, HasRepoPermissionAny
|
|
from rhodecode.lib.auth_otp import SudoOTPRequired
|
|
from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
|
|
from rhodecode.lib.type_utils import str2bool
|
|
from rhodecode.lib.utils2 import safe_int
|
|
from rhodecode.lib.vcs import RepositoryError
|
|
from rhodecode.model.db import Session, UserFollowing, User, Repository
|
|
from rhodecode.model.permission import PermissionModel
|
|
from rhodecode.model.quota_model import UserQuotaModel
|
|
from rhodecode.model.repo import RepoModel, ForksAction
|
|
from rhodecode.model.scm import ScmModel
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class RepoSettingsAdvancedView(RepoAppView):
|
|
def load_default_context(self):
|
|
c = self._get_local_tmpl_context()
|
|
return c
|
|
|
|
def _get_users_with_permissions(self):
|
|
user_permissions = {}
|
|
for perm in self.db_repo.permissions():
|
|
user_permissions[perm.user_id] = perm
|
|
|
|
return user_permissions
|
|
|
|
@LoginRequired()
|
|
@HasRepoPermissionAnyDecorator("repository.admin")
|
|
def edit_advanced(self):
|
|
_ = self.request.translate
|
|
c = self.load_default_context()
|
|
c.active = "advanced"
|
|
|
|
is_broken = self.request.params.get("broken")
|
|
if is_broken:
|
|
# Special case: the repository is missing on the filesystem, so its state is unknown.
|
|
# Only allow deletion in this mode.
|
|
c.fork_links = []
|
|
c.pr_link = None
|
|
c.in_public_journal = None
|
|
c.ver_info_dict = {}
|
|
c.hooks_outdated = False
|
|
return self._get_template_context(c)
|
|
|
|
c.fork_links = self._resolve_repo_forks_links()
|
|
c.pr_link = h.route_path("pullrequest_show_all", repo_name=self.db_repo_name)
|
|
|
|
c.default_user_id = User.get_default_user_id()
|
|
c.in_public_journal = (
|
|
UserFollowing.query()
|
|
.filter(UserFollowing.user_id == c.default_user_id)
|
|
.filter(UserFollowing.follows_repository == self.db_repo)
|
|
.scalar()
|
|
)
|
|
|
|
c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
|
|
c.hooks_outdated = False
|
|
|
|
try:
|
|
if Version(c.ver_info_dict["pre_version"]) < Version(c.rhodecode_version):
|
|
c.hooks_outdated = True
|
|
except Exception:
|
|
pass
|
|
|
|
# update commit cache if GET flag is present
|
|
if self.request.GET.get("update_commit_cache"):
|
|
self.db_repo.update_commit_cache()
|
|
h.flash(_("updated commit cache"), category="success")
|
|
|
|
return self._get_template_context(c)
|
|
|
|
def _resolve_repo_forks_links(self):
|
|
def resolve_fork_link(repo):
|
|
for fork in repo.forks:
|
|
yield h.route_path("repo_summary", repo_name=fork.repo_name)
|
|
yield from resolve_fork_link(fork)
|
|
|
|
return list(resolve_fork_link(self.db_repo))
|
|
|
|
@LoginRequired()
|
|
@HasRepoPermissionAnyDecorator("repository.admin")
|
|
@CSRFRequired()
|
|
def edit_advanced_archive(self):
|
|
"""
|
|
Archives the repository. It will become read-only, and not visible in search
|
|
or other queries. But still visible for super-admins.
|
|
"""
|
|
|
|
_ = self.request.translate
|
|
|
|
try:
|
|
old_data = self.db_repo.get_api_data()
|
|
RepoModel().archive(self.db_repo)
|
|
|
|
repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
|
|
audit_logger.store_web(
|
|
"repo.archive", action_data={"old_data": old_data}, user=self._rhodecode_user, repo=repo
|
|
)
|
|
|
|
ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
|
|
h.flash(_("Archived repository `%s`") % self.db_repo_name, category="success")
|
|
Session().commit()
|
|
except Exception:
|
|
log.exception("Exception during archiving of repository")
|
|
h.flash(_("An error occurred during archiving of `%s`") % self.db_repo_name, category="error")
|
|
# redirect to advanced for more deletion options
|
|
raise HTTPFound(h.route_path("edit_repo_advanced", repo_name=self.db_repo_name, _anchor="advanced-archive"))
|
|
|
|
# flush permissions for all users defined in permissions
|
|
affected_user_ids = self._get_users_with_permissions().keys()
|
|
PermissionModel().trigger_permission_flush(affected_user_ids)
|
|
|
|
raise HTTPFound(h.route_path("home"))
|
|
|
|
@LoginRequired()
|
|
@HasRepoPermissionAnyDecorator("repository.admin")
|
|
@CSRFRequired()
|
|
@SudoOTPRequired
|
|
def edit_advanced_delete(self):
|
|
"""
|
|
Deletes the repository, or shows warnings if deletion is not possible
|
|
because of attached forks or other errors.
|
|
"""
|
|
_ = self.request.translate
|
|
|
|
repo_advanced_url = h.route_path("edit_repo_advanced", repo_name=self.db_repo_name, _anchor="advanced-delete")
|
|
try:
|
|
delete_cache = True
|
|
self._invalidate_remote_cache(delete=delete_cache)
|
|
|
|
backup = str2bool(
|
|
self.request.POST.get("backup", "false")
|
|
) # if this option is absent, this means that on FE side checkbox was unchecked
|
|
delete_res = RepoModel().delete(self.db_repo, fs_backup=backup)
|
|
|
|
fork_action = delete_res.fork_action_result.action
|
|
fork_cnt = delete_res.fork_action_result.fork_count
|
|
|
|
if fork_cnt > 0 and fork_action is not None:
|
|
if fork_action is ForksAction.DETACH:
|
|
h.flash(_("Detached %s forks") % fork_cnt, category="success")
|
|
elif fork_action is ForksAction.DELETE:
|
|
h.flash(_("Deleted %s forks") % fork_cnt, category="success")
|
|
|
|
repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
|
|
old_data = self.db_repo.get_api_data()
|
|
audit_logger.store_web(
|
|
"repo.delete", action_data={"old_data": old_data}, user=self._rhodecode_user, repo=repo
|
|
)
|
|
|
|
ScmModel().mark_for_invalidation(self.db_repo_name, delete=delete_cache)
|
|
h.flash(_("Deleted repository `%s`") % self.db_repo_name, category="success")
|
|
Session().commit()
|
|
except AttachedForksError:
|
|
delete_anchor = h.link_to(_("detach or delete"), repo_advanced_url)
|
|
h.flash(
|
|
_(
|
|
"Cannot delete `{repo}` it still contains attached forks. Try using {delete_or_detach} option."
|
|
).format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
|
|
category="warning",
|
|
)
|
|
|
|
# redirect to advanced for forks handle action ?
|
|
raise HTTPFound(repo_advanced_url)
|
|
|
|
except AttachedPullRequestsError:
|
|
attached_prs = len(self.db_repo.pull_requests_source + self.db_repo.pull_requests_target)
|
|
h.flash(
|
|
_(
|
|
"Cannot delete `{repo}` it still contains {num} attached pull requests. "
|
|
"Consider archiving the repository instead."
|
|
).format(repo=self.db_repo_name, num=attached_prs),
|
|
category="warning",
|
|
)
|
|
|
|
# redirect to advanced for forks handle action ?
|
|
raise HTTPFound(repo_advanced_url)
|
|
|
|
except AttachedArtifactsError:
|
|
attached_artifacts = len(self.db_repo.artifacts)
|
|
h.flash(
|
|
_(
|
|
"Cannot delete `{repo}` it still contains {num} attached artifacts. "
|
|
"Consider archiving the repository instead."
|
|
).format(repo=self.db_repo_name, num=attached_artifacts),
|
|
category="warning",
|
|
)
|
|
|
|
# redirect to advanced for forks handle action ?
|
|
raise HTTPFound(repo_advanced_url)
|
|
except Exception as e:
|
|
log.exception(f"Exception during deletion of repository: {str(e)}")
|
|
h.flash(_("An error occurred during deletion of `%s`") % self.db_repo_name, category="error")
|
|
# redirect to advanced for more deletion options
|
|
raise HTTPFound(h.route_path("edit_repo_advanced", repo_name=self.db_repo_name, _anchor="advanced-delete"))
|
|
|
|
raise HTTPFound(h.route_path("home"))
|
|
|
|
def _invalidate_remote_cache(self, delete: bool):
|
|
log.debug(f"Invalidating remote cache, for repository: {self.db_repo_name}")
|
|
scm_repo = self.db_repo.scm_instance(cache=False)
|
|
if scm_repo is not None:
|
|
# if it's None - probably repo is broken and physically absent on the disk
|
|
scm_repo.vcsserver_invalidate_cache(delete=delete)
|
|
|
|
@LoginRequired()
|
|
@HasRepoPermissionAnyDecorator("repository.admin")
|
|
@CSRFRequired()
|
|
def edit_advanced_journal(self):
|
|
"""
|
|
Set's this repository to be visible in public journal,
|
|
in other words making default user to follow this repo
|
|
"""
|
|
_ = self.request.translate
|
|
|
|
try:
|
|
user_id = User.get_default_user_id()
|
|
ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
|
|
h.flash(_("Updated repository visibility in public journal"), category="success")
|
|
Session().commit()
|
|
except Exception:
|
|
h.flash(_("An error occurred during setting this repository in public journal"), category="error")
|
|
|
|
raise HTTPFound(h.route_path("edit_repo_advanced", repo_name=self.db_repo_name))
|
|
|
|
@LoginRequired()
|
|
@HasRepoPermissionAnyDecorator("repository.admin")
|
|
@CSRFRequired()
|
|
def edit_advanced_fork(self):
|
|
"""
|
|
Mark given repository as a fork of another
|
|
"""
|
|
_ = self.request.translate
|
|
|
|
new_fork_id = safe_int(self.request.POST.get("id_fork_of"))
|
|
|
|
# valid repo, re-check permissions
|
|
if new_fork_id:
|
|
repo = Repository.get(new_fork_id)
|
|
# ensure we have at least read access to the repo we mark
|
|
perm_check = HasRepoPermissionAny("repository.read", "repository.write", "repository.admin")
|
|
|
|
if repo and perm_check(repo_name=repo.repo_name):
|
|
new_fork_id = repo.repo_id
|
|
else:
|
|
new_fork_id = None
|
|
|
|
try:
|
|
repo = ScmModel().mark_as_fork(self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
|
|
fork = repo.fork.repo_name if repo.fork else _("Nothing")
|
|
Session().commit()
|
|
h.flash(_("Marked repo %s as fork of %s") % (self.db_repo_name, fork), category="success")
|
|
except RepositoryError as e:
|
|
log.exception("Repository Error occurred")
|
|
h.flash(str(e), category="error")
|
|
except Exception:
|
|
log.exception("Exception while editing fork")
|
|
h.flash(_("An error occurred during this operation"), category="error")
|
|
|
|
raise HTTPFound(h.route_path("edit_repo_advanced", repo_name=self.db_repo_name))
|
|
|
|
@LoginRequired()
|
|
@HasRepoPermissionAnyDecorator("repository.admin")
|
|
@CSRFRequired()
|
|
def edit_advanced_toggle_locking(self):
|
|
"""
|
|
Toggle locking of repository
|
|
"""
|
|
_ = self.request.translate
|
|
set_lock = self.request.POST.get("set_lock")
|
|
set_unlock = self.request.POST.get("set_unlock")
|
|
|
|
try:
|
|
if set_lock:
|
|
Repository.lock(self.db_repo, self._rhodecode_user.user_id, lock_reason=Repository.LOCK_WEB)
|
|
h.flash(_("Locked repository"), category="success")
|
|
elif set_unlock:
|
|
Repository.unlock(self.db_repo)
|
|
h.flash(_("Unlocked repository"), category="success")
|
|
except Exception as e:
|
|
log.exception("Exception during unlocking")
|
|
h.flash(_("An error occurred during unlocking"), category="error")
|
|
|
|
raise HTTPFound(h.route_path("edit_repo_advanced", repo_name=self.db_repo_name))
|
|
|
|
@LoginRequired()
|
|
@HasRepoPermissionAnyDecorator("repository.admin")
|
|
def edit_advanced_install_hooks(self):
|
|
"""
|
|
Install Hooks for repository
|
|
"""
|
|
_ = self.request.translate
|
|
self.load_default_context()
|
|
self.rhodecode_vcs_repo.install_hooks(force=True)
|
|
h.flash(_("installed updated hooks into this repository"), category="success")
|
|
raise HTTPFound(h.route_path("edit_repo_advanced", repo_name=self.db_repo_name))
|