feature: initial implementation of qoutas for a user, repo count part
This commit is contained in:
parent
a78d2157a2
commit
746c5b4699
11 changed files with 273 additions and 3 deletions
|
|
@ -74,6 +74,10 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
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,79 @@ 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
|
||||
|
||||
monkeypatch.setitem(app.CONFIG, "user.quotas.enabled", True)
|
||||
|
||||
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=200,
|
||||
max_repository_count_allowance=2,
|
||||
current_repos_count=2,
|
||||
used_disk_space_bytes=0,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def test_create_count_user_repos(self, backend, monkeypatch):
|
||||
import rhodecode as app
|
||||
|
||||
monkeypatch.setitem(app.CONFIG, "user.quotas.enabled", True)
|
||||
|
||||
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=200,
|
||||
max_repository_count_allowance=2,
|
||||
current_repos_count=0,
|
||||
used_disk_space_bytes=0,
|
||||
)
|
||||
|
||||
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)
|
||||
assert user.get_quotas()["current_repos_count"] == 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")
|
||||
|
|
|
|||
|
|
@ -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,42 @@ 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
|
||||
|
||||
monkeypatch.setitem(app.CONFIG, "user.quotas.enabled", True)
|
||||
|
||||
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=200,
|
||||
max_repository_count_allowance=2,
|
||||
current_repos_count=1,
|
||||
used_disk_space_bytes=0,
|
||||
)
|
||||
|
||||
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)
|
||||
assert user.get_quotas()["current_repos_count"] == 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
|
||||
|
||||
|
|
@ -157,6 +158,9 @@ class RepoSettingsAdvancedView(RepoAppView):
|
|||
self.request.POST.get("backup", "false")
|
||||
) # if this option is absent, this means that on FE side checkbox was unchecked
|
||||
delete_res = RepoModel().delete(self.db_repo, fs_backup=backup)
|
||||
repo_owner = self.db_repo.user
|
||||
quota_model = UserQuotaModel(repo_owner)
|
||||
quota_model.decrement_repo_count()
|
||||
|
||||
fork_action = delete_res.fork_action_result.action
|
||||
fork_cnt = delete_res.fork_action_result.fork_count
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -279,6 +280,9 @@ def create_repo(form_data, cur_user):
|
|||
repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id),
|
||||
)
|
||||
|
||||
quota_model = UserQuotaModel(cur_user)
|
||||
quota_model.increment_repo_count()
|
||||
|
||||
Session().commit()
|
||||
|
||||
PermissionModel().trigger_permission_flush()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
110
rhodecode/model/quota_model.py
Normal file
110
rhodecode/model/quota_model.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from rhodecode.model.db import User
|
||||
import rhodecode as app
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def always_valid_for_super_admin(f):
|
||||
@wraps(f)
|
||||
def inner(self, *args, **kwargs):
|
||||
if self.user.is_admin:
|
||||
return True
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def skip_for_super_admin(f):
|
||||
@wraps(f)
|
||||
def inner(self, *args, **kwargs):
|
||||
if self.user.is_admin:
|
||||
log.debug(f"Skipping {f.__name__} for super admin.")
|
||||
return
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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")
|
||||
self.user = user
|
||||
|
||||
def init_quotas(self):
|
||||
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,
|
||||
current_repos_count=0,
|
||||
used_disk_space_bytes=0,
|
||||
)
|
||||
|
||||
@always_valid_for_super_admin
|
||||
def is_repo_creation_allowed(self):
|
||||
if not self.enabled:
|
||||
return True
|
||||
|
||||
if self._user_quotas_valid():
|
||||
u_quotas = self.user.get_quotas()
|
||||
return u_quotas.get("max_repository_count_allowance") > u_quotas.get("current_repos_count")
|
||||
|
||||
log.warning("User quotas not present, probably old user, allowing repo creation.")
|
||||
return True
|
||||
|
||||
@skip_for_super_admin
|
||||
def increment_repo_count(self):
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
if self._user_quotas_valid():
|
||||
u_quotas = self.user.get_quotas()
|
||||
new_count = u_quotas["current_repos_count"] + 1
|
||||
self.user.update_quotas(current_repos_count=new_count)
|
||||
|
||||
log.warning("User quotas not present, probably old user, skipping.")
|
||||
|
||||
@skip_for_super_admin
|
||||
def decrement_repo_count(self):
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
u_quotas = self.user.get_quotas()
|
||||
if self._user_quotas_valid() and u_quotas["current_repos_count"] > 0:
|
||||
new_count = u_quotas["current_repos_count"] - 1
|
||||
self.user.update_quotas(current_repos_count=new_count)
|
||||
|
||||
log.warning("User quotas not present, probably old user, skipping.")
|
||||
|
||||
def _user_quotas_valid(self):
|
||||
u_quotas = self.user.get_quotas()
|
||||
for key in [
|
||||
"max_disk_space_mb_allowance",
|
||||
"max_repository_count_allowance",
|
||||
"current_repos_count",
|
||||
"used_disk_space_bytes",
|
||||
]:
|
||||
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__)
|
||||
|
|
@ -409,6 +410,10 @@ 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:
|
||||
|
|
@ -443,6 +448,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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue