Merge branch 'main' into feature/setting-to-pre-populate-branch-in-pr
This commit is contained in:
commit
c0727ac73d
33 changed files with 816 additions and 357 deletions
37
docs/release-notes/release-notes-5.7.0.rst
Normal file
37
docs/release-notes/release-notes-5.7.0.rst
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
|RCE| 5.7.0 |RNS|
|
||||
-----------------
|
||||
|
||||
Release Date
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- 2025-08-15
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
- squash: The pull request UI now includes a dropdown menu, allowing users to select "Squash" or "Close" when merging pull requests in Git and Mercurial repositories, instead of only having the "Merge" option. This feature is exclusive to the Enterprise Edition (EE) and must be enabled by an administrator.
|
||||
- pull request: Introduced a new checkbox in the pull request UI, allowing users to select the option to close the branch after merging. This feature is exclusive to the Enterprise Edition (EE).
|
||||
- user: It is now possible to remove users from the system. When a user is deleted, all of their assets will automatically be reassigned to a new system user, ‘Ghost User’.
|
||||
- active directory: Added the ability to sync active and inactive users from LDAP/Active Directory. This feature must be enabled in the LDAP authentication plugin by an administrator, is available only for Active Directory, and is exclusive to the Enterprise Edition (EE).
|
||||
|
||||
General
|
||||
^^^^^^^
|
||||
|
||||
|
||||
Security
|
||||
^^^^^^^^
|
||||
|
||||
|
||||
|
||||
Performance
|
||||
^^^^^^^^^^^
|
||||
|
||||
|
||||
|
||||
Fixes
|
||||
^^^^^
|
||||
|
||||
- ssh: Fixed an issue where the app.service_api.token field could be left empty in the configuration. The application will now automatically populate this field if it is missing.
|
||||
- timezone: Fixed an issue in the age function where comparing a timezone-aware and a naive datetime object could cause an exception.
|
||||
|
||||
Upgrade notes
|
||||
^^^^^^^^^^^^^
|
||||
|
|
@ -19,6 +19,8 @@
|
|||
import mock
|
||||
import pytest
|
||||
|
||||
from rhodecode.model.db import Repository
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.api.tests.utils import build_data, api_call, assert_error, assert_ok, crash
|
||||
|
||||
|
|
@ -27,6 +29,9 @@ from rhodecode.api.tests.utils import build_data, api_call, assert_error, assert
|
|||
class TestApiDeleteRepo(object):
|
||||
def test_api_delete_repo(self, backend):
|
||||
repo = backend.create_repo()
|
||||
repo.private = True
|
||||
Session().commit()
|
||||
|
||||
repo_name = repo.repo_name
|
||||
id_, params = build_data(
|
||||
self.apikey,
|
||||
|
|
@ -38,6 +43,46 @@ class TestApiDeleteRepo(object):
|
|||
expected = {"msg": "Deleted repository `%s`" % (repo_name,), "success": True}
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fork_action, expected_fork_msg",
|
||||
[
|
||||
("detach", "Detached 1 forks"),
|
||||
("delete", "Deleted 1 forks"),
|
||||
],
|
||||
)
|
||||
def test_api_delete_repo_with_forks(self, backend, fork_action, expected_fork_msg):
|
||||
repo = backend.create_repo()
|
||||
repo.private = True
|
||||
fork = backend.create_fork()
|
||||
fork_name = fork.repo_name
|
||||
Session().commit()
|
||||
|
||||
assert fork.parent is not None
|
||||
|
||||
repo_name = repo.repo_name
|
||||
id_, params = build_data(
|
||||
self.apikey,
|
||||
"delete_repo",
|
||||
repoid=repo.repo_name,
|
||||
forks=fork_action,
|
||||
)
|
||||
response = api_call(self.app, params)
|
||||
|
||||
expected = {
|
||||
"msg": "Deleted repository `%s` %s; NOTE: 'forks' option will be deprecated."
|
||||
% (repo_name, expected_fork_msg),
|
||||
"success": True,
|
||||
}
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
self._validate_fork_db_state(fork_action, fork_name)
|
||||
|
||||
def _validate_fork_db_state(self, fork_action, fork_name):
|
||||
fork = Repository.get_by_repo_name(repo_name=fork_name)
|
||||
if fork_action == "detach":
|
||||
assert fork.parent is None
|
||||
elif fork_action == "delete":
|
||||
assert fork is None
|
||||
|
||||
def test_api_delete_repo_by_non_admin(self, backend, user_regular):
|
||||
repo = backend.create_repo(cur_user=user_regular.username)
|
||||
repo_name = repo.repo_name
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ def assert_ok(id_, expected, given):
|
|||
|
||||
expected = jsonify({"id": id_, "error": None, "result": expected})
|
||||
|
||||
assert expected == given
|
||||
assert expected == given, "Expected: %s, Given: %s" % (expected, given)
|
||||
|
||||
|
||||
def assert_error(id_, expected, given):
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ from rhodecode.model.comment import CommentsModel
|
|||
from rhodecode.model.db import Session, ChangesetStatus, RepositoryField, Repository, RepoGroup, ChangesetComment
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
from rhodecode.model.pull_request import PullRequestModel
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.model.repo import RepoModel, ForksAction
|
||||
from rhodecode.model.scm import ScmModel, RepoList
|
||||
from rhodecode.model.settings import SettingsModel, VcsSettingsModel
|
||||
from rhodecode.model import validation_schema
|
||||
|
|
@ -1360,17 +1360,18 @@ def delete_repo(request, apiuser, repoid, forks=Optional("")):
|
|||
validate_repo_permissions(apiuser, repoid, repo, _perms)
|
||||
|
||||
try:
|
||||
handle_forks = Optional.extract(forks)
|
||||
str_fork_action = Optional.extract(forks)
|
||||
_forks_msg = ""
|
||||
_forks = [f for f in repo.forks]
|
||||
if handle_forks == "detach":
|
||||
if str_fork_action == "detach":
|
||||
_forks_msg = " " + f"Detached {len(_forks)} forks"
|
||||
elif handle_forks == "delete":
|
||||
elif str_fork_action == "delete":
|
||||
_forks_msg = " " + f"Deleted {len(_forks)} forks"
|
||||
elif _forks:
|
||||
raise JSONRPCError(f"Cannot delete `{repo.repo_name}` it still contains attached forks")
|
||||
|
||||
old_data = repo.get_api_data()
|
||||
RepoModel().delete(repo, forks=forks)
|
||||
RepoModel().delete(repo, forks_action=ForksAction(str_fork_action))
|
||||
|
||||
repo = audit_logger.RepoWrap(repo_id=None, repo_name=repo.repo_name)
|
||||
|
||||
|
|
@ -1378,7 +1379,12 @@ def delete_repo(request, apiuser, repoid, forks=Optional("")):
|
|||
|
||||
ScmModel().mark_for_invalidation(repo_name, delete=True)
|
||||
Session().commit()
|
||||
return {"msg": f"Deleted repository `{repo_name}`{_forks_msg}", "success": True}
|
||||
res_message = {"msg": f"Deleted repository `{repo_name}`{_forks_msg}", "success": True}
|
||||
if str_fork_action:
|
||||
deprecation_warning = "; NOTE: 'forks' option will be deprecated."
|
||||
res_message["msg"] += deprecation_warning
|
||||
|
||||
return res_message
|
||||
except Exception:
|
||||
log.exception("Exception occurred while trying to delete repo")
|
||||
raise JSONRPCError(f"failed to delete repository `{repo_name}`")
|
||||
|
|
|
|||
|
|
@ -587,8 +587,14 @@ class AdminSettingsView(BaseAppView):
|
|||
c.active = "search"
|
||||
|
||||
c.searcher = searcher_from_config(self.request.registry.settings)
|
||||
c.statistics = c.searcher.statistics(self.request.translate)
|
||||
c.error = None
|
||||
c.statistics = None
|
||||
|
||||
try:
|
||||
c.statistics = c.searcher.statistics(self.request.translate)
|
||||
except Exception as e:
|
||||
log.exception("Exception during search statistics retrieval")
|
||||
c.error = e
|
||||
return self._get_template_context(c)
|
||||
|
||||
@LoginRequired()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ from unittest.mock import patch, MagicMock
|
|||
from rhodecode.apps.repository.views.repo_settings_advanced import RepoSettingsAdvancedView
|
||||
from rhodecode.lib.str_utils import safe_str
|
||||
from rhodecode.model.db import Repository
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.repo import RepoModel, ForksAction
|
||||
from rhodecode.tests import HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator
|
||||
from rhodecode.tests.fixtures.rc_fixture import Fixture
|
||||
from rhodecode.tests.utils import repo_on_filesystem
|
||||
|
|
@ -61,7 +62,7 @@ class TestAdminRepoSettingsAdvanced(object):
|
|||
|
||||
response.mustcontain(opt)
|
||||
|
||||
fixture.destroy_repo(target_repo, forks="detach")
|
||||
fixture.destroy_repo(target_repo, forks_action=ForksAction.DETACH)
|
||||
|
||||
@pytest.mark.backends("hg", "git")
|
||||
def test_set_fork_of_other_type_repo(self, autologin_user, backend, csrf_token):
|
||||
|
|
@ -97,7 +98,10 @@ class TestAdminRepoSettingsAdvanced(object):
|
|||
|
||||
@pytest.mark.parametrize("suffix", ["", "ąęł", "123"], ids=no_newline_id_generator)
|
||||
def test_advanced_repo_delete(self, autologin_user, backend, suffix, csrf_token):
|
||||
repo = backend.create_repo(name_suffix=suffix)
|
||||
repo = backend.create_repo(name_suffix=suffix, private_repo=True)
|
||||
repo.private = True
|
||||
Session().commit()
|
||||
|
||||
repo_name = repo.repo_name
|
||||
repo_name_str = safe_str(repo.repo_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ 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.repo import RepoModel
|
||||
from rhodecode.model.repo import RepoModel, ForksAction
|
||||
from rhodecode.model.scm import ScmModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -56,6 +56,9 @@ class RepoSettingsAdvancedView(RepoAppView):
|
|||
c = self.load_default_context()
|
||||
c.active = "advanced"
|
||||
|
||||
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()
|
||||
|
|
@ -80,6 +83,14 @@ class RepoSettingsAdvancedView(RepoAppView):
|
|||
|
||||
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()
|
||||
|
|
@ -124,11 +135,6 @@ class RepoSettingsAdvancedView(RepoAppView):
|
|||
because of attached forks or other errors.
|
||||
"""
|
||||
_ = self.request.translate
|
||||
handle_forks = self.request.POST.get("forks", None)
|
||||
if handle_forks == "detach_forks":
|
||||
handle_forks = "detach"
|
||||
elif handle_forks == "delete_forks":
|
||||
handle_forks = "delete"
|
||||
|
||||
repo_advanced_url = h.route_path("edit_repo_advanced", repo_name=self.db_repo_name, _anchor="advanced-delete")
|
||||
try:
|
||||
|
|
@ -137,14 +143,16 @@ class RepoSettingsAdvancedView(RepoAppView):
|
|||
delete_cache = True
|
||||
self._invalidate_remote_cache(delete=delete_cache)
|
||||
|
||||
RepoModel().delete(self.db_repo, forks=handle_forks)
|
||||
delete_res = RepoModel().delete(self.db_repo)
|
||||
|
||||
_forks = self.db_repo.forks.count()
|
||||
if _forks and handle_forks:
|
||||
if handle_forks == "detach_forks":
|
||||
h.flash(_("Detached %s forks") % _forks, category="success")
|
||||
elif handle_forks == "delete_forks":
|
||||
h.flash(_("Deleted %s forks") % _forks, category="success")
|
||||
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)
|
||||
audit_logger.store_web(
|
||||
|
|
|
|||
|
|
@ -21,28 +21,22 @@ RhodeCode authentication plugin for LDAP
|
|||
"""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import colander
|
||||
|
||||
from rhodecode.authentication.plugins.services.ldap_dao import LdapDao
|
||||
from rhodecode.translation import _
|
||||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property
|
||||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin, hybrid_property
|
||||
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
|
||||
from rhodecode.authentication.routes import AuthnPluginResourceBase
|
||||
from rhodecode.lib.colander_utils import strip_whitespace
|
||||
from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
|
||||
from rhodecode.lib.exceptions import LdapUsernameError, LdapPasswordError, LdapImportError
|
||||
from rhodecode.lib.str_utils import safe_str
|
||||
from rhodecode.model.db import User
|
||||
from rhodecode.model.validators import Missing
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import ldap
|
||||
except ImportError:
|
||||
# means that python-ldap is not installed, we use Missing object to mark
|
||||
# ldap lib is Missing
|
||||
ldap = Missing
|
||||
|
||||
|
||||
class LdapError(Exception):
|
||||
pass
|
||||
|
|
@ -61,188 +55,6 @@ class LdapAuthnResource(AuthnPluginResourceBase):
|
|||
pass
|
||||
|
||||
|
||||
class AuthLdap(AuthLdapBase):
|
||||
default_tls_cert_dir = "/etc/openldap/cacerts"
|
||||
|
||||
scope_labels = {
|
||||
ldap.SCOPE_BASE: "SCOPE_BASE",
|
||||
ldap.SCOPE_ONELEVEL: "SCOPE_ONELEVEL",
|
||||
ldap.SCOPE_SUBTREE: "SCOPE_SUBTREE",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server,
|
||||
base_dn,
|
||||
port=389,
|
||||
bind_dn="",
|
||||
bind_pass="",
|
||||
tls_kind="PLAIN",
|
||||
tls_reqcert="DEMAND",
|
||||
tls_cert_file=None,
|
||||
tls_cert_dir=None,
|
||||
ldap_version=3,
|
||||
search_scope="SUBTREE",
|
||||
attr_login="uid",
|
||||
ldap_filter="",
|
||||
timeout=None,
|
||||
):
|
||||
if ldap == Missing:
|
||||
raise LdapImportError("Missing or incompatible ldap library")
|
||||
|
||||
self.debug = False
|
||||
self.timeout = timeout or 60 * 5
|
||||
self.ldap_version = ldap_version
|
||||
self.ldap_server_type = "ldap"
|
||||
|
||||
self.TLS_KIND = tls_kind
|
||||
|
||||
if self.TLS_KIND == "LDAPS":
|
||||
port = port or 636
|
||||
self.ldap_server_type += "s"
|
||||
|
||||
OPT_X_TLS_DEMAND = 2
|
||||
self.TLS_REQCERT = getattr(ldap, "OPT_X_TLS_%s" % tls_reqcert, OPT_X_TLS_DEMAND)
|
||||
self.TLS_CERT_FILE = tls_cert_file or ""
|
||||
self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
|
||||
|
||||
# split server into list
|
||||
self.SERVER_ADDRESSES = self._get_server_list(server)
|
||||
self.LDAP_SERVER_PORT = port
|
||||
|
||||
# USE FOR READ ONLY BIND TO LDAP SERVER
|
||||
self.attr_login = attr_login
|
||||
|
||||
self.LDAP_BIND_DN = safe_str(bind_dn)
|
||||
self.LDAP_BIND_PASS = safe_str(bind_pass)
|
||||
|
||||
self.SEARCH_SCOPE = getattr(ldap, "SCOPE_%s" % search_scope)
|
||||
self.BASE_DN = safe_str(base_dn)
|
||||
self.LDAP_FILTER = safe_str(ldap_filter)
|
||||
|
||||
def _get_ldap_conn(self):
|
||||
if self.debug:
|
||||
ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
|
||||
|
||||
if self.TLS_CERT_FILE and hasattr(ldap, "OPT_X_TLS_CACERTFILE"):
|
||||
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
|
||||
|
||||
elif hasattr(ldap, "OPT_X_TLS_CACERTDIR"):
|
||||
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
|
||||
|
||||
if self.TLS_KIND != "PLAIN":
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
|
||||
|
||||
ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
|
||||
ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
|
||||
|
||||
# init connection now
|
||||
ldap_servers = self._build_servers(self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
|
||||
log.debug("initializing LDAP connection to:%s", ldap_servers)
|
||||
ldap_conn = ldap.initialize(ldap_servers)
|
||||
ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
|
||||
ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
|
||||
ldap_conn.timeout = self.timeout
|
||||
|
||||
if self.ldap_version == 2:
|
||||
ldap_conn.protocol = ldap.VERSION2
|
||||
else:
|
||||
ldap_conn.protocol = ldap.VERSION3
|
||||
|
||||
if self.TLS_KIND == "START_TLS":
|
||||
ldap_conn.start_tls_s()
|
||||
|
||||
if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
|
||||
log.debug("Trying simple_bind with password and given login DN: %r", self.LDAP_BIND_DN)
|
||||
ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
|
||||
log.debug("simple_bind successful")
|
||||
return ldap_conn
|
||||
|
||||
def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
|
||||
scope = ldap.SCOPE_BASE
|
||||
scope_label = self.scope_labels.get(scope)
|
||||
ldap_filter = "(objectClass=*)"
|
||||
|
||||
try:
|
||||
log.debug(
|
||||
"Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)", dn, scope_label, ldap_filter
|
||||
)
|
||||
ldap_conn.simple_bind_s(dn, safe_str(password))
|
||||
response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=["*", "+"])
|
||||
|
||||
if not response:
|
||||
log.error("search bind returned empty results: %r", response)
|
||||
return {}
|
||||
else:
|
||||
_dn, attrs = response[0]
|
||||
return attrs
|
||||
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
log.debug("LDAP rejected password for user '%s': %s, org_exc:", username, dn, exc_info=True)
|
||||
|
||||
def authenticate_ldap(self, username, password):
|
||||
"""
|
||||
Authenticate a user via LDAP and return his/her LDAP properties.
|
||||
|
||||
Raises AuthenticationError if the credentials are rejected, or
|
||||
EnvironmentError if the LDAP server can't be reached.
|
||||
|
||||
:param username: username
|
||||
:param password: password
|
||||
"""
|
||||
|
||||
uid = self.get_uid(username, self.SERVER_ADDRESSES)
|
||||
user_attrs = {}
|
||||
dn = ""
|
||||
|
||||
self.validate_password(username, password)
|
||||
self.validate_username(username)
|
||||
scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
|
||||
|
||||
ldap_conn = None
|
||||
try:
|
||||
ldap_conn = self._get_ldap_conn()
|
||||
filter_ = "(&{}({}={}))".format(self.LDAP_FILTER, self.attr_login, username)
|
||||
log.debug("Authenticating %r filter %s and scope: %s", self.BASE_DN, filter_, scope_label)
|
||||
|
||||
ldap_objects = ldap_conn.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=["*", "+"])
|
||||
|
||||
if not ldap_objects:
|
||||
log.debug("No matching LDAP objects for authentication of UID:'%s' username:(%s)", uid, username)
|
||||
raise ldap.NO_SUCH_OBJECT()
|
||||
|
||||
log.debug("Found %s matching ldap object[s], trying to authenticate on each one now...", len(ldap_objects))
|
||||
for dn, _attrs in ldap_objects:
|
||||
if dn is None:
|
||||
continue
|
||||
|
||||
user_attrs = self.fetch_attrs_from_simple_bind(ldap_conn, dn, username, password)
|
||||
|
||||
if user_attrs:
|
||||
log.debug("Got authenticated user attributes from DN:%s", dn)
|
||||
break
|
||||
else:
|
||||
raise LdapPasswordError(f"Failed to authenticate user `{username}` with given password")
|
||||
|
||||
except ldap.NO_SUCH_OBJECT:
|
||||
log.debug("LDAP says no such user '%s' (%s), org_exc:", uid, username, exc_info=True)
|
||||
raise LdapUsernameError("Unable to find user")
|
||||
except ldap.SERVER_DOWN:
|
||||
org_exc = traceback.format_exc()
|
||||
raise LdapConnectionError("LDAP can't access authentication server, org_exc:%s" % org_exc)
|
||||
finally:
|
||||
if ldap_conn:
|
||||
log.debug("ldap: connection release")
|
||||
try:
|
||||
ldap_conn.unbind_s()
|
||||
except Exception:
|
||||
# for any reason this can raise exception we must catch it
|
||||
# to not crush the server
|
||||
pass
|
||||
|
||||
return dn, user_attrs
|
||||
|
||||
|
||||
class LdapSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
|
||||
tls_kind_choices = ["PLAIN", "LDAPS", "START_TLS"]
|
||||
tls_reqcert_choices = ["NEVER", "ALLOW", "TRY", "DEMAND", "HARD"]
|
||||
|
|
@ -338,7 +150,7 @@ class LdapSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSet
|
|||
)
|
||||
tls_cert_dir = colander.SchemaNode(
|
||||
colander.String(),
|
||||
default=AuthLdap.default_tls_cert_dir,
|
||||
default=LdapDao.default_tls_cert_dir,
|
||||
description=_(
|
||||
"This specifies the path of a directory that contains individual CA certificates in separate files."
|
||||
),
|
||||
|
|
@ -420,6 +232,38 @@ class LdapSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSet
|
|||
title=_("Last Name Attribute"),
|
||||
widget="string",
|
||||
)
|
||||
sync_active_directory_users = colander.SchemaNode(
|
||||
colander.Bool(),
|
||||
default=False,
|
||||
description=_(
|
||||
"A cron job that periodically retrieves all users from an LDAP-based Active Directory server and syncs "
|
||||
"them with the RhodeCode database.\n"
|
||||
"Note: This feature is specific to Active Directory. Enabling it for other types of LDAP servers will have no effect."
|
||||
),
|
||||
missing=False,
|
||||
preparer=strip_whitespace,
|
||||
title=_("Sync Active Directory Users"),
|
||||
widget="bool",
|
||||
)
|
||||
|
||||
|
||||
def get_ldap_args(settings: dict):
|
||||
return {
|
||||
"server": settings.get("host", ""),
|
||||
"base_dn": settings.get("base_dn", ""),
|
||||
"port": settings.get("port"),
|
||||
"bind_dn": settings.get("dn_user"),
|
||||
"bind_pass": settings.get("dn_pass"),
|
||||
"tls_kind": settings.get("tls_kind"),
|
||||
"tls_reqcert": settings.get("tls_reqcert"),
|
||||
"tls_cert_file": settings.get("tls_cert_file"),
|
||||
"tls_cert_dir": settings.get("tls_cert_dir"),
|
||||
"search_scope": settings.get("search_scope"),
|
||||
"attr_login": settings.get("attr_login"),
|
||||
"ldap_version": 3,
|
||||
"ldap_filter": settings.get("filter"),
|
||||
"timeout": settings.get("timeout"),
|
||||
}
|
||||
|
||||
|
||||
class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
||||
|
|
@ -509,30 +353,15 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
log.debug("Empty username or password skipping...")
|
||||
return None
|
||||
|
||||
ldap_args = {
|
||||
"server": settings.get("host", ""),
|
||||
"base_dn": settings.get("base_dn", ""),
|
||||
"port": settings.get("port"),
|
||||
"bind_dn": settings.get("dn_user"),
|
||||
"bind_pass": settings.get("dn_pass"),
|
||||
"tls_kind": settings.get("tls_kind"),
|
||||
"tls_reqcert": settings.get("tls_reqcert"),
|
||||
"tls_cert_file": settings.get("tls_cert_file"),
|
||||
"tls_cert_dir": settings.get("tls_cert_dir"),
|
||||
"search_scope": settings.get("search_scope"),
|
||||
"attr_login": settings.get("attr_login"),
|
||||
"ldap_version": 3,
|
||||
"ldap_filter": settings.get("filter"),
|
||||
"timeout": settings.get("timeout"),
|
||||
}
|
||||
ldap_args = get_ldap_args(settings)
|
||||
|
||||
ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
|
||||
|
||||
log.debug("Checking for ldap authentication.")
|
||||
|
||||
try:
|
||||
auth_ldap = AuthLdap(**ldap_args)
|
||||
(user_dn, ldap_attrs) = auth_ldap.authenticate_ldap(username, password)
|
||||
ldap_dao = LdapDao(**ldap_args)
|
||||
(user_dn, ldap_attrs) = ldap_dao.authenticate_ldap(username, password)
|
||||
log.debug("Got ldap DN response %s", user_dn)
|
||||
|
||||
def get_ldap_attr(k) -> str:
|
||||
|
|
|
|||
0
rhodecode/authentication/plugins/services/__init__.py
Normal file
0
rhodecode/authentication/plugins/services/__init__.py
Normal file
219
rhodecode/authentication/plugins/services/ldap_dao.py
Normal file
219
rhodecode/authentication/plugins/services/ldap_dao.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import logging
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from rhodecode.model.validators import Missing
|
||||
from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
|
||||
from rhodecode.authentication.base import AuthLdapBase
|
||||
from rhodecode.lib.str_utils import safe_str
|
||||
|
||||
try:
|
||||
import ldap
|
||||
except ImportError:
|
||||
# means that python-ldap is not installed, we use Missing object to mark
|
||||
# ldap lib is Missing
|
||||
ldap = Missing
|
||||
|
||||
|
||||
class LdapDao(AuthLdapBase):
|
||||
default_tls_cert_dir = "/etc/openldap/cacerts"
|
||||
|
||||
scope_labels = {
|
||||
ldap.SCOPE_BASE: "SCOPE_BASE",
|
||||
ldap.SCOPE_ONELEVEL: "SCOPE_ONELEVEL",
|
||||
ldap.SCOPE_SUBTREE: "SCOPE_SUBTREE",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server,
|
||||
base_dn,
|
||||
port=389,
|
||||
bind_dn="",
|
||||
bind_pass="",
|
||||
tls_kind="PLAIN",
|
||||
tls_reqcert="DEMAND",
|
||||
tls_cert_file=None,
|
||||
tls_cert_dir=None,
|
||||
ldap_version=3,
|
||||
search_scope="SUBTREE",
|
||||
attr_login="uid",
|
||||
ldap_filter="",
|
||||
timeout=None,
|
||||
):
|
||||
if ldap == Missing:
|
||||
raise LdapImportError("Missing or incompatible ldap library")
|
||||
|
||||
self.log = logging.getLogger(LdapDao.__name__)
|
||||
|
||||
self.debug = False
|
||||
self.timeout = timeout or 60 * 5
|
||||
self.ldap_version = ldap_version
|
||||
self.ldap_server_type = "ldap"
|
||||
|
||||
self.TLS_KIND = tls_kind
|
||||
|
||||
if self.TLS_KIND == "LDAPS":
|
||||
port = port or 636
|
||||
self.ldap_server_type += "s"
|
||||
|
||||
OPT_X_TLS_DEMAND = 2
|
||||
self.TLS_REQCERT = getattr(ldap, "OPT_X_TLS_%s" % tls_reqcert, OPT_X_TLS_DEMAND)
|
||||
self.TLS_CERT_FILE = tls_cert_file or ""
|
||||
self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
|
||||
|
||||
# split server into list
|
||||
self.SERVER_ADDRESSES = self._get_server_list(server)
|
||||
self.LDAP_SERVER_PORT = port
|
||||
|
||||
# USE FOR READ ONLY BIND TO LDAP SERVER
|
||||
self.attr_login = attr_login
|
||||
|
||||
self.LDAP_BIND_DN = safe_str(bind_dn)
|
||||
self.LDAP_BIND_PASS = safe_str(bind_pass)
|
||||
|
||||
self.SEARCH_SCOPE = getattr(ldap, "SCOPE_%s" % search_scope)
|
||||
self.BASE_DN = safe_str(base_dn)
|
||||
self.LDAP_FILTER = safe_str(ldap_filter)
|
||||
|
||||
def _get_ldap_conn(self):
|
||||
if self.debug:
|
||||
ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
|
||||
|
||||
if self.TLS_CERT_FILE and hasattr(ldap, "OPT_X_TLS_CACERTFILE"):
|
||||
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
|
||||
|
||||
elif hasattr(ldap, "OPT_X_TLS_CACERTDIR"):
|
||||
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
|
||||
|
||||
if self.TLS_KIND != "PLAIN":
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
|
||||
|
||||
ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
|
||||
ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
|
||||
|
||||
# init connection now
|
||||
ldap_servers = self._build_servers(self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
|
||||
self.log.debug("initializing LDAP connection to:%s", ldap_servers)
|
||||
ldap_conn = ldap.initialize(ldap_servers)
|
||||
ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
|
||||
ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
|
||||
ldap_conn.timeout = self.timeout
|
||||
|
||||
if self.ldap_version == 2:
|
||||
ldap_conn.protocol = ldap.VERSION2
|
||||
else:
|
||||
ldap_conn.protocol = ldap.VERSION3
|
||||
|
||||
if self.TLS_KIND == "START_TLS":
|
||||
ldap_conn.start_tls_s()
|
||||
|
||||
if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
|
||||
self.log.debug("Trying simple_bind with password and given login DN: %r", self.LDAP_BIND_DN)
|
||||
ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
|
||||
self.log.debug("simple_bind successful")
|
||||
return ldap_conn
|
||||
|
||||
def fetch_all(self, ldap_filter: str = "(objectClass=*)", attributes: List[str] = None) -> Optional[List[dict]]:
|
||||
ldap_conn = None
|
||||
try:
|
||||
if attributes is None:
|
||||
attributes = ["*", "+"]
|
||||
|
||||
ldap_conn = self._get_ldap_conn()
|
||||
self.log.debug("fetching users for DN: %s", self.BASE_DN)
|
||||
ldap_objects = ldap_conn.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE, ldap_filter, attrlist=attributes)
|
||||
return [attrs for _, attrs in ldap_objects]
|
||||
except Exception as e:
|
||||
self.log.error("Error fetching users for DN: %s. Error: %s", self.BASE_DN, str(e))
|
||||
return None
|
||||
finally:
|
||||
self._releease_connection(ldap_conn)
|
||||
|
||||
def _releease_connection(self, ldap_conn):
|
||||
if ldap_conn:
|
||||
self.log.debug("ldap: connection release")
|
||||
try:
|
||||
ldap_conn.unbind_s()
|
||||
except Exception as e:
|
||||
# for any reason this can raise exception we must catch it
|
||||
# to not crush the server
|
||||
self.log.warning("unbind_s failed, error: %s", str(e))
|
||||
|
||||
def _fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
|
||||
scope = ldap.SCOPE_BASE
|
||||
scope_label = self.scope_labels.get(scope)
|
||||
ldap_filter = "(objectClass=*)"
|
||||
|
||||
try:
|
||||
self.log.debug(
|
||||
"Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)", dn, scope_label, ldap_filter
|
||||
)
|
||||
ldap_conn.simple_bind_s(dn, safe_str(password))
|
||||
response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=["*", "+"])
|
||||
|
||||
if not response:
|
||||
self.log.error("search bind returned empty results: %r", response)
|
||||
return {}
|
||||
else:
|
||||
_dn, attrs = response[0]
|
||||
return attrs
|
||||
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
self.log.debug("LDAP rejected password for user '%s': %s, org_exc:", username, dn, exc_info=True)
|
||||
|
||||
def authenticate_ldap(self, username, password):
|
||||
"""
|
||||
Authenticate a user via LDAP and return his/her LDAP properties.
|
||||
|
||||
Raises AuthenticationError if the credentials are rejected, or
|
||||
EnvironmentError if the LDAP server can't be reached.
|
||||
|
||||
:param username: username
|
||||
:param password: password
|
||||
"""
|
||||
|
||||
uid = self.get_uid(username, self.SERVER_ADDRESSES)
|
||||
|
||||
self.validate_password(username, password)
|
||||
self.validate_username(username)
|
||||
scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
|
||||
|
||||
ldap_conn = None
|
||||
try:
|
||||
ldap_conn = self._get_ldap_conn()
|
||||
filter_ = "(&{}({}={}))".format(self.LDAP_FILTER, self.attr_login, username)
|
||||
self.log.debug("Authenticating %r filter %s and scope: %s", self.BASE_DN, filter_, scope_label)
|
||||
|
||||
ldap_objects = ldap_conn.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=["*", "+"])
|
||||
|
||||
if not ldap_objects:
|
||||
self.log.debug("No matching LDAP objects for authentication of UID:'%s' username:(%s)", uid, username)
|
||||
raise ldap.NO_SUCH_OBJECT()
|
||||
|
||||
self.log.debug(
|
||||
"Found %s matching ldap object[s], trying to authenticate on each one now...", len(ldap_objects)
|
||||
)
|
||||
for dn, _attrs in ldap_objects:
|
||||
if dn is None:
|
||||
continue
|
||||
|
||||
user_attrs = self._fetch_attrs_from_simple_bind(ldap_conn, dn, username, password)
|
||||
|
||||
if user_attrs:
|
||||
self.log.debug("Got authenticated user attributes from DN:%s", dn)
|
||||
break
|
||||
else:
|
||||
raise LdapPasswordError(f"Failed to authenticate user `{username}` with given password")
|
||||
|
||||
except ldap.NO_SUCH_OBJECT:
|
||||
self.log.debug("LDAP says no such user '%s' (%s), org_exc:", uid, username, exc_info=True)
|
||||
raise LdapUsernameError("Unable to find user")
|
||||
except ldap.SERVER_DOWN:
|
||||
org_exc = traceback.format_exc()
|
||||
raise LdapConnectionError("LDAP can't access authentication server, org_exc:%s" % org_exc)
|
||||
finally:
|
||||
self._releease_connection(ldap_conn)
|
||||
|
||||
return dn, user_attrs
|
||||
0
rhodecode/authentication/tests/services/__init__.py
Normal file
0
rhodecode/authentication/tests/services/__init__.py
Normal file
52
rhodecode/authentication/tests/services/test_ldap_dao.py
Normal file
52
rhodecode/authentication/tests/services/test_ldap_dao.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import ldap
|
||||
|
||||
from rhodecode.authentication.plugins.services.ldap_dao import LdapDao
|
||||
from rhodecode.lib.diff_match_patch import patch_obj
|
||||
|
||||
|
||||
@patch("rhodecode.authentication.plugins.services.ldap_dao.ldap")
|
||||
class TestLdapDao(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._server_list = "test_srv1,test_srv2,test_srv3"
|
||||
self._base_dn = "test_dn"
|
||||
self.ldap_dao = LdapDao(
|
||||
server=self._server_list,
|
||||
base_dn=self._base_dn,
|
||||
)
|
||||
|
||||
@patch.object(LdapDao, "_get_ldap_conn")
|
||||
def test_fetch_all_from_ldap_server(self, _get_ldap_conn_mock, ldap_mock):
|
||||
conn = MagicMock()
|
||||
no_objects_in_ldap_server = []
|
||||
conn.search_ext_s.return_value = no_objects_in_ldap_server
|
||||
|
||||
_get_ldap_conn_mock.return_value = conn
|
||||
|
||||
returned_value = self.ldap_dao.fetch_all()
|
||||
|
||||
conn.search_ext_s.assert_called_once_with(self._base_dn, 2, "(objectClass=*)", attrlist=["*", "+"])
|
||||
|
||||
assert returned_value == no_objects_in_ldap_server
|
||||
|
||||
@patch.object(LdapDao, "_get_ldap_conn")
|
||||
def test_fetch_all_release_connection(self, _get_ldap_conn_mock, ldap_mock):
|
||||
conn = MagicMock()
|
||||
_get_ldap_conn_mock.return_value = conn
|
||||
|
||||
self.ldap_dao.fetch_all()
|
||||
|
||||
conn.unbind_s.assert_called_once()
|
||||
|
||||
@patch.object(LdapDao, "_get_ldap_conn")
|
||||
def test_fetch_all_release_connection_on_error(self, _get_ldap_conn_mock, ldap_mock):
|
||||
conn = MagicMock()
|
||||
_get_ldap_conn_mock.return_value = conn
|
||||
conn.search_ext_s.side_effect = Exception("Test exception")
|
||||
|
||||
res = self.ldap_dao.fetch_all()
|
||||
|
||||
assert res is None
|
||||
conn.unbind_s.assert_called_once()
|
||||
63
rhodecode/authentication/tests/test_auth_plugin_view.py
Normal file
63
rhodecode/authentication/tests/test_auth_plugin_view.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from rhodecode.authentication.views import AuthnPluginViewBase
|
||||
from rhodecode.lib.celerylib import tasks
|
||||
|
||||
|
||||
class TestAuthPluginView:
|
||||
@pytest.mark.parametrize(
|
||||
"sync_active_directory_users, expected_celery_task",
|
||||
[
|
||||
(
|
||||
True,
|
||||
tasks.schedule_sync_ldap_ad_users_producer,
|
||||
),
|
||||
(
|
||||
False,
|
||||
tasks.unschedule_sync_ldap_ad_users_producer,
|
||||
),
|
||||
],
|
||||
)
|
||||
@patch("rhodecode.apps._base.BaseAppView.__init__", return_value=None)
|
||||
@patch("rhodecode.authentication.views.h")
|
||||
@patch("rhodecode.authentication.views.SettingsModel")
|
||||
@patch.object(AuthnPluginViewBase, "load_default_context")
|
||||
@patch("rhodecode.authentication.views.run_task")
|
||||
def test_enable_ad_sync_schedule(
|
||||
self,
|
||||
run_task_mock,
|
||||
load_default_context,
|
||||
settings_model,
|
||||
h,
|
||||
_init_,
|
||||
sync_active_directory_users,
|
||||
expected_celery_task,
|
||||
):
|
||||
view = self._get_instance()
|
||||
|
||||
settings_post = AuthnPluginViewBase.__dict__[ # ignore decorators
|
||||
"settings_post"
|
||||
].__wrapped__.__wrapped__.__wrapped__
|
||||
|
||||
schema = MagicMock()
|
||||
schema.deserialize.return_value = {"sync_active_directory_users": sync_active_directory_users}
|
||||
|
||||
self._plugin.get_settings_schema.return_value = schema
|
||||
|
||||
settings_post(view)
|
||||
|
||||
run_task_mock.assert_called_once_with(expected_celery_task)
|
||||
|
||||
def _get_instance(self):
|
||||
# since base __init__ was patched, there is a need to inject mocks manually
|
||||
self._context = MagicMock()
|
||||
self._request = MagicMock()
|
||||
self._plugin = MagicMock()
|
||||
|
||||
view = AuthnPluginViewBase(self._context, self._request)
|
||||
view.request = self._request
|
||||
view.plugin = self._plugin
|
||||
view.context = self._context
|
||||
return view
|
||||
|
|
@ -28,6 +28,7 @@ from rhodecode.apps._base import BaseAppView
|
|||
from rhodecode.authentication.base import get_authn_registry
|
||||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, CSRFRequired
|
||||
from rhodecode.lib.celerylib import run_task, tasks
|
||||
from rhodecode.model.forms import AuthSettingsForm
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
|
@ -90,6 +91,9 @@ class AuthnPluginViewBase(BaseAppView):
|
|||
# Store validated data.
|
||||
for name, value in valid_data.items():
|
||||
self.plugin.create_or_update_setting(name, value)
|
||||
if name == "sync_active_directory_users":
|
||||
self._handle_ad_users_sync_schedule(name, value)
|
||||
|
||||
Session().commit()
|
||||
SettingsModel().invalidate_settings_cache()
|
||||
|
||||
|
|
@ -102,6 +106,17 @@ class AuthnPluginViewBase(BaseAppView):
|
|||
|
||||
return HTTPFound(redirect_to)
|
||||
|
||||
def _handle_ad_users_sync_schedule(self, name: str, enabled: bool):
|
||||
if name != "sync_active_directory_users":
|
||||
return
|
||||
|
||||
if enabled:
|
||||
log.debug("Scheduling AD users sync")
|
||||
run_task(tasks.schedule_sync_ldap_ad_users_producer)
|
||||
else:
|
||||
log.debug("Removing schedule for AD users sync")
|
||||
run_task(tasks.unschedule_sync_ldap_ad_users_producer)
|
||||
|
||||
|
||||
class AuthSettingsView(BaseAppView):
|
||||
def load_default_context(self):
|
||||
|
|
|
|||
|
|
@ -22,36 +22,12 @@ import logging
|
|||
|
||||
from pyramid.settings import asbool
|
||||
|
||||
from rhodecode.config.settings_maker import SettingsMaker
|
||||
from rhodecode.config.settings_maker import SettingsMaker, generate_token
|
||||
from rhodecode.config import utils as config_utils
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def initialize_ini_config_default_values_if_not_present(ini_path: str):
|
||||
from configupdater import ConfigUpdater # use configupdater to not break comments and formatting's
|
||||
|
||||
def dump_config():
|
||||
with open(ini_path, "w") as configfile:
|
||||
updater.write(configfile)
|
||||
|
||||
updater = ConfigUpdater()
|
||||
updater.read(ini_path)
|
||||
|
||||
section = "app:main"
|
||||
option = "app.service_api.token"
|
||||
|
||||
if not updater[section][option].value.strip():
|
||||
updater[section][option] = generate_token()
|
||||
dump_config()
|
||||
|
||||
|
||||
def generate_token(length: int = 32) -> str:
|
||||
import secrets
|
||||
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
|
||||
def sanitize_settings_and_apply_defaults(global_config, settings):
|
||||
"""
|
||||
Applies settings defaults and does all type conversion.
|
||||
|
|
@ -180,8 +156,8 @@ def sanitize_settings_and_apply_defaults(global_config, settings):
|
|||
settings_maker.make_setting(
|
||||
"exception_tracker.store_path",
|
||||
default=jn(default_cache_dir, "exc_store"),
|
||||
default_when_empty=True,
|
||||
parser="dir:ensured",
|
||||
default_when_empty=True,
|
||||
)
|
||||
|
||||
settings_maker.make_setting(
|
||||
|
|
@ -279,11 +255,11 @@ def sanitize_settings_and_apply_defaults(global_config, settings):
|
|||
settings_maker.make_setting("archive_cache.objectstore.retry_backoff", 1, parser="int")
|
||||
settings_maker.make_setting("archive_cache.objectstore.retry_attempts", 10, parser="int")
|
||||
|
||||
settings_maker.make_setting("app.service_api.token", generate_token(), parser="string", write_default_to_ini=True)
|
||||
|
||||
settings_maker.env_expand()
|
||||
|
||||
# configure instance id
|
||||
config_utils.set_instance_id(settings)
|
||||
|
||||
initialize_ini_config_default_values_if_not_present(global_config.get("__file__"))
|
||||
|
||||
return settings
|
||||
|
|
|
|||
|
|
@ -33,6 +33,33 @@ log = logging.getLogger(__name__)
|
|||
set_keys = {"__file__": ""}
|
||||
|
||||
|
||||
def initialize_ini_config_default_values_if_not_present(
|
||||
ini_path: str, option: str, default_val: object, section: str = "app:main"
|
||||
):
|
||||
from configupdater import ConfigUpdater # use configupdater to not break comments and formatting's
|
||||
|
||||
def dump_config():
|
||||
with open(ini_path, "w") as configfile:
|
||||
updater.write(configfile)
|
||||
|
||||
if not ini_path or not os.path.exists(ini_path):
|
||||
log.warning("Config file %s not found.", ini_path)
|
||||
return
|
||||
|
||||
updater = ConfigUpdater()
|
||||
updater.read(ini_path)
|
||||
|
||||
if option not in updater[section] or not updater[section][option].value.strip():
|
||||
updater[section][option] = default_val
|
||||
dump_config()
|
||||
|
||||
|
||||
def generate_token(length: int = 32) -> str:
|
||||
import secrets
|
||||
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
|
||||
class SettingsMaker:
|
||||
def __init__(self, app_settings):
|
||||
self.settings = app_settings
|
||||
|
|
@ -154,9 +181,16 @@ class SettingsMaker:
|
|||
logging.config.fileConfig(f.name)
|
||||
os.remove(f.name)
|
||||
|
||||
def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
|
||||
def make_setting(
|
||||
self, key, default, lower=False, default_when_empty=False, parser=None, write_default_to_ini=False
|
||||
):
|
||||
input_val = self.settings.get(key, default)
|
||||
|
||||
if write_default_to_ini:
|
||||
initialize_ini_config_default_values_if_not_present(
|
||||
ini_path=self.settings.get("__file__"), option=key, default_val=default
|
||||
)
|
||||
|
||||
if default_when_empty and not input_val:
|
||||
# use default value when value is set in the config but it is empty
|
||||
input_val = default
|
||||
|
|
|
|||
|
|
@ -2583,10 +2583,10 @@ msgstr ""
|
|||
msgid "Confirm to delete this repository"
|
||||
msgstr ""
|
||||
|
||||
#: rhodecode/templates/admin/repos/repo_edit.html:310
|
||||
#: rhodecode/templates/admin/repos/repo_edit.html:208
|
||||
#, python-format
|
||||
msgid "this repository has %s fork"
|
||||
msgid_plural "this repository has %s forks"
|
||||
msgid "Following fork will be %s"
|
||||
msgid_plural "Following forks will be %s"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
|
|
|
|||
|
|
@ -7567,7 +7567,7 @@ msgstr ""
|
|||
msgid "Delete this repository"
|
||||
msgstr ""
|
||||
|
||||
#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:242
|
||||
#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:249
|
||||
msgid "This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ by celery daemon
|
|||
import os
|
||||
import time
|
||||
|
||||
from celery import current_app
|
||||
from pyramid_mailer.mailer import Mailer
|
||||
from pyramid_mailer.message import Message
|
||||
from email.utils import formatdate
|
||||
|
|
@ -35,7 +36,7 @@ from rhodecode.lib import hooks_base
|
|||
from rhodecode.lib.utils import adopt_for_celery
|
||||
from rhodecode.lib.utils2 import safe_int, str2bool, aslist
|
||||
from rhodecode.lib.statsd_client import StatsdClient
|
||||
from rhodecode.model.db import true, null, Session, IntegrityError, Repository, RepoGroup, User
|
||||
from rhodecode.model.db import true, null, Session, IntegrityError, Repository, RepoGroup, User, ScheduleEntry
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
|
||||
|
||||
|
|
@ -417,6 +418,47 @@ def beat_check(*args, **kwargs):
|
|||
return time.time()
|
||||
|
||||
|
||||
@async_task(ignore_result=True)
|
||||
def schedule_sync_ldap_ad_users_producer():
|
||||
log = get_logger(schedule_sync_ldap_ad_users_producer)
|
||||
|
||||
try:
|
||||
from rc_ee.lib.celerylib.scheduler import RcScheduler
|
||||
except ImportError:
|
||||
log.error("Attempt to schedule EE feature")
|
||||
return
|
||||
|
||||
scheduler = RcScheduler(app=current_app)
|
||||
scheduler.sync()
|
||||
task_name = "rc_ee.lib.celerylib.tasks.sync_ldap_ad_users_producer"
|
||||
if task_name not in scheduler.schedule:
|
||||
entries = {
|
||||
task_name: {
|
||||
"task": task_name,
|
||||
"schedule_type": "crontab",
|
||||
"schedule_value": {"hour": 0, "minute": 4},
|
||||
"options": {"expires": 12 * 3600},
|
||||
}
|
||||
}
|
||||
scheduler.update_from_dict(entries)
|
||||
|
||||
|
||||
@async_task(ignore_result=True)
|
||||
def unschedule_sync_ldap_ad_users_producer():
|
||||
log = get_logger(unschedule_sync_ldap_ad_users_producer)
|
||||
|
||||
try:
|
||||
from rc_ee.lib.celerylib.scheduler import RcScheduler
|
||||
except ImportError:
|
||||
log.error("Attempt to unschedule EE feature")
|
||||
return
|
||||
|
||||
task_name = "rc_ee.lib.celerylib.tasks.sync_ldap_ad_users_producer"
|
||||
existing_task = ScheduleEntry.query().filter(ScheduleEntry.task_dot_notation == task_name).first()
|
||||
if existing_task:
|
||||
Session().delete(existing_task)
|
||||
|
||||
|
||||
@async_task
|
||||
@adopt_for_celery
|
||||
def repo_size(extras):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
import time
|
||||
import logging
|
||||
|
||||
from rhodecode.config.config_maker import initialize_ini_config_default_values_if_not_present
|
||||
from rhodecode.config.settings_maker import initialize_ini_config_default_values_if_not_present, generate_token
|
||||
from rhodecode.lib.config_utils import get_app_config_lightweight
|
||||
|
||||
from rhodecode.lib.hook_daemon.base import Hooks
|
||||
|
|
@ -34,7 +34,9 @@ def prepare_callback_daemon(extras, protocol: str, txn_id=None):
|
|||
hooks_config = {}
|
||||
match protocol:
|
||||
case "celery":
|
||||
initialize_ini_config_default_values_if_not_present(extras["config"])
|
||||
initialize_ini_config_default_values_if_not_present(
|
||||
ini_path=extras["config"], option="app.service_api.token", default_val=generate_token()
|
||||
)
|
||||
config = get_app_config_lightweight(extras["config"])
|
||||
|
||||
broker_url = config.get("celery.broker_url")
|
||||
|
|
|
|||
|
|
@ -21,19 +21,20 @@ import os
|
|||
from pyramid.paster import bootstrap as pyramid_bootstrap, setup_logging # pragma: no cover
|
||||
from pyramid.threadlocal import get_current_request as pyramid_current_request
|
||||
|
||||
from rhodecode.config.config_maker import initialize_ini_config_default_values_if_not_present
|
||||
|
||||
|
||||
def bootstrap(config_uri, options=None, env=None):
|
||||
from rhodecode.config.utils import DEFAULT_USER
|
||||
from rhodecode.lib.config_utils import get_app_config_lightweight
|
||||
from rhodecode.lib.utils2 import AttributeDict
|
||||
from rhodecode.lib.request import Request
|
||||
from rhodecode.config.settings_maker import initialize_ini_config_default_values_if_not_present, generate_token
|
||||
|
||||
if env:
|
||||
os.environ.update(env)
|
||||
|
||||
initialize_ini_config_default_values_if_not_present(config_uri)
|
||||
initialize_ini_config_default_values_if_not_present(
|
||||
ini_path=config_uri, option="app.service_api.token", default_val=generate_token()
|
||||
)
|
||||
config = get_app_config_lightweight(config_uri)
|
||||
base_url = config["app.base_url"]
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ from rhodecode.model import meta
|
|||
from rhodecode.model.db import Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup
|
||||
from rhodecode.model.meta import Session
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
REMOVED_REPO_PAT = re.compile(r"rm__\d{8}_\d{6}_\d{6}__.*")
|
||||
|
|
@ -665,7 +664,7 @@ def repo2db_mapper(initial_repo_list, force_hooks_rebuild=False):
|
|||
|
||||
|
||||
def repo2db_cleanup(skip_repos=None, skip_groups=None):
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.model.repo import RepoModel, ForksAction
|
||||
from rhodecode.model.repo_group import RepoGroupModel
|
||||
|
||||
sa = meta.Session()
|
||||
|
|
@ -688,7 +687,7 @@ def repo2db_cleanup(skip_repos=None, skip_groups=None):
|
|||
if not instance:
|
||||
log.debug("Removing non-existing repository found in db `%s`", db_repo_name)
|
||||
try:
|
||||
RepoModel(sa).delete(db_repo, forks="detach", fs_remove=False, call_events=False)
|
||||
RepoModel(sa).delete(db_repo, forks_action=ForksAction.DETACH, fs_remove=False, call_events=False)
|
||||
sa.commit()
|
||||
removed.append(db_repo_name)
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -330,11 +330,24 @@ def age(prevdate, now=None, show_short_version=False, show_suffix=True, short_fo
|
|||
def get_year(prevdate):
|
||||
return prevdate.year
|
||||
|
||||
def _normalize_datetime_pair(prevdate, now):
|
||||
"""
|
||||
Ensure both datetime objects are the same kind (either both naive or both timezone-aware).
|
||||
If not, comparing them will raise a TypeError due to mismatched timezone awareness.
|
||||
"""
|
||||
if prevdate.tzinfo or now.tzinfo:
|
||||
utc = datetime.timezone.utc
|
||||
return prevdate.replace(tzinfo=utc), now.replace(tzinfo=utc)
|
||||
|
||||
return prevdate, now # no timezone
|
||||
|
||||
now = now or datetime.datetime.now()
|
||||
order = ["year", "month", "day", "hour", "minute", "second"]
|
||||
deltas = {}
|
||||
future = False
|
||||
|
||||
prevdate, now = _normalize_datetime_pair(prevdate, now)
|
||||
|
||||
if prevdate > now:
|
||||
now_old = now
|
||||
now = prevdate
|
||||
|
|
|
|||
|
|
@ -2411,6 +2411,17 @@ class MergeCheck(object):
|
|||
return merge_check
|
||||
|
||||
log.debug("MergeCheck: is failed: %s", merge_check.failed)
|
||||
|
||||
if merge_check.failed:
|
||||
close_branch = PullRequestModel()._close_branch_before_merging(pull_request)
|
||||
if close_branch:
|
||||
close_msg = cls._get_branch_close_or_delete_message(_, pull_request)
|
||||
merge_check.error_details["extra_info"] = {
|
||||
"details": "detailed_info",
|
||||
"message": close_msg,
|
||||
"error_type": "extra_info",
|
||||
}
|
||||
|
||||
return merge_check
|
||||
|
||||
@classmethod
|
||||
|
|
@ -2428,17 +2439,29 @@ class MergeCheck(object):
|
|||
|
||||
close_branch = model._close_branch_before_merging(pull_request)
|
||||
if close_branch:
|
||||
repo_type = pull_request.target_repo.repo_type
|
||||
close_msg = ""
|
||||
if repo_type == "hg":
|
||||
close_msg = _("Source branch will be closed before the merge.")
|
||||
elif repo_type == "git":
|
||||
close_msg = _("Source branch will be deleted after the merge.")
|
||||
|
||||
close_msg = cls._get_branch_close_or_delete_message(_, pull_request)
|
||||
merge_details["close_branch"] = dict(details={}, message=close_msg)
|
||||
|
||||
return merge_details
|
||||
|
||||
@classmethod
|
||||
def _get_branch_close_or_delete_message(cls, translator, pull_request):
|
||||
_ = translator
|
||||
|
||||
repo_type = pull_request.target_repo.repo_type
|
||||
close_msg = ""
|
||||
branch_name = pull_request.source_ref_parts.name
|
||||
max_branch_length = 20
|
||||
shorten_branch_name = (
|
||||
f"{branch_name[:max_branch_length]}..." if len(branch_name) > max_branch_length else branch_name
|
||||
)
|
||||
message_template = "Source branch '%s' will be {action} before the merge." % shorten_branch_name
|
||||
if repo_type == "hg":
|
||||
close_msg = _(message_template.format(action="closed"))
|
||||
elif repo_type == "git":
|
||||
close_msg = _(message_template.format(action="deleted"))
|
||||
return close_msg
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ChangeTuple:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
# 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 itertools
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
|
@ -23,6 +23,8 @@ import time
|
|||
import logging
|
||||
import traceback
|
||||
import datetime
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum, auto
|
||||
|
||||
from sqlalchemy.orm import aliased
|
||||
from zope.cachedescriptors.property import Lazy as LazyProperty
|
||||
|
|
@ -69,11 +71,32 @@ from rhodecode.model.db import (
|
|||
UserLog,
|
||||
)
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
from rhodecode.model.settings import VcsSettingsModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ForksAction(StrEnum):
|
||||
DELETE = auto()
|
||||
DETACH = auto()
|
||||
UNKNOWN = auto()
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls(cls.UNKNOWN)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForkActionResult:
|
||||
fork_count: int = 0
|
||||
action: ForksAction = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeleteRepoResult:
|
||||
is_success: bool
|
||||
fork_action_result: ForkActionResult = field(default_factory=ForkActionResult)
|
||||
|
||||
|
||||
class RepoModel(BaseModel):
|
||||
cls = Repository
|
||||
|
||||
|
|
@ -750,47 +773,49 @@ class RepoModel(BaseModel):
|
|||
raise
|
||||
|
||||
def delete(
|
||||
self, repo, forks=None, pull_requests=None, artifacts=None, fs_remove=True, cur_user=None, call_events=True
|
||||
):
|
||||
self,
|
||||
repo: Repository,
|
||||
forks_action: ForksAction = None, # used only in API to enforce action
|
||||
fs_remove: bool = True,
|
||||
cur_user: User = None,
|
||||
call_events: bool = True,
|
||||
) -> DeleteRepoResult:
|
||||
"""
|
||||
Delete given repository, forks parameter defines what do do with
|
||||
attached forks. Throws AttachedForksError if deleted repo has attached
|
||||
forks
|
||||
Delete a given repository.
|
||||
Throws AttachedForksError if a deleted repo has attached forks
|
||||
|
||||
:param repo:
|
||||
:param forks: str 'delete' or 'detach'
|
||||
:param pull_requests: str 'delete' or None
|
||||
:param artifacts: str 'delete' or None
|
||||
:param forks_action:
|
||||
NOTE: used only in API to enforce action;
|
||||
If the repository is private, then forks will be removed.
|
||||
If the repository is public, then forks will be detached.
|
||||
|
||||
:param fs_remove: remove(archive) repo from filesystem
|
||||
"""
|
||||
if not cur_user:
|
||||
cur_user = getattr(get_current_rhodecode_user(), "username", None)
|
||||
repo = self._get_repo(repo)
|
||||
if not repo:
|
||||
return False
|
||||
return DeleteRepoResult(is_success=False)
|
||||
|
||||
if forks == "detach":
|
||||
if not cur_user:
|
||||
cur_user = getattr(get_current_rhodecode_user(), "username", None)
|
||||
|
||||
if forks_action is None or forks_action is ForksAction.UNKNOWN:
|
||||
is_private = repo.private
|
||||
forks_action = ForksAction.DELETE if is_private else ForksAction.DETACH
|
||||
|
||||
forks_cnt = repo.forks.count()
|
||||
|
||||
if forks_action is ForksAction.DETACH:
|
||||
for r in repo.forks:
|
||||
r.fork = None
|
||||
self.sa.add(r)
|
||||
elif forks == "delete":
|
||||
elif forks_action is ForksAction.DELETE:
|
||||
for r in repo.forks:
|
||||
self.delete(r, forks="delete")
|
||||
elif [f for f in repo.forks]:
|
||||
self.delete(r, forks_action=forks_action)
|
||||
elif repo.forks.first() is not None:
|
||||
raise AttachedForksError()
|
||||
|
||||
# check for pull requests
|
||||
pr_sources = repo.pull_requests_source
|
||||
pr_targets = repo.pull_requests_target
|
||||
if pull_requests != "delete" and (pr_sources or pr_targets):
|
||||
raise AttachedPullRequestsError()
|
||||
|
||||
artifacts_objs = repo.artifacts
|
||||
if artifacts == "delete":
|
||||
for a in artifacts_objs:
|
||||
self.sa.delete(a)
|
||||
elif [a for a in artifacts_objs]:
|
||||
raise AttachedArtifactsError()
|
||||
self._delete_repo_related_assets(repo)
|
||||
|
||||
old_repo_dict = repo.get_dict()
|
||||
if call_events:
|
||||
|
|
@ -815,7 +840,17 @@ class RepoModel(BaseModel):
|
|||
log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
return True
|
||||
return DeleteRepoResult(
|
||||
is_success=True, fork_action_result=ForkActionResult(action=forks_action, fork_count=forks_cnt)
|
||||
)
|
||||
|
||||
def _delete_repo_related_assets(self, repo):
|
||||
pr_sources = repo.pull_requests_source
|
||||
pr_targets = repo.pull_requests_target
|
||||
artifacts = repo.artifacts
|
||||
logs = repo.logs
|
||||
for obj_to_remove in itertools.chain(pr_sources, pr_targets, artifacts, logs):
|
||||
self.sa.delete(obj_to_remove)
|
||||
|
||||
def grant_user_permission(self, repo, user, perm):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ var AgeModule = (function () {
|
|||
return {
|
||||
age: function(prevdate, now, show_short_version, show_suffix, short_format) {
|
||||
|
||||
var prevdate = moment(prevdate);
|
||||
var now = now || moment().utc();
|
||||
var prevdate = moment(prevdate).utc();
|
||||
var now = now ? moment(now).utc() : moment().utc();
|
||||
|
||||
var show_short_version = show_short_version || false;
|
||||
var show_suffix = show_suffix || true;
|
||||
|
|
|
|||
|
|
@ -200,50 +200,42 @@
|
|||
<div class="panel-body">
|
||||
${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=c.repo_name), request=request)}
|
||||
<table class="display">
|
||||
<% forks = len(c.fork_links) %>
|
||||
<% delete_forks = c.rhodecode_db_repo.private %>
|
||||
% if forks:
|
||||
<tr>
|
||||
<td>
|
||||
${_ungettext('This repository has %s fork.', 'This repository has %s forks.', c.rhodecode_db_repo.forks.count()) % c.rhodecode_db_repo.forks.count()}
|
||||
<td class="alert-${'error' if delete_forks else 'warning'}" style="padding: 10px">
|
||||
${_ungettext('Following fork will be %s:', 'Following forks will be %s:', forks) % ('deleted' if delete_forks else 'detached')}
|
||||
</td>
|
||||
<td>
|
||||
%if c.rhodecode_db_repo.forks.count():
|
||||
<input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label>
|
||||
%endif
|
||||
</td>
|
||||
<td>
|
||||
%if c.rhodecode_db_repo.forks.count():
|
||||
<input type="radio" name="forks" value="delete_forks"/> <label for="forks">${_('Delete forks')}</label>
|
||||
%endif
|
||||
<ul>
|
||||
% for fork in c.fork_links:
|
||||
<li class="list-unstyled"><a href="${fork}" target="_blank" rel="noopener noreferrer">${fork.replace("/", "")} <span aria-hidden="true" style="font-size: 0.9em;">↗️</span></a></li>
|
||||
% endfor
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
|
||||
<% attached_prs = len(c.rhodecode_db_repo.pull_requests_source + c.rhodecode_db_repo.pull_requests_target) %>
|
||||
% if c.rhodecode_db_repo.pull_requests_source or c.rhodecode_db_repo.pull_requests_target:
|
||||
% if attached_prs:
|
||||
<tr>
|
||||
<td>
|
||||
${_ungettext('This repository has %s attached pull request.', 'This repository has %s attached pull requests.', attached_prs) % attached_prs}
|
||||
<br/>
|
||||
<br/>
|
||||
<strong>${_('Consider to archive this repository instead.')}</strong>
|
||||
<td colspan="2" style="padding: 10px">
|
||||
<a href="${c.pr_link}" target="_blank" rel="noopener noreferrer">
|
||||
${_ungettext('This repository has %s attached pull request (open/close).', 'This repository has %s attached pull requests (open/close).', attached_prs) % attached_prs} <span aria-hidden="true" style="font-size: 0.9em;">↗️</span>
|
||||
</a>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
% endif
|
||||
|
||||
<% attached_artifacts = len(c.rhodecode_db_repo.artifacts) %>
|
||||
% if attached_artifacts:
|
||||
<tr>
|
||||
<td>
|
||||
<td colspan="2">
|
||||
${_ungettext('This repository has %s attached artifact.', 'This repository has %s attached artifacts.', attached_artifacts) % attached_artifacts}
|
||||
<br/>
|
||||
<br/>
|
||||
<strong>${_('Consider to archive this repository instead.')}</strong>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
% endif
|
||||
|
||||
</table>
|
||||
<div style="margin: 0 0 20px 0" class="fake-space"></div>
|
||||
|
||||
|
|
@ -259,7 +251,6 @@
|
|||
${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools.')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
% if c.statistics:
|
||||
% for stat in c.statistics:
|
||||
% if stat.get('sep'):
|
||||
<dt></dt>
|
||||
|
|
@ -13,6 +14,18 @@
|
|||
<dd>${stat['value']}</dd>
|
||||
% endif
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
% if c.error:
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">${_('ERROR')}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
${c.error}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@
|
|||
</div>
|
||||
<div class="input">
|
||||
${h.select('extern_type', c.extern_type, c.allowed_extern_types)}
|
||||
<p class="help-block">${_('When user was created using an external source. He is bound to authentication using this method.')}</p>
|
||||
<p class="help-block">${_('When user was created using an external source, he is bound to authentication using this method. If this set to "internal" user can login using RhodeCode internal auth and external identity. If this is set to some external auth system, internal login/password auth will be disabled.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
|
|||
2
rhodecode/tests/fixtures/rc_fixture.py
vendored
2
rhodecode/tests/fixtures/rc_fixture.py
vendored
|
|
@ -294,7 +294,7 @@ class Fixture(object):
|
|||
return r
|
||||
|
||||
def destroy_repo(self, repo_name, **kwargs):
|
||||
RepoModel().delete(repo_name, pull_requests="delete", artifacts="delete", **kwargs)
|
||||
RepoModel().delete(repo_name, **kwargs)
|
||||
Session().commit()
|
||||
|
||||
def destroy_repo_on_filesystem(self, repo_name):
|
||||
|
|
|
|||
|
|
@ -177,6 +177,32 @@ def test_age(age_args, expected, kw, baseapp):
|
|||
assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"prevdate_fn, now_fn",
|
||||
[
|
||||
(functools.partial(datetime.datetime.now, tz=datetime.timezone.max), datetime.datetime.now),
|
||||
(datetime.datetime.now, datetime.datetime.now),
|
||||
(datetime.datetime.now, functools.partial(datetime.datetime.now, tz=datetime.timezone.max)),
|
||||
(
|
||||
functools.partial(datetime.datetime.now, tz=datetime.timezone.max),
|
||||
functools.partial(datetime.datetime.now, tz=datetime.timezone.max),
|
||||
),
|
||||
(functools.partial(datetime.datetime.now, tz=datetime.timezone.max), lambda: None),
|
||||
(datetime.datetime.now, lambda: None),
|
||||
],
|
||||
)
|
||||
def test_age_timezone_aware(now_fn, prevdate_fn):
|
||||
from rhodecode.lib.utils2 import age
|
||||
|
||||
now = now_fn()
|
||||
prevdate = prevdate_fn()
|
||||
|
||||
try:
|
||||
age(prevdate, now)
|
||||
except Exception as e:
|
||||
pytest.fail(f"Function raised an exception: {e}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"age_args, expected, kw",
|
||||
[
|
||||
|
|
@ -326,19 +352,19 @@ def test_metatag_extraction(sample, expected_tags):
|
|||
(("lang", "[lang => .NET]"), '<div class="metatag" tag="lang">.NET</div>'),
|
||||
(
|
||||
("license", "[license => BSD 3-clause]"),
|
||||
'<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/BSD 3-clause">BSD 3-clause</a></div>',
|
||||
'<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/BSD 3-clause">BSD 3-clause</a></div>', # noqa: W605
|
||||
),
|
||||
(
|
||||
("license", "[license => GPLv3]"),
|
||||
'<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/GPLv3">GPLv3</a></div>',
|
||||
'<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/GPLv3">GPLv3</a></div>', # noqa: W605
|
||||
),
|
||||
(
|
||||
("license", "[license => MIT]"),
|
||||
'<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/MIT">MIT</a></div>',
|
||||
'<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/MIT">MIT</a></div>', # noqa: W605
|
||||
),
|
||||
(
|
||||
("license", "[license => AGPLv3]"),
|
||||
'<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/AGPLv3">AGPLv3</a></div>',
|
||||
'<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/AGPLv3">AGPLv3</a></div>', # noqa: W605
|
||||
),
|
||||
(
|
||||
("ref", "[requires => RepoName]"),
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ def test_repo2db_mapper_adds_new_repos(request, backend):
|
|||
for _repo in cleanup_repos:
|
||||
del_result = RepoModel().delete(_repo, call_events=False)
|
||||
Session().commit()
|
||||
assert del_result is True
|
||||
assert del_result.is_success is True
|
||||
|
||||
for _repo_group in cleanup_groups:
|
||||
del_result = RepoGroupModel().delete(_repo_group, force_delete=True, call_events=False)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from rhodecode.lib.exceptions import AttachedForksError
|
|||
from rhodecode.lib.utils import make_db_config
|
||||
from rhodecode.model.db import Repository
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.model.repo import RepoModel, ForksAction
|
||||
from rhodecode.model.scm import ScmModel
|
||||
|
||||
|
||||
|
|
@ -42,14 +42,34 @@ class TestRepoModel(object):
|
|||
assert Repository.get_by_repo_name(repo_name=backend.repo_name) is None
|
||||
assert repo.repo_name not in repos
|
||||
|
||||
def test_remove_repo_raises_exc_when_attached_forks(self, backend):
|
||||
def test_remove_repo_forks_are_detached(self, backend):
|
||||
repo = backend.create_repo()
|
||||
Session().commit()
|
||||
backend.create_fork()
|
||||
fork = backend.create_fork()
|
||||
fork_name = fork.repo_name
|
||||
Session().commit()
|
||||
|
||||
with pytest.raises(AttachedForksError):
|
||||
RepoModel().delete(repo=repo)
|
||||
assert fork.parent is not None
|
||||
|
||||
RepoModel().delete(repo=repo)
|
||||
|
||||
fork = Repository.get_by_repo_name(repo_name=fork_name)
|
||||
assert fork.parent is None
|
||||
|
||||
def test_remove_repo_forks_are_deleted(self, backend):
|
||||
repo = backend.create_repo()
|
||||
repo.private = True
|
||||
Session().commit()
|
||||
fork = backend.create_fork()
|
||||
fork_name = fork.repo_name
|
||||
Session().commit()
|
||||
|
||||
assert fork.parent is not None
|
||||
|
||||
RepoModel().delete(repo=repo)
|
||||
|
||||
fork = Repository.get_by_repo_name(repo_name=fork_name)
|
||||
assert fork is None
|
||||
|
||||
def test_remove_repo_delete_forks(self, backend):
|
||||
repo = backend.create_repo()
|
||||
|
|
@ -61,7 +81,7 @@ class TestRepoModel(object):
|
|||
fork_of_fork = backend.create_fork()
|
||||
Session().commit()
|
||||
|
||||
RepoModel().delete(repo=repo, forks="delete")
|
||||
RepoModel().delete(repo=repo, forks_action=ForksAction.DELETE)
|
||||
Session().commit()
|
||||
|
||||
assert Repository.get_by_repo_name(repo_name=repo.repo_name) is None
|
||||
|
|
@ -78,7 +98,7 @@ class TestRepoModel(object):
|
|||
fork_of_fork = backend.create_fork()
|
||||
Session().commit()
|
||||
|
||||
RepoModel().delete(repo=repo, forks="detach")
|
||||
RepoModel().delete(repo=repo, forks_action=ForksAction.DETACH)
|
||||
Session().commit()
|
||||
|
||||
assert Repository.get_by_repo_name(repo_name=repo.repo_name) is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue