diff --git a/configs/development.ini b/configs/development.ini index 54a2b0bd..7a367583 100644 --- a/configs/development.ini +++ b/configs/development.ini @@ -74,10 +74,6 @@ prefix = / use = egg:rhodecode-enterprise-ce -user.quotas.enabled = false -user.quotas.disk_space_mb = 100 -user.quotas.repository_count = 5 - ; enable proxy prefix middleware, defined above #filter-with = proxy-prefix diff --git a/configs/production.ini b/configs/production.ini index 61c76441..defb2512 100644 --- a/configs/production.ini +++ b/configs/production.ini @@ -68,10 +68,6 @@ prefix = / use = egg:rhodecode-enterprise-ce -user.quotas.enabled = false -user.quotas.disk_space_mb = 100 -user.quotas.repository_count = 5 - ; enable proxy prefix middleware, defined above #filter-with = proxy-prefix diff --git a/rhodecode/apps/_base/navigation.py b/rhodecode/apps/_base/navigation.py index a9ae29e6..6506f275 100644 --- a/rhodecode/apps/_base/navigation.py +++ b/rhodecode/apps/_base/navigation.py @@ -90,6 +90,7 @@ class NavigationRegistry(object): NavEntry("process_management", _("Processes"), "admin_settings_process_management"), NavEntry("sessions", _("User Sessions"), "admin_settings_sessions"), NavEntry("open_source", _("Open Source Licenses"), "admin_settings_open_source"), + NavEntry("user_quotas", _("User Quotas"), "admin_settings_user_quotas"), ] _labs_entry = NavEntry("labs", _("Labs"), "admin_settings_labs") diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py index 85373f20..453a482a 100644 --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -20,6 +20,7 @@ from rhodecode.apps._base import ADMIN_PREFIX from rhodecode.apps._base.navigation import includeme as nav_includeme from rhodecode.apps.admin.views.main_views import AdminMainView +from rhodecode.apps.admin.views.user_quotas import AdminUserQuotasView def admin_routes(config): @@ -163,6 +164,24 @@ def admin_routes(config): renderer="json_ext", ) + config.add_route("admin_settings_user_quotas", pattern="/settings/user_quotas") + config.add_view( + AdminUserQuotasView, + attr="admin_settings_user_quotas", + route_name="admin_settings_user_quotas", + request_method="GET", + renderer="rhodecode:templates/admin/settings/settings.mako", + ) + + config.add_route("admin_settings_user_quotas_update", pattern="/settings/user_quotas/update") + config.add_view( + AdminUserQuotasView, + attr="admin_settings_user_quotas_update", + route_name="admin_settings_user_quotas_update", + request_method="POST", + renderer="rhodecode:templates/admin/settings/settings.mako", + ) + config.add_route(name="admin_settings_vcs_svn_generate_cfg", pattern="/settings/vcs/svn_generate_cfg") config.add_view( AdminSvnConfigView, diff --git a/rhodecode/apps/admin/tests/test_admin_repos.py b/rhodecode/apps/admin/tests/test_admin_repos.py index b2763f96..2134607f 100644 --- a/rhodecode/apps/admin/tests/test_admin_repos.py +++ b/rhodecode/apps/admin/tests/test_admin_repos.py @@ -19,7 +19,7 @@ import urllib.request import urllib.parse import urllib.error -from unittest.mock import PropertyMock +from unittest.mock import PropertyMock, MagicMock, patch import mock import pytest @@ -101,10 +101,15 @@ class TestAdminRepos(object): self.assert_repository_is_created_correctly(repo_name, description, backend) - def test_create_fails_due_to_quota(self, backend, monkeypatch): - import rhodecode as app + @patch("rhodecode.model.quota_model.SettingsModel") + def test_create_fails_due_to_quota(self, settings_model_mock, backend): + test_quota_config = { + "rhodecode_user_quotas_enabled": True, + "rhodecode_global_user_repo_quotas": 1, + } - monkeypatch.setitem(app.CONFIG, "user.quotas.enabled", True) + mock_instance = settings_model_mock.return_value + mock_instance.get_all_settings.return_value = test_quota_config user: User = User.get_by_username(TEST_USER_REGULAR_LOGIN) existing_repo = backend.create_repo() @@ -121,7 +126,6 @@ class TestAdminRepos(object): user.update_quotas( max_disk_space_mb_allowance=100, max_repository_count_allowance=1, - used_disk_space_bytes=0, ) self.app.post( @@ -144,10 +148,15 @@ class TestAdminRepos(object): new_repo = RepoModel().get_by_repo_name(repo_name) assert new_repo is None - def test_create_count_user_repos(self, backend, monkeypatch): - import rhodecode as app + @patch("rhodecode.model.quota_model.SettingsModel") + def test_create_count_user_repos(self, settings_model_mock, backend): + test_quota_config = { + "rhodecode_user_quotas_enabled": True, + "rhodecode_global_user_repo_quotas": 1, + } - monkeypatch.setitem(app.CONFIG, "user.quotas.enabled", True) + mock_instance = settings_model_mock.return_value + mock_instance.get_all_settings.return_value = test_quota_config session = login_user_session(self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) csrf_token = auth.get_csrf_token(session) @@ -159,7 +168,6 @@ class TestAdminRepos(object): user.update_quotas( max_disk_space_mb_allowance=100, max_repository_count_allowance=1, - used_disk_space_bytes=0, ) self.app.post( diff --git a/rhodecode/apps/admin/views/user_quotas.py b/rhodecode/apps/admin/views/user_quotas.py new file mode 100644 index 00000000..2d0fb1b0 --- /dev/null +++ b/rhodecode/apps/admin/views/user_quotas.py @@ -0,0 +1,96 @@ +import json +import logging + +import formencode + +from pyramid.httpexceptions import HTTPFound +from rhodecode.apps._base import BaseAppView +from rhodecode.apps._base.navigation import navigation_list +from rhodecode.apps.ai_agents.ai_service import get_ai_service +from rhodecode.apps.ai_agents.ai_settings import AIModelName +from rhodecode.apps.ai_agents.models.base import AIService +from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, CSRFRequired +from rhodecode.lib import helpers as h +from rhodecode.model.db import User +from rhodecode.model.forms import AiSettingsForm, AiSettingsModelVersionUpdateForm, UserQuotasSettingsForm +from rhodecode.model.quota_model import UserQuotaModel +from rhodecode.model.settings import SettingsModel +from rhodecode.model.meta import Session + +log = logging.getLogger(__name__) + + +class AdminUserQuotasView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + return c + + @LoginRequired() + @HasPermissionAllDecorator("hg.admin") + def admin_settings_user_quotas(self): + c = self.load_default_context() + c.active = "user_quotas" + c.navlist = navigation_list(self.request) + + app_settings = c.rc_config + c.user_quotas_enabled = app_settings.get("rhodecode_user_quotas_enabled", UserQuotaModel.DEFAULT_QUOTAS_ENABLED) + c.global_user_repo_quotas = app_settings.get( + "rhodecode_global_user_repo_quotas", UserQuotaModel.DEFAULT_REPO_QUOTA + ) + + return self._get_template_context(c) + + @CSRFRequired() + @LoginRequired() + @HasPermissionAllDecorator("hg.admin") + def admin_settings_user_quotas_update(self): + c = self.load_default_context() + c.active = "user_quotas" + c.navlist = navigation_list(self.request) + _ = self.request.translate + + parsed_form = self._parse_form(_) + self._save_settings(_, parsed_form) + + return HTTPFound(h.route_path("admin_settings_user_quotas")) + + def _save_settings(self, _, data): + try: + repo_quotas_key = "rhodecode_global_user_repo_quotas" + apply_user_quotas = False + + settings = [ + ("user_quotas_enabled", "rhodecode_user_quotas_enabled", "bool"), + ("global_user_repo_quotas", repo_quotas_key, "int"), + ] + for setting, form_key, type_ in settings: + sett = SettingsModel().create_or_update_setting(setting, data[form_key], type_) + if setting == "user_quotas_enabled" and isinstance(data[form_key], bool): + apply_user_quotas = data[form_key] + + Session().add(sett) + + Session().commit() + SettingsModel().invalidate_settings_cache() + if apply_user_quotas: + h.flash(_(f"Applying user quotas, value: {data[repo_quotas_key]}"), category="success") + self._apply_user_repo_quotas() + + h.flash(_("User Quotas settings saved"), category="success") + except Exception as e: + log.exception("Exception saving User Quotas settings: %s", e) + h.flash(_(f"Error occurred during saving User Quotas settings, error: {str(e)}"), category="error") + + def _parse_form(self, _): + try: + form = UserQuotasSettingsForm()() + data = form.to_python(self.request.POST) + except formencode.Invalid as errors: + error = errors + h.flash(_(f"Invalid form error: {error}"), category="error") + raise HTTPFound(h.route_path("admin_settings_user_quotas")) + return data + + def _apply_user_repo_quotas(self): + for user in User.get_all(): + UserQuotaModel(user).init_quotas() diff --git a/rhodecode/apps/repository/tests/test_repo_settings_advanced.py b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py index 5a8690e1..84dcc40f 100644 --- a/rhodecode/apps/repository/tests/test_repo_settings_advanced.py +++ b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py @@ -117,10 +117,15 @@ class TestAdminRepoSettingsAdvanced(object): assert RepoModel().get_by_repo_name(repo_name) is None assert not repo_on_filesystem(repo_name_str) - def test_advanced_repo_delete_decrement_repo_quota(self, backend, monkeypatch): - import rhodecode as app + @patch("rhodecode.model.quota_model.SettingsModel") + def test_advanced_repo_delete_decrement_repo_quota(self, settings_model_mock, backend): + test_quota_config = { + "rhodecode_user_quotas_enabled": True, + "rhodecode_global_user_repo_quotas": 1, + } - monkeypatch.setitem(app.CONFIG, "user.quotas.enabled", True) + mock_instance = settings_model_mock.return_value + mock_instance.get_all_settings.return_value = test_quota_config session = login_user_session(self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) csrf_token = auth.get_csrf_token(session) @@ -129,7 +134,6 @@ class TestAdminRepoSettingsAdvanced(object): user.update_quotas( max_disk_space_mb_allowance=100, max_repository_count_allowance=1, - used_disk_space_bytes=0, ) repo = backend.create_repo(name_suffix="quota") diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index d1d88bff..8e68a252 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -706,3 +706,12 @@ def AiSettingsModelVersionUpdateForm(): rhodecode_ai_api_key = v.UnicodeString(strip=True) return _AiSettingsModelVersionUpdateForm + + +def UserQuotasSettingsForm(): + class _UserQuotasSettingsForm(formencode.Schema): + allow_extra_fields = True + rhodecode_user_quotas_enabled = v.StringBoolean(if_missing=False) + rhodecode_global_user_repo_quotas = v.Int(strip=True, required=True) + + return _UserQuotasSettingsForm diff --git a/rhodecode/model/quota_model.py b/rhodecode/model/quota_model.py index 2fa23bf6..aa8a2427 100644 --- a/rhodecode/model/quota_model.py +++ b/rhodecode/model/quota_model.py @@ -2,53 +2,46 @@ import logging from sqlalchemy import func, select -import rhodecode as app -from rhodecode.model.db import User, Repository +from rhodecode.model.db import Repository from rhodecode.model.meta import Session +from rhodecode.model.settings import SettingsModel log = logging.getLogger(__name__) class UserQuotaModel: - """ - NOTE: quotas are applied only when a user registers itself or the admin adds a user, - in plugins there are more ways to add a user. - """ - UNLIMITED = float("inf") + DEFAULT_REPO_QUOTA = 5 + DEFAULT_QUOTAS_ENABLED = False class QuotaError(Exception): pass - def __init__(self, user: User): - self._settings = app.CONFIG - self.enabled = self._settings.get("user.quotas.enabled") - self.default_max_disk_space_mb_allowance = self._settings.get("user.quotas.disk_space_mb") - self.default_max_repository_count_allowance = self._settings.get("user.quotas.repository_count") + def __init__(self, user): + self._settings = SettingsModel().get_all_settings() + self.enabled = self._settings.get("rhodecode_user_quotas_enabled", UserQuotaModel.DEFAULT_QUOTAS_ENABLED) + self.max_disk_space_mb_allowance = 100 # not implemented yet + self.max_repository_count_allowance = self._settings.get( + "rhodecode_global_user_repo_quotas", UserQuotaModel.DEFAULT_REPO_QUOTA + ) self.user = user def init_quotas(self): + from rhodecode.model.db import User # resolve circular dependency + if not self.enabled: log.debug("quotas are disabled") return - self.user.update_quotas( - max_disk_space_mb_allowance=self.default_max_disk_space_mb_allowance, - max_repository_count_allowance=self.default_max_repository_count_allowance, - used_disk_space_bytes=0, - ) - - def _init_quotas_for_old_users(self): - if not self.enabled: - log.debug("quotas are disabled") - return - - log.debug("Initializing quotas for old users.") + mb_allowance = self.max_disk_space_mb_allowance + repo_count_allowance = self.max_repository_count_allowance + if self.user.admin or self.user.username in [User.AI_USER, User.DEFAULT_USER, User.GHOST_USER]: + mb_allowance = self.UNLIMITED + repo_count_allowance = self.UNLIMITED self.user.update_quotas( - max_disk_space_mb_allowance=self.UNLIMITED, - max_repository_count_allowance=self.UNLIMITED, - used_disk_space_bytes=0, + max_disk_space_mb_allowance=mb_allowance, + max_repository_count_allowance=repo_count_allowance, ) def user_repositories_count(self): @@ -64,8 +57,8 @@ class UserQuotaModel: u_quotas = self.user.get_quotas() return u_quotas.get("max_repository_count_allowance") > self.user_repositories_count() - log.warning("User quotas not present, probably old user.") - self._init_quotas_for_old_users() + log.warning("User quotas not present, re-initialize quotas.") + self.init_quotas() return self.is_repo_creation_allowed() def _user_quotas_valid(self): @@ -73,7 +66,6 @@ class UserQuotaModel: for key in [ "max_disk_space_mb_allowance", "max_repository_count_allowance", - "used_disk_space_bytes", ]: if u_quotas.get(key) is None: log.error( diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py index 9bc2552f..bf3a8327 100644 --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -410,10 +410,6 @@ class UserModel(BaseModel): new_user.update_userdata(language=language) new_user.update_userdata(notification_status=True) - if new_user.is_new_user: - qm = UserQuotaModel(new_user) - qm.init_quotas() - self.sa.add(new_user) if not edit and create_repo_group: diff --git a/rhodecode/templates/admin/settings/settings_user_quotas.mako b/rhodecode/templates/admin/settings/settings_user_quotas.mako new file mode 100644 index 00000000..93ce280b --- /dev/null +++ b/rhodecode/templates/admin/settings/settings_user_quotas.mako @@ -0,0 +1,33 @@ +
+
+

${_('Global User Quotas')}

+
+
+ ${h.secure_form(h.route_path('admin_settings_user_quotas_update'), id='user_quotas_form', request=request)} +
+ +
+
+ +
+
+ ${h.checkbox('rhodecode_user_quotas_enabled',value=True, checked=c.user_quotas_enabled==True)} +
+
+ +
+
+ +
+
+ ${h.text('rhodecode_global_user_repo_quotas', c.global_user_repo_quotas)} +
+
+ +
+ ${h.submit('save',_('Save'),class_="btn")} +
+ ${h.end_form()} +
+
+