forked from Github-Mirrors/canaille
refactor: user permissions lazy loading
This commit is contained in:
parent
30bd71c5b5
commit
1fbb074cc5
9 changed files with 47 additions and 61 deletions
|
@ -78,7 +78,7 @@ def permissions_needed(*args):
|
||||||
@wraps(view_function)
|
@wraps(view_function)
|
||||||
def decorator(*args, **kwargs):
|
def decorator(*args, **kwargs):
|
||||||
user = current_user()
|
user = current_user()
|
||||||
if not user or not permissions.issubset(user._permissions):
|
if not user or not user.can(*permissions):
|
||||||
abort(403)
|
abort(403)
|
||||||
return view_function(*args, user=user, **kwargs)
|
return view_function(*args, user=user, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -61,14 +61,6 @@ class User(canaille.core.models.User, LDAPObject):
|
||||||
|
|
||||||
return super().match_filter(filter)
|
return super().match_filter(filter)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, *args, **kwargs):
|
|
||||||
user = super().get(*args, **kwargs)
|
|
||||||
if user:
|
|
||||||
user.load_permissions()
|
|
||||||
|
|
||||||
return user
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
return self.rdn_value
|
return self.rdn_value
|
||||||
|
@ -123,10 +115,6 @@ class User(canaille.core.models.User, LDAPObject):
|
||||||
password.encode("utf-8"),
|
password.encode("utf-8"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def reload(self):
|
|
||||||
super().reload()
|
|
||||||
self.load_permissions()
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
group_attr = self.python_attribute_to_ldap("groups")
|
group_attr = self.python_attribute_to_ldap("groups")
|
||||||
new_groups = self.changes.get(group_attr)
|
new_groups = self.changes.get(group_attr)
|
||||||
|
|
|
@ -244,14 +244,6 @@ class User(canaille.core.models.User, MemoryModel):
|
||||||
"groups": ("Group", "members"),
|
"groups": ("Group", "members"),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.load_permissions()
|
|
||||||
|
|
||||||
def reload(self):
|
|
||||||
super().reload()
|
|
||||||
self.load_permissions()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_from_login(cls, login=None, **kwargs):
|
def get_from_login(cls, login=None, **kwargs):
|
||||||
return User.get(user_name=login)
|
return User.get(user_name=login)
|
||||||
|
|
|
@ -14,7 +14,6 @@ from sqlalchemy import or_
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Mapped
|
from sqlalchemy.orm import Mapped
|
||||||
from sqlalchemy.orm import mapped_column
|
from sqlalchemy.orm import mapped_column
|
||||||
from sqlalchemy.orm import reconstructor
|
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy_json import MutableJson
|
from sqlalchemy_json import MutableJson
|
||||||
from sqlalchemy_utils import PasswordType
|
from sqlalchemy_utils import PasswordType
|
||||||
|
@ -172,18 +171,6 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel):
|
||||||
TZDateTime(timezone=True), nullable=True
|
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
|
@classmethod
|
||||||
def get_from_login(cls, login=None, **kwargs):
|
def get_from_login(cls, login=None, **kwargs):
|
||||||
return User.get(user_name=login)
|
return User.get(user_name=login)
|
||||||
|
|
|
@ -143,7 +143,7 @@ def about():
|
||||||
def users(user):
|
def users(user):
|
||||||
table_form = TableForm(
|
table_form = TableForm(
|
||||||
models.User,
|
models.User,
|
||||||
fields=user._readable_fields | user._writable_fields,
|
fields=user.readable_fields | user.writable_fields,
|
||||||
formdata=request.form,
|
formdata=request.form,
|
||||||
)
|
)
|
||||||
if request.form and not table_form.validate():
|
if request.form and not table_form.validate():
|
||||||
|
@ -404,7 +404,7 @@ def email_confirmation(data, hash):
|
||||||
@bp.route("/profile", methods=("GET", "POST"))
|
@bp.route("/profile", methods=("GET", "POST"))
|
||||||
@permissions_needed("manage_users")
|
@permissions_needed("manage_users")
|
||||||
def profile_creation(user):
|
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)
|
form.process(CombinedMultiDict((request.files, request.form)) or None)
|
||||||
|
|
||||||
for field in form:
|
for field in form:
|
||||||
|
@ -458,8 +458,6 @@ def profile_create(current_app, form):
|
||||||
user.set_password(form["password1"].data)
|
user.set_password(form["password1"].data)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
user.load_permissions()
|
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@ -488,8 +486,8 @@ def profile_edition_main_form(user, edited_user, emails_readonly):
|
||||||
if emails_readonly:
|
if emails_readonly:
|
||||||
available_fields.remove("emails")
|
available_fields.remove("emails")
|
||||||
|
|
||||||
readable_fields = user._readable_fields & available_fields
|
readable_fields = user.readable_fields & available_fields
|
||||||
writable_fields = user._writable_fields & available_fields
|
writable_fields = user.writable_fields & available_fields
|
||||||
data = {
|
data = {
|
||||||
field: getattr(edited_user, field)
|
field: getattr(edited_user, field)
|
||||||
for field in writable_fields | readable_fields
|
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):
|
def profile_edition_main_form_validation(user, edited_user, profile_form):
|
||||||
for field in 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):
|
if isinstance(field, wtforms.FieldList):
|
||||||
# too bad wtforms cannot sanitize the list itself
|
# too bad wtforms cannot sanitize the list itself
|
||||||
data = [value for value in field.data if value] or None
|
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):
|
def profile_settings_edit(editor, edited_user):
|
||||||
menuitem = "profile" if editor.id == editor.id else "users"
|
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"}
|
available_fields = {"password", "groups", "user_name", "lock_date"}
|
||||||
data = {
|
data = {
|
||||||
|
@ -758,8 +756,8 @@ def profile_settings_edit(editor, edited_user):
|
||||||
data["groups"] = [g.id for g in edited_user.groups]
|
data["groups"] = [g.id for g in edited_user.groups]
|
||||||
|
|
||||||
form = build_profile_form(
|
form = build_profile_form(
|
||||||
editor._writable_fields & available_fields,
|
editor.writable_fields & available_fields,
|
||||||
editor._readable_fields & available_fields,
|
editor.readable_fields & available_fields,
|
||||||
edited_user,
|
edited_user,
|
||||||
)
|
)
|
||||||
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
|
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
|
||||||
|
@ -774,7 +772,7 @@ def profile_settings_edit(editor, edited_user):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for attribute in form:
|
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)
|
setattr(edited_user, attribute.name, attribute.data)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -239,11 +239,9 @@ class User(Model):
|
||||||
lock_date: Optional[datetime.datetime] = None
|
lock_date: Optional[datetime.datetime] = None
|
||||||
"""A DateTime indicating when the resource was locked."""
|
"""A DateTime indicating when the resource was locked."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
_readable_fields = None
|
||||||
self._readable_fields = set()
|
_writable_fields = None
|
||||||
self._writable_fields = set()
|
_permissions = None
|
||||||
self._permissions = set()
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_from_login(cls, login=None, **kwargs) -> Optional["User"]:
|
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
|
return self.emails[0] if self.emails else None
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if name.startswith("can_") and name != "can_read":
|
prefix = "can_"
|
||||||
permission = name[4:]
|
if name.startswith(prefix) and name != "can_read":
|
||||||
return permission in self._permissions
|
return self.can(name[len(prefix) :])
|
||||||
|
|
||||||
return super().__getattr__(name)
|
return super().__getattr__(name)
|
||||||
|
|
||||||
|
def can(self, *permissions):
|
||||||
|
if self._permissions is None:
|
||||||
|
self.load_permissions()
|
||||||
|
|
||||||
|
return set(permissions).issubset(self._permissions)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locked(self) -> bool:
|
def locked(self) -> bool:
|
||||||
"""Wether the user account has been locked or has expired."""
|
"""Wether the user account has been locked or has expired."""
|
||||||
|
@ -293,6 +297,26 @@ class User(Model):
|
||||||
self._readable_fields |= set(details["READ"])
|
self._readable_fields |= set(details["READ"])
|
||||||
self._writable_fields |= set(details["WRITE"])
|
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):
|
class Group(Model):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -16,14 +16,14 @@
|
||||||
render_func=render_field,
|
render_func=render_field,
|
||||||
**kwargs
|
**kwargs
|
||||||
) }}
|
) }}
|
||||||
{% elif field.name in edited_user._writable_fields %}
|
{% elif field.name in edited_user.writable_fields %}
|
||||||
{{ fui.render_field(
|
{{ fui.render_field(
|
||||||
field,
|
field,
|
||||||
user=user,
|
user=user,
|
||||||
render_func=render_field,
|
render_func=render_field,
|
||||||
**kwargs
|
**kwargs
|
||||||
) }}
|
) }}
|
||||||
{% elif field.name in edited_user._readable_fields %}
|
{% elif field.name in edited_user.readable_fields %}
|
||||||
{{ fui.render_field(
|
{{ fui.render_field(
|
||||||
field,
|
field,
|
||||||
user=user,
|
user=user,
|
||||||
|
|
|
@ -24,9 +24,9 @@
|
||||||
{% set lock_indicator = field.render_kw and ("readonly" in field.render_kw or "disabled" in field.render_kw) %}
|
{% 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 %}
|
{% if edited_user.user_name == user.user_name or lock_indicator or noindicator %}
|
||||||
{{ fui.render_field(field, **kwargs) }}
|
{{ 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) }}
|
{{ 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) }}
|
{{ fui.render_field(field, indicator_icon="eye", indicator_text=_("This user cannot edit this field"), **kwargs) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ fui.render_field(field, indicator_icon="eye slash", indicator_text=_("This user cannot see this field"), **kwargs) }}
|
{{ 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 right aligned container">
|
||||||
<div class="ui stackable buttons">
|
<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>
|
<button type="submit" class="ui right floated basic negative button confirm" name="action" value="confirm-lock" id="lock" formnovalidate>
|
||||||
{% trans %}Lock the account{% endtrans %}
|
{% trans %}Lock the account{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -162,7 +162,6 @@ def user(app, backend):
|
||||||
formatted_address="1235, somewhere",
|
formatted_address="1235, somewhere",
|
||||||
)
|
)
|
||||||
u.save()
|
u.save()
|
||||||
u.load_permissions()
|
|
||||||
yield u
|
yield u
|
||||||
u.delete()
|
u.delete()
|
||||||
|
|
||||||
|
@ -177,7 +176,6 @@ def admin(app, backend):
|
||||||
password="admin",
|
password="admin",
|
||||||
)
|
)
|
||||||
u.save()
|
u.save()
|
||||||
u.load_permissions()
|
|
||||||
yield u
|
yield u
|
||||||
u.delete()
|
u.delete()
|
||||||
|
|
||||||
|
@ -192,7 +190,6 @@ def moderator(app, backend):
|
||||||
password="moderator",
|
password="moderator",
|
||||||
)
|
)
|
||||||
u.save()
|
u.save()
|
||||||
u.load_permissions()
|
|
||||||
yield u
|
yield u
|
||||||
u.delete()
|
u.delete()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue