Merge pull request !2859 from rhodecode-enterprise-ce feature/RCCE-321_user_quotas
Changes from branch: Feature/RCCE 321 user quotas
This commit is contained in:
commit
586b363efb
17 changed files with 530 additions and 3 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -942,6 +961,27 @@ def admin_routes(config):
|
|||
request_method="POST",
|
||||
)
|
||||
|
||||
# user quotas
|
||||
config.add_route(name="edit_user_quotas", pattern=r"/users/{user_id:\d+}/edit/quotas", user_route=True)
|
||||
config.add_view(
|
||||
UsersView,
|
||||
attr="user_quotas",
|
||||
route_name="edit_user_quotas",
|
||||
request_method="GET",
|
||||
renderer="rhodecode:templates/admin/users/user_edit.mako",
|
||||
)
|
||||
|
||||
config.add_route(
|
||||
name="edit_user_quotas_update", pattern=r"/users/{user_id:\d+}/edit/quotas/update", user_route=True
|
||||
)
|
||||
config.add_view(
|
||||
UsersView,
|
||||
attr="user_quotas_update",
|
||||
route_name="edit_user_quotas_update",
|
||||
request_method="POST",
|
||||
renderer="rhodecode:templates/admin/users/user_edit.mako",
|
||||
)
|
||||
|
||||
# user audit logs
|
||||
config.add_route(name="edit_user_audit_logs", pattern=r"/users/{user_id:\d+}/edit/audit", user_route=True)
|
||||
config.add_view(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
from unittest.mock import PropertyMock, MagicMock, patch
|
||||
|
||||
import mock
|
||||
import pytest
|
||||
|
|
@ -29,6 +30,7 @@ from rhodecode.lib.utils2 import safe_str
|
|||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.model.db import Repository, RepoGroup, UserRepoToPerm, User, Permission
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.quota_model import UserQuotaModel
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.model.repo_group import RepoGroupModel
|
||||
from rhodecode.model.user import UserModel
|
||||
|
|
@ -85,7 +87,7 @@ class TestAdminRepos(object):
|
|||
description_unicode = "description for newly created repo" + suffix
|
||||
description = description_unicode
|
||||
|
||||
response = self.app.post(
|
||||
self.app.post(
|
||||
route_path("repo_create"),
|
||||
fixture._get_repo_create_params(
|
||||
repo_private=False,
|
||||
|
|
@ -99,6 +101,91 @@ class TestAdminRepos(object):
|
|||
|
||||
self.assert_repository_is_created_correctly(repo_name, description, backend)
|
||||
|
||||
@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,
|
||||
}
|
||||
|
||||
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()
|
||||
existing_repo.user = user
|
||||
Session().commit()
|
||||
|
||||
session = login_user_session(self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
|
||||
csrf_token = auth.get_csrf_token(session)
|
||||
|
||||
repo_name_unicode = backend.new_repo_name(suffix="no_quota")
|
||||
repo_name = repo_name_unicode
|
||||
|
||||
user: User = User.get_by_username(TEST_USER_REGULAR_LOGIN)
|
||||
user.update_quotas(
|
||||
max_disk_space_mb_allowance=100,
|
||||
max_repository_count_allowance=1,
|
||||
)
|
||||
|
||||
self.app.post(
|
||||
route_path("repo_create"),
|
||||
fixture._get_repo_create_params(
|
||||
repo_name=repo_name,
|
||||
repo_type=backend.alias,
|
||||
repo_description="no_quota",
|
||||
csrf_token=csrf_token,
|
||||
),
|
||||
status=302,
|
||||
)
|
||||
|
||||
response = self.app.get(route_path("repo_creating_check", repo_name=repo_name))
|
||||
assert response.json == {"result": False}
|
||||
|
||||
flash_msg = "User quotas exceeded. You can't create more repositories."
|
||||
assert_session_flash(response, flash_msg)
|
||||
|
||||
new_repo = RepoModel().get_by_repo_name(repo_name)
|
||||
assert new_repo is None
|
||||
|
||||
@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,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
repo_name_unicode = backend.new_repo_name(suffix="no_quota")
|
||||
repo_name = repo_name_unicode
|
||||
|
||||
user: User = User.get_by_username(TEST_USER_REGULAR_LOGIN)
|
||||
user.update_quotas(
|
||||
max_disk_space_mb_allowance=100,
|
||||
max_repository_count_allowance=1,
|
||||
)
|
||||
|
||||
self.app.post(
|
||||
route_path("repo_create"),
|
||||
fixture._get_repo_create_params(
|
||||
repo_name=repo_name,
|
||||
repo_type=backend.alias,
|
||||
repo_description="quota",
|
||||
csrf_token=csrf_token,
|
||||
),
|
||||
status=302,
|
||||
)
|
||||
|
||||
self.assert_repository_is_created_correctly(repo_name, "quota", backend)
|
||||
user: User = User.get_by_username(TEST_USER_REGULAR_LOGIN)
|
||||
user_repos = Repository.get_all_repos(user.user_id)
|
||||
assert len(user_repos) == 1
|
||||
|
||||
def test_create_numeric_name(self, autologin_user, backend, csrf_token):
|
||||
numeric_repo = "1234"
|
||||
repo_name = numeric_repo
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ from rhodecode.lib.utils import repo_name_slug
|
|||
from rhodecode.lib.utils2 import safe_int, safe_str
|
||||
from rhodecode.model.forms import RepoForm
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
from rhodecode.model.quota_model import UserQuotaModel
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
|
@ -220,6 +221,11 @@ class AdminReposView(BaseAppView, DataGridAppView):
|
|||
copy_permissions = form_result.get("repo_copy_permissions")
|
||||
# create is done sometimes async on celery, db transaction
|
||||
# management is handled there.
|
||||
|
||||
quota_model = UserQuotaModel(self._rhodecode_db_user)
|
||||
if not quota_model.is_repo_creation_allowed():
|
||||
raise UserQuotaModel.QuotaError("User quotas exceeded. You can't create more repositories.")
|
||||
|
||||
task = RepoModel().create(form_result, self._rhodecode_user.user_id)
|
||||
task_id = get_task_id(task)
|
||||
except formencode.Invalid as errors:
|
||||
|
|
@ -234,6 +240,11 @@ class AdminReposView(BaseAppView, DataGridAppView):
|
|||
)
|
||||
return Response(html)
|
||||
|
||||
except UserQuotaModel.QuotaError as e:
|
||||
msg = str(e)
|
||||
log.error(msg)
|
||||
h.flash(msg, category="error")
|
||||
raise HTTPFound(h.route_path("home"))
|
||||
except Exception as e:
|
||||
msg = self._log_creation_exception(e, form_result.get("repo_name"))
|
||||
h.flash(msg, category="error")
|
||||
|
|
|
|||
96
rhodecode/apps/admin/views/user_quotas.py
Normal file
96
rhodecode/apps/admin/views/user_quotas.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -55,8 +55,10 @@ from rhodecode.model.forms import (
|
|||
UserPermissionsForm,
|
||||
UserExtraEmailForm,
|
||||
UserExtraIpForm,
|
||||
UserQuotasUpdateForm,
|
||||
)
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
from rhodecode.model.quota_model import UserQuotaModel
|
||||
from rhodecode.model.repo_group import RepoGroupModel
|
||||
from rhodecode.model.ssh_key import SshKeyModel
|
||||
from rhodecode.model.user import UserModel
|
||||
|
|
@ -283,6 +285,9 @@ class UsersView(UserAppView):
|
|||
("zh", "Chinese (zh)"),
|
||||
]
|
||||
|
||||
app_settings = c.rc_config
|
||||
c.quotas_enabled = app_settings.get("rhodecode_user_quotas_enabled", UserQuotaModel.DEFAULT_QUOTAS_ENABLED)
|
||||
|
||||
c.allowed_extern_types = [(x.uid, x.get_display_name()) for x in self.get_auth_plugins()]
|
||||
perms = req.registry.settings.get("available_permissions")
|
||||
if not perms:
|
||||
|
|
@ -1122,6 +1127,43 @@ class UsersView(UserAppView):
|
|||
|
||||
return HTTPFound(h.route_path("edit_user_groups_management", user_id=user_id))
|
||||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator("hg.admin")
|
||||
def user_quotas(self):
|
||||
_ = self.request.translate
|
||||
c = self.load_default_context()
|
||||
c.user = self.db_user
|
||||
|
||||
quota_model = UserQuotaModel(c.user)
|
||||
|
||||
c.active = "user_quotas"
|
||||
c.user_repo_quotas = quota_model.formatted_user_max_repository_count_allowance()
|
||||
c.user_repo_count = quota_model.user_repositories_count
|
||||
|
||||
return self._get_template_context(c)
|
||||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator("hg.admin")
|
||||
def user_quotas_update(self):
|
||||
_ = self.request.translate
|
||||
c = self.load_default_context()
|
||||
c.user = self.db_user
|
||||
|
||||
c.active = "user_quotas"
|
||||
|
||||
user_form = UserQuotasUpdateForm()()
|
||||
try:
|
||||
quota_model = UserQuotaModel(c.user)
|
||||
form_result = user_form.to_python(dict(self.request.POST))
|
||||
quota_model.user_max_repository_count_allowance = form_result["user_repo_quotas"]
|
||||
h.flash(_("Updated user '%s' repository quota") % c.user.username, category="success")
|
||||
except Exception as e:
|
||||
log.exception("Exception updating user quotas, %s", e)
|
||||
h.flash(
|
||||
_("Exception updating user quotas of user %s") % self.request.POST.get("username"), category="error"
|
||||
)
|
||||
raise HTTPFound(h.route_path("edit_user_quotas", user_id=c.user.user_id))
|
||||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator("hg.admin")
|
||||
def user_audit_logs(self):
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@ import pytest
|
|||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from rhodecode.apps.repository.views.repo_settings_advanced import RepoSettingsAdvancedView
|
||||
from rhodecode.bootstrap import TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS
|
||||
from rhodecode.lib import auth
|
||||
from rhodecode.lib.str_utils import safe_str
|
||||
from rhodecode.model.db import Repository
|
||||
from rhodecode.model.db import Repository, User
|
||||
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 import HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator, login_user_session
|
||||
from rhodecode.tests.fixtures.rc_fixture import Fixture
|
||||
from rhodecode.tests.utils import repo_on_filesystem
|
||||
from rhodecode.tests.routes import route_path
|
||||
|
|
@ -115,6 +117,46 @@ class TestAdminRepoSettingsAdvanced(object):
|
|||
assert RepoModel().get_by_repo_name(repo_name) is None
|
||||
assert not repo_on_filesystem(repo_name_str)
|
||||
|
||||
@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,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
user: User = User.get_by_username(TEST_USER_REGULAR_LOGIN)
|
||||
user.update_quotas(
|
||||
max_disk_space_mb_allowance=100,
|
||||
max_repository_count_allowance=1,
|
||||
)
|
||||
|
||||
repo = backend.create_repo(name_suffix="quota")
|
||||
repo.user = user
|
||||
Session().commit()
|
||||
|
||||
repo_name = repo.repo_name
|
||||
repo_name_str = safe_str(repo.repo_name)
|
||||
|
||||
response = self.app.post(
|
||||
route_path("edit_repo_advanced_delete", repo_name=repo_name_str), params={"csrf_token": csrf_token}
|
||||
)
|
||||
assert_session_flash(response, "Deleted repository `{}`".format(repo_name))
|
||||
response.follow()
|
||||
|
||||
user: User = User.get_by_username(TEST_USER_REGULAR_LOGIN)
|
||||
|
||||
# check if repo was deleted from db
|
||||
assert RepoModel().get_by_repo_name(repo_name) is None
|
||||
assert not repo_on_filesystem(repo_name_str)
|
||||
user_repos = Repository.get_all_repos(user.user_id)
|
||||
assert len(user_repos) == 0
|
||||
|
||||
@pytest.mark.parametrize("suffix", ["", "ąęł", "123"], ids=no_newline_id_generator)
|
||||
def test_delete_broken_repo(self, autologin_user, backend, suffix, csrf_token):
|
||||
repo = backend.create_repo(name_suffix=suffix, private_repo=True)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,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.quota_model import UserQuotaModel
|
||||
from rhodecode.model.repo import RepoModel, ForksAction
|
||||
from rhodecode.model.scm import ScmModel
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,10 @@ def sanitize_settings_and_apply_defaults(global_config, settings):
|
|||
settings_maker.make_setting("celery.deduplicate_lock_store", "redis://redis:6379/9", parser="string")
|
||||
settings_maker.make_setting("celery.lock_ttl_seconds", 3600, parser="int")
|
||||
|
||||
settings_maker.make_setting("user.quotas.enabled", False, parser="bool")
|
||||
settings_maker.make_setting("user.quotas.disk_space_mb", 200, parser="int")
|
||||
settings_maker.make_setting("user.quotas.repository_count", 5, parser="int")
|
||||
|
||||
settings_maker.make_setting("exception_tracker.send_email", False, parser="bool")
|
||||
settings_maker.make_setting("exception_tracker.email_prefix", "[RHODECODE ERROR]", default_when_empty=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ from rhodecode.model.db import (
|
|||
)
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
from rhodecode.model.pull_request import PullRequestModel
|
||||
from rhodecode.model.quota_model import UserQuotaModel
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1256,6 +1256,15 @@ class User(Base, BaseModel):
|
|||
if user:
|
||||
return user
|
||||
|
||||
def update_quotas(self, **kwargs):
|
||||
quotas = self.user_data.get("quotas", {})
|
||||
quotas.update(**kwargs)
|
||||
self.update_userdata(quotas=quotas)
|
||||
Session().commit()
|
||||
|
||||
def get_quotas(self):
|
||||
return self.user_data.get("quotas", {})
|
||||
|
||||
def update_userdata(self, **kwargs):
|
||||
usr = self
|
||||
old = usr.user_data
|
||||
|
|
|
|||
|
|
@ -706,3 +706,20 @@ 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
|
||||
|
||||
|
||||
def UserQuotasUpdateForm():
|
||||
class _UserQuotasUpdateForm(formencode.Schema):
|
||||
allow_extra_fields = True
|
||||
user_repo_quotas = v.Int(strip=True, required=True)
|
||||
|
||||
return _UserQuotasUpdateForm
|
||||
|
|
|
|||
95
rhodecode/model/quota_model.py
Normal file
95
rhodecode/model/quota_model.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import logging
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from rhodecode.model.db import Repository
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserQuotaModel:
|
||||
UNLIMITED = -1
|
||||
DEFAULT_REPO_QUOTA = 5
|
||||
DEFAULT_QUOTAS_ENABLED = False
|
||||
|
||||
class QuotaError(Exception):
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
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=mb_allowance,
|
||||
max_repository_count_allowance=repo_count_allowance,
|
||||
)
|
||||
|
||||
@property
|
||||
def user_repositories_count(self) -> int:
|
||||
return Session().scalar(
|
||||
select(func.count()).select_from(Repository).where(Repository.user_id == self.user.user_id)
|
||||
)
|
||||
|
||||
@property
|
||||
def user_max_repository_count_allowance(self) -> int:
|
||||
quotas = self.user.get_quotas()
|
||||
return quotas.get("max_repository_count_allowance")
|
||||
|
||||
@user_max_repository_count_allowance.setter
|
||||
def user_max_repository_count_allowance(self, new_value: int):
|
||||
self.user.update_quotas(max_repository_count_allowance=new_value)
|
||||
|
||||
def formatted_user_max_repository_count_allowance(self) -> str:
|
||||
max_repos = self.user_max_repository_count_allowance
|
||||
if max_repos == self.UNLIMITED:
|
||||
return "Unlimited"
|
||||
return str(max_repos)
|
||||
|
||||
def is_repo_creation_allowed(self):
|
||||
if not self.enabled:
|
||||
return True
|
||||
|
||||
if self._user_quotas_valid():
|
||||
if self.user_max_repository_count_allowance == self.UNLIMITED:
|
||||
# since DB can't save float('inf'), we need to use a special case
|
||||
return True
|
||||
|
||||
return self.user_max_repository_count_allowance > self.user_repositories_count
|
||||
|
||||
log.warning("User quotas not present, re-initialize quotas.")
|
||||
self.init_quotas()
|
||||
return self.is_repo_creation_allowed()
|
||||
|
||||
def _user_quotas_valid(self):
|
||||
u_quotas = self.user.get_quotas()
|
||||
for key in [
|
||||
"max_disk_space_mb_allowance",
|
||||
"max_repository_count_allowance",
|
||||
]:
|
||||
if u_quotas.get(key) is None:
|
||||
log.error(
|
||||
f"User {self.user.username} has no quota value for {key}, probably quota initialization was not done."
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
@ -62,6 +62,7 @@ from rhodecode.model.db import (
|
|||
)
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.auth_token import AuthTokenModel
|
||||
from rhodecode.model.quota_model import UserQuotaModel
|
||||
from rhodecode.model.repo_group import RepoGroupModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -411,6 +412,13 @@ class UserModel(BaseModel):
|
|||
|
||||
self.sa.add(new_user)
|
||||
|
||||
# TODO: fix tests and uncomment this, because tests launching pyramid app, this part fails during
|
||||
# fixture setup, in normal app this works as intended
|
||||
# user quotas will be lazily initialized anyway, but this is workaround
|
||||
# if new_user.is_new_user:
|
||||
# qm = UserQuotaModel(new_user)
|
||||
# qm.init_quotas()
|
||||
|
||||
if not edit and create_repo_group:
|
||||
RepoGroupModel().create_personal_repo_group(new_user, commit_early=False)
|
||||
|
||||
|
|
@ -443,6 +451,8 @@ class UserModel(BaseModel):
|
|||
form_data["extern_type"] = extern_type
|
||||
new_user = self.create(form_data)
|
||||
|
||||
qm = UserQuotaModel(new_user)
|
||||
qm.init_quotas()
|
||||
self.sa.add(new_user)
|
||||
self.sa.flush()
|
||||
|
||||
|
|
|
|||
34
rhodecode/templates/admin/settings/settings_user_quotas.mako
Normal file
34
rhodecode/templates/admin/settings/settings_user_quotas.mako
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">${_('Global User Quotas')}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
${h.secure_form(h.route_path('admin_settings_user_quotas_update'), id='user_quotas_form', request=request)}
|
||||
<div class="fields">
|
||||
|
||||
<div class="field" id="user-quotas-toggle">
|
||||
<div class="label label-checkbox">
|
||||
<label for="rhodecode_user_quotas_enabled">${_('Enable User Quotas')}:</label>
|
||||
</div>
|
||||
<div class="checkboxes">
|
||||
${h.checkbox('rhodecode_user_quotas_enabled',value=True, checked=c.user_quotas_enabled==True)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" id="rhodecode_global_user_repo_quotas">
|
||||
<div class="label label">
|
||||
<label for="rhodecode_global_user_repo_quotas">${_('Maximum repositories per user')}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
${h.text('rhodecode_global_user_repo_quotas', c.global_user_repo_quotas)}
|
||||
<p class="help-block pre-formatting">${_('If you want to set an unlimited quota, enter -1.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('save',_('Save'),class_="btn")}
|
||||
</div>
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -46,6 +46,9 @@
|
|||
<li class="${h.is_active('groups', c.active)}"><a href="${h.route_path('edit_user_groups_management', user_id=c.user.user_id)}">${_('User Groups Management')}</a></li>
|
||||
<li class="${h.is_active('audit', c.active)}"><a href="${h.route_path('edit_user_audit_logs', user_id=c.user.user_id)}">${_('Audit logs')}</a></li>
|
||||
<li class="${h.is_active('caches', c.active)}"><a href="${h.route_path('edit_user_caches', user_id=c.user.user_id)}">${_('Caches')}</a></li>
|
||||
%if c.quotas_enabled:
|
||||
<li class="${h.is_active('user_quotas', c.active)}"><a href="${h.route_path('edit_user_quotas', user_id=c.user.user_id)}">${_('User Quotas')}</a></li>
|
||||
%endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
34
rhodecode/templates/admin/users/user_edit_user_quotas.mako
Normal file
34
rhodecode/templates/admin/users/user_edit_user_quotas.mako
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">${_('User Quotas')}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
${h.secure_form(h.route_path('edit_user_quotas_update', user_id=c.user.user_id), id='user_quotas_form', request=request)}
|
||||
<div class="fields">
|
||||
|
||||
<div class="field" id="user_repo_count">
|
||||
<div class="label label">
|
||||
<label for="user_repo_count">${_('Current Repository Count')}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
${h.text('user_repo_count', c.user_repo_count, readonly=True, class_='no-border')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" id="user_repo_quotas">
|
||||
<div class="label label">
|
||||
<label for="user_repo_quotas">${_('Maximum repositories')}</label>
|
||||
</div>
|
||||
<div class="input">
|
||||
${h.text('user_repo_quotas', c.user_repo_quotas)}
|
||||
<p class="help-block pre-formatting">${_('If you want to set an unlimited quota, enter -1.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('save',_('Save'),class_="btn")}
|
||||
</div>
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue