From a67b8d25a1c6453a1753ec3eafc55d13c3e64a15 Mon Sep 17 00:00:00 2001 From: UlrichB22 <97119703+UlrichB22@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:07:24 +0200 Subject: [PATCH 1/2] move 'user.may.write' and 'destroy' call from template to views.py --- src/moin/apps/frontend/views.py | 47 +++++++++++++++++++++-- src/moin/items/__init__.py | 6 ++- src/moin/items/blog.py | 5 +++ src/moin/security/__init__.py | 9 +++-- src/moin/storage/middleware/protecting.py | 14 +++++-- src/moin/templates/history.html | 6 +-- src/moin/templates/itemviews.html | 8 ++-- src/moin/templates/show.html | 4 +- 8 files changed, 76 insertions(+), 23 deletions(-) diff --git a/src/moin/apps/frontend/views.py b/src/moin/apps/frontend/views.py index 8a4020501..f8cd0b230 100644 --- a/src/moin/apps/frontend/views.py +++ b/src/moin/apps/frontend/views.py @@ -565,6 +565,17 @@ def flash_if_item_deleted(item_name, rev_id, itemrev): return False +def get_item_permissions(fqname, item): + """ + get users permissions for item + Return a dict with permission type and permission + """ + permission_list = ["write", "create", "destroy"] + user = flaskg.user.may.names + item_may = dict([(key, flaskg.storage.may(fqname, key, usernames=user, item=item)) for key in permission_list]) + return item_may + + # The first form accepts POST to allow modifying behavior like modify_item. # The second form only accepts GET since modifying a historical revision is not allowed. @frontend.route("/", defaults=dict(rev=CURRENT), methods=["GET", "POST"]) @@ -579,7 +590,8 @@ def show_item(item_name, rev): item = Item.create(item_name, rev_id=rev) flaskg.user.add_trail(item_name) item_is_deleted = flash_if_item_deleted(item_name, rev, item) - result = item.do_show(rev, item_is_deleted=item_is_deleted) + item_may = get_item_permissions(fqname, item) + result = item.do_show(rev, item_is_deleted=item_is_deleted, item_may=item_may) except AccessDenied: abort(403) except FieldNotUniqueError: @@ -633,6 +645,7 @@ def indexable(item_name, rev): def highlight_item(item): rev_navigation_ids_dates = rev_navigation.prior_next_revs(request.view_args["rev"], item.fqname) item_is_deleted = flash_if_item_deleted(item.fqname.fullname, item.rev.meta[REVID], item) + item_may = get_item_permissions(item.fqname, item) try: ret = render_template( "highlight.html", @@ -644,6 +657,7 @@ def highlight_item(item): rev_navigation_ids_dates=rev_navigation_ids_dates, meta=item._meta_info(), item_is_deleted=item_is_deleted, + may=item_may, ) except UnicodeDecodeError: return _crash(item, None, None) @@ -655,6 +669,7 @@ def highlight_item(item): def show_item_meta(item): rev_navigation_ids_dates = rev_navigation.prior_next_revs(request.view_args["rev"], item.fqname) item_is_deleted = flash_if_item_deleted(item.fqname.fullname, item.rev.meta[REVID], item) + item_may = get_item_permissions(item.fqname, item) ret = render_template( "meta.html", item=item, @@ -665,6 +680,7 @@ def show_item_meta(item): rev_navigation_ids_dates=rev_navigation_ids_dates, meta=item._meta_info(), item_is_deleted=item_is_deleted, + may=item_may, ) close_file(item.meta.revision.data) return ret @@ -742,10 +758,16 @@ def convert_item(item_name): abort(403) if isinstance(item, NonExistent): abort(404, item_name) + item_may = get_item_permissions(item_name, item) form = ConvertForm.from_flat(request.form) if request.method in ["GET", "HEAD"]: return render_template( - "convert.html", item=item, form=form, contenttype=item.contenttype, fqname=split_fqname(item_name) + "convert.html", + item=item, + form=form, + contenttype=item.contenttype, + fqname=split_fqname(item_name), + may=item_may, ) item.rev.data.seek(0) @@ -831,8 +853,9 @@ def modify_item(item_name): abort(403) if not flaskg.user.may.write(item_name): abort(403) + item_may = get_item_permissions(item_name, item) try: - ret = item.do_modify() + ret = item.do_modify(item_may=item_may) except ValueError as err: # user may have changed or deleted namespace, contenttype... causing meta data validation failure # or data unicode validation failed @@ -951,6 +974,7 @@ def rename_item(item_name): abort(403) if isinstance(item, NonExistent): abort(404, item_name) + item_may = get_item_permissions(item_name, item) subitem_names = [] if request.method in ["GET", "HEAD"]: form = RenameItemForm.from_defaults() @@ -988,6 +1012,7 @@ def rename_item(item_name): form=form, data_rendered=Markup(item.content._render_data()), len=len, + may=item_may, ) close_file(item.meta.revision.data) return ret @@ -1003,6 +1028,7 @@ def delete_item(item_name): abort(403) if isinstance(item, NonExistent): abort(404, item_name) + item_may = get_item_permissions(item_name, item) subitem_names = [] if request.method in ["GET", "HEAD"]: form = DeleteItemForm.from_defaults() @@ -1032,6 +1058,7 @@ def delete_item(item_name): fqname=split_fqname(item_name), form=form, data_rendered=data_rendered, + may=item_may, ) close_file(item.rev.data) return ret @@ -1232,6 +1259,7 @@ def destroy_item(item_name, rev): abort(403) if isinstance(item, NonExistent): abort(404, fqname.fullname) + item_may = get_item_permissions(fqname, item) item_is_deleted = flash_if_item_deleted(item_name, rev, item) subitem_names = [] alias_names = [] @@ -1274,6 +1302,7 @@ def destroy_item(item_name, rev): form=form, data_rendered=Markup(item.content._render_data()), item_is_deleted=item_is_deleted, + may=item_may, ) close_file(item.meta.revision.data) close_file(item.rev.data) @@ -1411,6 +1440,7 @@ def name_initial(files, uppercase=False, lowercase=False): item = Item.create(item_name) # when item_name='', it gives toplevel index except AccessDenied: abort(403) + item_may = get_item_permissions(item_name, item) # request.args is a MultiDict instance, which degenerates into a normal # single-valued dict on most occasions (making the first value the *only* @@ -1488,6 +1518,7 @@ def name_initial(files, uppercase=False, lowercase=False): selected_groups=selected_groups, str=str, app=app, + may=item_may, ) @@ -1572,12 +1603,14 @@ def forwardrefs(item_name): :returns: a page with all the items linked from this item """ refs = _forwardrefs(item_name) + item_may = get_item_permissions(item_name, None) return render_template( "link_list_item_panel.html", item_name=item_name, fqname=split_fqname(item_name), headline=_("Items that are referred by '{item_name}'").format(item_name=shorten_item_id(item_name)), fq_names=split_fqname_list(refs), + may=item_may, ) @@ -1614,6 +1647,7 @@ def backrefs(item_name): except AccessDenied: abort(403) refs_here = _backrefs(item_name) + item_may = get_item_permissions(item_name, None) return render_template( "link_list_item_panel.html", item=item, @@ -1621,6 +1655,7 @@ def backrefs(item_name): fqname=split_fqname(item_name), headline=_("Items which refer to '{item_name}'").format(item_name=shorten_item_id(item_name)), fq_names=refs_here, + may=item_may, ) @@ -1650,6 +1685,7 @@ def history(item_name): abort(404, item_name) item_is_deleted = flash_if_item_deleted(item_name, CURRENT, item) + item_may = get_item_permissions(item_name, item) page_num = request.values.get("page_num", 1) page_num = max(int(page_num), 1) bookmark_time = int(request.values.get("bookmark", 0)) @@ -1712,6 +1748,7 @@ def history(item_name): len=len, trash=trash, item_is_deleted=item_is_deleted, + may=item_may, ) flaskg.clock.stop("renderrevs") close_file(item.rev.data) @@ -2808,6 +2845,7 @@ def similar_names(item_name): except AccessDenied: abort(403) fq_name = split_fqname(item_name) + item_may = get_item_permissions(fq_name, item) start, end, matches = find_matches(fq_name) keys = sorted(matches.keys()) # TODO later we could add titles for the misc ranks: @@ -2829,6 +2867,7 @@ def similar_names(item_name): item_name=item_name, # XXX no item fqname=split_fqname(item_name), fq_names=fq_names, + may=item_may, ) @@ -2849,6 +2888,7 @@ def sitemap(item_name): abort(403) if isinstance(item, NonExistent): abort(404, item_name) + item_may = get_item_permissions(item_name, item) backrefs, junk, junk2 = NestedItemListBuilder().recurse_build([fq_name], backrefs=True) del backrefs[0] # don't show current item name as sole toplevel list item @@ -2863,6 +2903,7 @@ def sitemap(item_name): fqname=fq_name, no_read_auth=no_read_auth, missing=missing, + may=item_may, ) diff --git a/src/moin/items/__init__.py b/src/moin/items/__init__.py index 7af704308..d177ba0aa 100644 --- a/src/moin/items/__init__.py +++ b/src/moin/items/__init__.py @@ -1436,7 +1436,7 @@ def _do_modify_show_templates(self): data_rendered="", ) - def do_show(self, revid, item_is_deleted=False): + def do_show(self, revid, item_is_deleted=False, item_may=None): """ Display an item. If this is not the current revision, page content will be prefaced with links to the next-rev and prior-rev. @@ -1459,6 +1459,7 @@ def do_show(self, revid, item_is_deleted=False): data_rendered=Markup(self.content._render_data()), html_head_meta=html_head_meta, item_is_deleted=item_is_deleted, + may=item_may, ) def doc_link(self, content_name, link_text): @@ -1513,7 +1514,7 @@ def meta_changed(self, meta): return True return False - def do_modify(self): + def do_modify(self, item_may=None): if isinstance(self.content, NonExistentContent) and not flaskg.user.may.create(self.fqname): abort( 403, @@ -1737,6 +1738,7 @@ def do_modify(self): draft_data=draft_data, lock_duration=lock_duration, tuple=tuple, + may=item_may, ) diff --git a/src/moin/items/blog.py b/src/moin/items/blog.py index 500c478cb..9aa44a877 100644 --- a/src/moin/items/blog.py +++ b/src/moin/items/blog.py @@ -15,6 +15,7 @@ from whoosh.query import Term, And, Prefix from whoosh.sorting import FunctionFacet +from moin.apps.frontend.views import get_item_permissions from moin.i18n import L_ from moin.themes import render_template from moin.forms import Text, Tags, DateTime @@ -87,6 +88,7 @@ def ptime_sort_key(searcher, docnum): revs = flaskg.storage.search(query, sortedby=ptime_sort_facet, reverse=True, limit=None) blog_entry_items = [Item.create(rev.name, rev_id=rev.revid) for rev in revs] + item_may = get_item_permissions(self.name, self) return render_template( "blog/main.html", item_name=self.name, @@ -95,6 +97,7 @@ def ptime_sort_key(searcher, docnum): blog_entry_items=blog_entry_items, tag=tag, item=self, + may=item_may, ) @@ -126,6 +129,7 @@ def do_show(self, revid, **kwargs): if not isinstance(blog_item, Blog): # The parent item of this blog entry item is not a Blog item. abort(403) + item_may = get_item_permissions(blog_item_name, blog_item) return render_template( "blog/entry.html", item_name=self.name, @@ -133,4 +137,5 @@ def do_show(self, revid, **kwargs): blog_item=blog_item, blog_entry_item=self, item=self, + may=item_may, ) diff --git a/src/moin/security/__init__.py b/src/moin/security/__init__.py index 6390af714..960566e18 100644 --- a/src/moin/security/__init__.py +++ b/src/moin/security/__init__.py @@ -3,6 +3,7 @@ # Copyright: 2003 Gustavo Niemeyer # Copyright: 2005 Oliver Graf # Copyright: 2007 Alexander Schremmer +# Copyright: 2024 MoinMoin:UlrichB # License: GNU GPL v2 (or any later version), see LICENSE.txt for details. """ @@ -71,17 +72,17 @@ def read(self, itemname): def __init__(self, user): self.names = user.name - def read(self, itemname): + def read(self, itemname, item=None): """read permission is special as we have 2 kinds of read capabilities: * READ - gives permission to read, unconditionally * PUBREAD - gives permission to read, when published """ - return flaskg.storage.may(itemname, rights.READ, usernames=self.names) or flaskg.storage.may( - itemname, rights.PUBREAD, usernames=self.names + return flaskg.storage.may(itemname, rights.READ, usernames=self.names, item=item) or flaskg.storage.may( + itemname, rights.PUBREAD, usernames=self.names, item=item ) - def __getattr__(self, attr): + def __getattr__(self, attr, item=None): """Shortcut to handle all known ACL rights. if attr is a valid acl right, return a checking function for it. diff --git a/src/moin/storage/middleware/protecting.py b/src/moin/storage/middleware/protecting.py index 60d147610..b2b7bdb5d 100644 --- a/src/moin/storage/middleware/protecting.py +++ b/src/moin/storage/middleware/protecting.py @@ -1,4 +1,5 @@ # Copyright: 2011 MoinMoin:ThomasWaldmann +# Copyright: 2024 MoinMoin:UlrichB # License: GNU GPL v2 (or any later version), see LICENSE.txt for details. """ @@ -22,7 +23,6 @@ from moin.security import AccessControlList from moin.utils import close_file from moin.utils.interwiki import split_fqname, CompositeName - from moin import log logging = log.getLogger(__name__) @@ -292,7 +292,7 @@ def existing_item(self, **query): item = self.indexer.existing_item(**query) return ProtectedItem(self, item) - def may(self, fqname, capability, usernames=None): + def may(self, fqname, capability, usernames=None, item=None): if usernames is not None and isinstance(usernames, (bytes, str)): # we got a single username (maybe bytes), make a list of str: if isinstance(usernames, bytes): @@ -302,7 +302,10 @@ def may(self, fqname, capability, usernames=None): fqname = fqname[0] if isinstance(fqname, list) else fqname if isinstance(fqname, str): fqname = split_fqname(fqname) - item = self.get_item(**fqname.query) + if item: + item = ProtectedItem(self, item) + else: + item = self.get_item(**fqname.query) allowed = item.allows(capability, user_names=usernames) return allowed @@ -353,7 +356,10 @@ def full_acls(self): including before/default/after acl. """ fqname = self.item.fqname - itemid = self.item.itemid + if hasattr(self.item, "itemid"): + itemid = self.item.itemid + else: + itemid = None acl_cfg = self.protector._get_configured_acls(fqname) before_acl = acl_cfg["before"] after_acl = acl_cfg["after"] diff --git a/src/moin/templates/history.html b/src/moin/templates/history.html index f4fb6e014..c6026633e 100644 --- a/src/moin/templates/history.html +++ b/src/moin/templates/history.html @@ -10,8 +10,6 @@ {% set summary = history[0].get('summary', None) if history else None %} {% set heading = _("History of {fqname}").format(fqname=utils.item_moniker(item.meta, [fqname])) %} {% set first_itemid = history[0].itemid %} -{% set user_may_write = user.may.write(fqname) %} -{% set user_may_destroy = user.may.destroy(fqname) %} {% block content %} {%- if history %} @@ -125,7 +123,7 @@

{{ heading }}

- {%- if user_may_write and not doc.trash -%} + {%- if may.write and not doc.trash -%} {%- if loop.first %} @@ -145,7 +143,7 @@

{{ heading }}

{%- else %} {%- endif %} - {%- if user_may_destroy -%} + {%- if may.destroy -%} {%- if loop.first %}
diff --git a/src/moin/templates/itemviews.html b/src/moin/templates/itemviews.html index 923e74b2e..01230aa5d 100644 --- a/src/moin/templates/itemviews.html +++ b/src/moin/templates/itemviews.html @@ -51,7 +51,7 @@ {%- endif %} - {%- if endpoint == 'frontend.modify_item' and user.may.write(fqname) and not_trash %} + {%- if endpoint == 'frontend.modify_item' and may.write and not_trash %}
  • {{ a_label(icon_class, label) }}
  • @@ -59,7 +59,7 @@ {%- if endpoint in [ 'frontend.rename_item', 'frontend.delete_item', - ] and user.may.write(fqname) and not_trash %} + ] and may.write and not_trash %}
  • {{ a_label(icon_class, label) }}
  • @@ -67,13 +67,13 @@ {%- if endpoint in [ 'frontend.convert_item', - ] and user.may.write(fqname) and item and theme_supp.is_markup_or_text(item.contenttype) and not_trash %} + ] and may.write and item and theme_supp.is_markup_or_text(item.contenttype) and not_trash %}
  • {{ a_label(icon_class, label) }}
  • {%- endif %} - {%- if endpoint == 'frontend.destroy_item' and user.may.destroy(fqname) %} + {%- if endpoint == 'frontend.destroy_item' and may.destroy %}
  • {{ a_label(icon_class, label) }}
  • diff --git a/src/moin/templates/show.html b/src/moin/templates/show.html index 64b80c4d9..5c48effd1 100644 --- a/src/moin/templates/show.html +++ b/src/moin/templates/show.html @@ -10,7 +10,7 @@ {{ super() }} {# universal edit button support #} - {%- if user.may.write(item_name) and 'frontend.modify_item' not in cfg.endpoints_excluded -%} + {%- if may.write and 'frontend.modify_item' not in cfg.endpoints_excluded -%} {%- endif %} {% endblock %} @@ -51,7 +51,7 @@ {% endblock %} {% block options_for_javascript %} - {%- if item_name and user.edit_on_doubleclick and user.may.write(item_name) and data_rendered and not form -%} + {%- if item_name and user.edit_on_doubleclick and may.write and data_rendered and not form -%}
    {%- endif %} {%- if user.show_comments -%} From 00391d392c552982b435e2a916a3d0b972595e0e Mon Sep 17 00:00:00 2001 From: UlrichB22 <97119703+UlrichB22@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:15:27 +0200 Subject: [PATCH 2/2] Fix traceback: add may in _do_modify_show_templates --- src/moin/items/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/moin/items/__init__.py b/src/moin/items/__init__.py index d177ba0aa..bc0d2ea01 100644 --- a/src/moin/items/__init__.py +++ b/src/moin/items/__init__.py @@ -1419,6 +1419,7 @@ def _do_modify_show_templates(self): itemtype=self.itemtype, contenttype=self.contenttype, template="", + may=None, ) ) return render_template( @@ -1434,6 +1435,7 @@ def _do_modify_show_templates(self): last_rev_id=rev_ids and rev_ids[-1], meta_rendered="", data_rendered="", + may=None, ) def do_show(self, revid, item_is_deleted=False, item_may=None):