repo_group_schema.py
311 lines
| 11.3 KiB
| text/x-python
|
PythonLexer
r5608 | # Copyright (C) 2016-2024 RhodeCode GmbH | |||
r523 | # | |||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU Affero General Public License, version 3 | ||||
# (only), as published by the Free Software Foundation. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU Affero General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
# | ||||
# This program is dual-licensed. If you wish to learn more about the | ||||
# RhodeCode Enterprise Edition, including its added features, Support services, | ||||
# and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
import colander | ||||
r2175 | import deform.widget | |||
r523 | ||||
r4706 | from rhodecode.model.validation_schema.utils import username_converter | |||
r1154 | from rhodecode.translation import _ | |||
r523 | from rhodecode.model.validation_schema import validators, preparers, types | |||
r1154 | def get_group_and_repo(repo_name): | |||
from rhodecode.model.repo_group import RepoGroupModel | ||||
return RepoGroupModel()._get_group_name_and_parent( | ||||
repo_name, get_object=True) | ||||
r2241 | def get_repo_group(repo_group_id): | |||
from rhodecode.model.repo_group import RepoGroup | ||||
return RepoGroup.get(repo_group_id), RepoGroup.CHOICES_SEPARATOR | ||||
r1154 | @colander.deferred | |||
def deferred_can_write_to_group_validator(node, kw): | ||||
old_values = kw.get('old_values') or {} | ||||
request_user = kw.get('user') | ||||
def can_write_group_validator(node, value): | ||||
from rhodecode.lib.auth import ( | ||||
HasPermissionAny, HasRepoGroupPermissionAny) | ||||
from rhodecode.model.repo_group import RepoGroupModel | ||||
messages = { | ||||
'invalid_parent_repo_group': | ||||
r5095 | _("Parent repository group `{}` does not exist"), | |||
r1154 | # permissions denied we expose as not existing, to prevent | |||
# resource discovery | ||||
'permission_denied_parent_group': | ||||
r5095 | _("You do not have the permissions to store " | |||
"repository groups inside repository group `{}`"), | ||||
r1154 | 'permission_denied_root': | |||
r5095 | _("You do not have the permission to store " | |||
"repository groups in the root location.") | ||||
r1154 | } | |||
value = value['repo_group_name'] | ||||
parent_group_name = value | ||||
is_root_location = value is types.RootLocation | ||||
# NOT initialized validators, we must call them | ||||
can_create_repo_groups_at_root = HasPermissionAny( | ||||
'hg.admin', 'hg.repogroup.create.true') | ||||
if is_root_location: | ||||
if can_create_repo_groups_at_root(user=request_user): | ||||
# we can create repo group inside tool-level. No more checks | ||||
# are required | ||||
return | ||||
else: | ||||
raise colander.Invalid(node, messages['permission_denied_root']) | ||||
# check if the parent repo group actually exists | ||||
parent_group = None | ||||
if parent_group_name: | ||||
parent_group = RepoGroupModel().get_by_group_name(parent_group_name) | ||||
if value and not parent_group: | ||||
raise colander.Invalid( | ||||
node, messages['invalid_parent_repo_group'].format( | ||||
parent_group_name)) | ||||
# check if we have permissions to create new groups under | ||||
# parent repo group | ||||
# create repositories with write permission on group is set to true | ||||
create_on_write = HasPermissionAny( | ||||
'hg.create.write_on_repogroup.true')(user=request_user) | ||||
group_admin = HasRepoGroupPermissionAny('group.admin')( | ||||
parent_group_name, 'can write into group validator', user=request_user) | ||||
group_write = HasRepoGroupPermissionAny('group.write')( | ||||
parent_group_name, 'can write into group validator', user=request_user) | ||||
# creation by write access is currently disabled. Needs thinking if | ||||
# we want to allow this... | ||||
forbidden = not (group_admin or (group_write and create_on_write and 0)) | ||||
r4421 | old_name = old_values.get('group_name') | |||
if old_name and old_name == old_values.get('submitted_repo_group_name'): | ||||
# we're editing a repository group, we didn't change the name | ||||
# we skip the check for write into parent group now | ||||
# this allows changing settings for this repo group | ||||
return | ||||
r1154 | if parent_group and forbidden: | |||
r4421 | msg = messages['permission_denied_parent_group'].format(parent_group_name) | |||
r1154 | raise colander.Invalid(node, msg) | |||
return can_write_group_validator | ||||
@colander.deferred | ||||
def deferred_repo_group_owner_validator(node, kw): | ||||
def repo_owner_validator(node, value): | ||||
from rhodecode.model.db import User | ||||
r4706 | value = username_converter(value) | |||
r1154 | existing = User.get_by_username(value) | |||
if not existing: | ||||
r5095 | msg = _('Repo group owner with id `{}` does not exists').format( | |||
r1154 | value) | |||
raise colander.Invalid(node, msg) | ||||
return repo_owner_validator | ||||
@colander.deferred | ||||
def deferred_unique_name_validator(node, kw): | ||||
request_user = kw.get('user') | ||||
old_values = kw.get('old_values') or {} | ||||
def unique_name_validator(node, value): | ||||
from rhodecode.model.db import Repository, RepoGroup | ||||
name_changed = value != old_values.get('group_name') | ||||
existing = Repository.get_by_repo_name(value) | ||||
if name_changed and existing: | ||||
r5095 | msg = _('Repository with name `{}` already exists').format(value) | |||
r1154 | raise colander.Invalid(node, msg) | |||
existing_group = RepoGroup.get_by_group_name(value) | ||||
if name_changed and existing_group: | ||||
r5095 | msg = _('Repository group with name `{}` already exists').format( | |||
r1154 | value) | |||
raise colander.Invalid(node, msg) | ||||
return unique_name_validator | ||||
@colander.deferred | ||||
def deferred_repo_group_name_validator(node, kw): | ||||
return validators.valid_name_validator | ||||
r2175 | @colander.deferred | |||
def deferred_repo_group_validator(node, kw): | ||||
options = kw.get( | ||||
'repo_group_repo_group_options') | ||||
return colander.OneOf([x for x in options]) | ||||
@colander.deferred | ||||
def deferred_repo_group_widget(node, kw): | ||||
items = kw.get('repo_group_repo_group_items') | ||||
return deform.widget.Select2Widget(values=items) | ||||
r1154 | class GroupType(colander.Mapping): | |||
def _validate(self, node, value): | ||||
try: | ||||
return dict(repo_group_name=value) | ||||
except Exception as e: | ||||
raise colander.Invalid( | ||||
node, '"${val}" is not a mapping type: ${err}'.format( | ||||
val=value, err=e)) | ||||
def deserialize(self, node, cstruct): | ||||
if cstruct is colander.null: | ||||
return cstruct | ||||
r5095 | appstruct = super().deserialize(node, cstruct) | |||
r1154 | validated_name = appstruct['repo_group_name'] | |||
# inject group based on once deserialized data | ||||
(repo_group_name_without_group, | ||||
parent_group_name, | ||||
parent_group) = get_group_and_repo(validated_name) | ||||
r2241 | appstruct['repo_group_name_with_group'] = validated_name | |||
r1154 | appstruct['repo_group_name_without_group'] = repo_group_name_without_group | |||
appstruct['repo_group_name'] = parent_group_name or types.RootLocation | ||||
if parent_group: | ||||
appstruct['repo_group_id'] = parent_group.group_id | ||||
return appstruct | ||||
class GroupSchema(colander.SchemaNode): | ||||
schema_type = GroupType | ||||
validator = deferred_can_write_to_group_validator | ||||
missing = colander.null | ||||
class RepoGroup(GroupSchema): | ||||
repo_group_name = colander.SchemaNode( | ||||
types.GroupNameType()) | ||||
repo_group_id = colander.SchemaNode( | ||||
colander.String(), missing=None) | ||||
repo_group_name_without_group = colander.SchemaNode( | ||||
colander.String(), missing=None) | ||||
class RepoGroupAccessSchema(colander.MappingSchema): | ||||
repo_group = RepoGroup() | ||||
class RepoGroupNameUniqueSchema(colander.MappingSchema): | ||||
unique_repo_group_name = colander.SchemaNode( | ||||
colander.String(), | ||||
validator=deferred_unique_name_validator) | ||||
r523 | class RepoGroupSchema(colander.Schema): | |||
r1154 | ||||
repo_group_name = colander.SchemaNode( | ||||
types.GroupNameType(), | ||||
validator=deferred_repo_group_name_validator) | ||||
repo_group_owner = colander.SchemaNode( | ||||
colander.String(), | ||||
validator=deferred_repo_group_owner_validator) | ||||
repo_group_description = colander.SchemaNode( | ||||
r2175 | colander.String(), missing='', widget=deform.widget.TextAreaWidget()) | |||
r1154 | ||||
repo_group_copy_permissions = colander.SchemaNode( | ||||
types.StringBooleanType(), | ||||
r2175 | missing=False, widget=deform.widget.CheckboxWidget()) | |||
r1154 | ||||
repo_group_enable_locking = colander.SchemaNode( | ||||
types.StringBooleanType(), | ||||
r2175 | missing=False, widget=deform.widget.CheckboxWidget()) | |||
r1154 | ||||
def deserialize(self, cstruct): | ||||
""" | ||||
Custom deserialize that allows to chain validation, and verify | ||||
permissions, and as last step uniqueness | ||||
""" | ||||
r5095 | appstruct = super().deserialize(cstruct) | |||
r1154 | validated_name = appstruct['repo_group_name'] | |||
# second pass to validate permissions to repo_group | ||||
r4421 | if 'old_values' in self.bindings: | |||
# save current repo name for name change checks | ||||
self.bindings['old_values']['submitted_repo_group_name'] = validated_name | ||||
r1154 | second = RepoGroupAccessSchema().bind(**self.bindings) | |||
appstruct_second = second.deserialize({'repo_group': validated_name}) | ||||
# save result | ||||
appstruct['repo_group'] = appstruct_second['repo_group'] | ||||
# thirds to validate uniqueness | ||||
third = RepoGroupNameUniqueSchema().bind(**self.bindings) | ||||
third.deserialize({'unique_repo_group_name': validated_name}) | ||||
return appstruct | ||||
r2175 | ||||
class RepoGroupSettingsSchema(RepoGroupSchema): | ||||
repo_group = colander.SchemaNode( | ||||
colander.Integer(), | ||||
validator=deferred_repo_group_validator, | ||||
widget=deferred_repo_group_widget, | ||||
missing='') | ||||
def deserialize(self, cstruct): | ||||
""" | ||||
Custom deserialize that allows to chain validation, and verify | ||||
permissions, and as last step uniqueness | ||||
""" | ||||
# first pass, to validate given data | ||||
appstruct = super(RepoGroupSchema, self).deserialize(cstruct) | ||||
validated_name = appstruct['repo_group_name'] | ||||
r2241 | # because of repoSchema adds repo-group as an ID, we inject it as | |||
# full name here because validators require it, it's unwrapped later | ||||
# so it's safe to use and final name is going to be without group anyway | ||||
group, separator = get_repo_group(appstruct['repo_group']) | ||||
if group: | ||||
validated_name = separator.join([group.group_name, validated_name]) | ||||
r2175 | # second pass to validate permissions to repo_group | |||
r4421 | if 'old_values' in self.bindings: | |||
# save current repo name for name change checks | ||||
self.bindings['old_values']['submitted_repo_group_name'] = validated_name | ||||
r2175 | second = RepoGroupAccessSchema().bind(**self.bindings) | |||
appstruct_second = second.deserialize({'repo_group': validated_name}) | ||||
# save result | ||||
appstruct['repo_group'] = appstruct_second['repo_group'] | ||||
# thirds to validate uniqueness | ||||
third = RepoGroupNameUniqueSchema().bind(**self.bindings) | ||||
third.deserialize({'unique_repo_group_name': validated_name}) | ||||
return appstruct | ||||