refactor: user permissions lazy loading

This commit is contained in:
Éloi Rivard 2024-04-07 15:21:32 +02:00
parent 30bd71c5b5
commit 1fbb074cc5
No known key found for this signature in database
GPG key ID: 7EDA204EA57DD184
9 changed files with 47 additions and 61 deletions

View file

@ -78,7 +78,7 @@ def permissions_needed(*args):
@wraps(view_function)
def decorator(*args, **kwargs):
user = current_user()
if not user or not permissions.issubset(user._permissions):
if not user or not user.can(*permissions):
abort(403)
return view_function(*args, user=user, **kwargs)

View file

@ -61,14 +61,6 @@ class User(canaille.core.models.User, LDAPObject):
return super().match_filter(filter)
@classmethod
def get(cls, *args, **kwargs):
user = super().get(*args, **kwargs)
if user:
user.load_permissions()
return user
@property
def identifier(self):
return self.rdn_value
@ -123,10 +115,6 @@ class User(canaille.core.models.User, LDAPObject):
password.encode("utf-8"),
)
def reload(self):
super().reload()
self.load_permissions()
def save(self, *args, **kwargs):
group_attr = self.python_attribute_to_ldap("groups")
new_groups = self.changes.get(group_attr)

View file

@ -244,14 +244,6 @@ class User(canaille.core.models.User, MemoryModel):
"groups": ("Group", "members"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_permissions()
def reload(self):
super().reload()
self.load_permissions()
@classmethod
def get_from_login(cls, login=None, **kwargs):
return User.get(user_name=login)

View file

@ -14,7 +14,6 @@ from sqlalchemy import or_
from sqlalchemy import select
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import reconstructor
from sqlalchemy.orm import relationship
from sqlalchemy_json import MutableJson
from sqlalchemy_utils import PasswordType
@ -172,18 +171,6 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel):
TZDateTime(timezone=True), nullable=True
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_permissions()
def reload(self):
super().reload()
self.load_permissions()
@reconstructor
def load_permissions(self):
super().load_permissions()
@classmethod
def get_from_login(cls, login=None, **kwargs):
return User.get(user_name=login)

View file

@ -143,7 +143,7 @@ def about():
def users(user):
table_form = TableForm(
models.User,
fields=user._readable_fields | user._writable_fields,
fields=user.readable_fields | user.writable_fields,
formdata=request.form,
)
if request.form and not table_form.validate():
@ -404,7 +404,7 @@ def email_confirmation(data, hash):
@bp.route("/profile", methods=("GET", "POST"))
@permissions_needed("manage_users")
def profile_creation(user):
form = build_profile_form(user._writable_fields, user._readable_fields)
form = build_profile_form(user.writable_fields, user.readable_fields)
form.process(CombinedMultiDict((request.files, request.form)) or None)
for field in form:
@ -458,8 +458,6 @@ def profile_create(current_app, form):
user.set_password(form["password1"].data)
user.save()
user.load_permissions()
return user
@ -488,8 +486,8 @@ def profile_edition_main_form(user, edited_user, emails_readonly):
if emails_readonly:
available_fields.remove("emails")
readable_fields = user._readable_fields & available_fields
writable_fields = user._writable_fields & available_fields
readable_fields = user.readable_fields & available_fields
writable_fields = user.writable_fields & available_fields
data = {
field: getattr(edited_user, field)
for field in writable_fields | readable_fields
@ -509,7 +507,7 @@ def profile_edition_main_form(user, edited_user, emails_readonly):
def profile_edition_main_form_validation(user, edited_user, profile_form):
for field in profile_form:
if field.name in edited_user.attributes and field.name in user._writable_fields:
if field.name in edited_user.attributes and field.name in user.writable_fields:
if isinstance(field, wtforms.FieldList):
# too bad wtforms cannot sanitize the list itself
data = [value for value in field.data if value] or None
@ -744,7 +742,7 @@ def profile_settings(user, edited_user):
def profile_settings_edit(editor, edited_user):
menuitem = "profile" if editor.id == editor.id else "users"
fields = editor._readable_fields | editor._writable_fields
fields = editor.readable_fields | editor.writable_fields
available_fields = {"password", "groups", "user_name", "lock_date"}
data = {
@ -758,8 +756,8 @@ def profile_settings_edit(editor, edited_user):
data["groups"] = [g.id for g in edited_user.groups]
form = build_profile_form(
editor._writable_fields & available_fields,
editor._readable_fields & available_fields,
editor.writable_fields & available_fields,
editor.readable_fields & available_fields,
edited_user,
)
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
@ -774,7 +772,7 @@ def profile_settings_edit(editor, edited_user):
else:
for attribute in form:
if attribute.name in available_fields & editor._writable_fields:
if attribute.name in available_fields & editor.writable_fields:
setattr(edited_user, attribute.name, attribute.data)
if (

View file

@ -239,11 +239,9 @@ class User(Model):
lock_date: Optional[datetime.datetime] = None
"""A DateTime indicating when the resource was locked."""
def __init__(self, *args, **kwargs):
self._readable_fields = set()
self._writable_fields = set()
self._permissions = set()
super().__init__(*args, **kwargs)
_readable_fields = None
_writable_fields = None
_permissions = None
@classmethod
def get_from_login(cls, login=None, **kwargs) -> Optional["User"]:
@ -269,12 +267,18 @@ class User(Model):
return self.emails[0] if self.emails else None
def __getattr__(self, name):
if name.startswith("can_") and name != "can_read":
permission = name[4:]
return permission in self._permissions
prefix = "can_"
if name.startswith(prefix) and name != "can_read":
return self.can(name[len(prefix) :])
return super().__getattr__(name)
def can(self, *permissions):
if self._permissions is None:
self.load_permissions()
return set(permissions).issubset(self._permissions)
@property
def locked(self) -> bool:
"""Wether the user account has been locked or has expired."""
@ -293,6 +297,26 @@ class User(Model):
self._readable_fields |= set(details["READ"])
self._writable_fields |= set(details["WRITE"])
def reload(self):
self._readable = None
self._writable = None
self._permissions = None
super().reload()
@property
def readable_fields(self):
if self._writable_fields is None:
self.load_permissions()
return self._readable_fields
@property
def writable_fields(self):
if self._writable_fields is None:
self.load_permissions()
return self._writable_fields
class Group(Model):
"""

View file

@ -16,14 +16,14 @@
render_func=render_field,
**kwargs
) }}
{% elif field.name in edited_user._writable_fields %}
{% elif field.name in edited_user.writable_fields %}
{{ fui.render_field(
field,
user=user,
render_func=render_field,
**kwargs
) }}
{% elif field.name in edited_user._readable_fields %}
{% elif field.name in edited_user.readable_fields %}
{{ fui.render_field(
field,
user=user,

View file

@ -24,9 +24,9 @@
{% set lock_indicator = field.render_kw and ("readonly" in field.render_kw or "disabled" in field.render_kw) %}
{% if edited_user.user_name == user.user_name or lock_indicator or noindicator %}
{{ fui.render_field(field, **kwargs) }}
{% elif field.name in edited_user._writable_fields %}
{% elif field.name in edited_user.writable_fields %}
{{ fui.render_field(field, **kwargs) }}
{% elif field.name in edited_user._readable_fields %}
{% elif field.name in edited_user.readable_fields %}
{{ fui.render_field(field, indicator_icon="eye", indicator_text=_("This user cannot edit this field"), **kwargs) }}
{% else %}
{{ fui.render_field(field, indicator_icon="eye slash", indicator_text=_("This user cannot see this field"), **kwargs) }}
@ -140,7 +140,7 @@
<div class="ui right aligned container">
<div class="ui stackable buttons">
{% if has_account_lockability and "lock_date" in user._writable_fields and not edited_user.locked %}
{% if has_account_lockability and "lock_date" in user.writable_fields and not edited_user.locked %}
<button type="submit" class="ui right floated basic negative button confirm" name="action" value="confirm-lock" id="lock" formnovalidate>
{% trans %}Lock the account{% endtrans %}
</button>

View file

@ -162,7 +162,6 @@ def user(app, backend):
formatted_address="1235, somewhere",
)
u.save()
u.load_permissions()
yield u
u.delete()
@ -177,7 +176,6 @@ def admin(app, backend):
password="admin",
)
u.save()
u.load_permissions()
yield u
u.delete()
@ -192,7 +190,6 @@ def moderator(app, backend):
password="moderator",
)
u.save()
u.load_permissions()
yield u
u.delete()