You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2026/05/26 20:56:16 UTC
(allura) branch db/8607 created (now d54ba3c6b)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
at d54ba3c6b [#8607] solr: syntax problems deserve an escaped retry; don't expose underlying error messages
This branch includes the following new commits:
new 324d4de11 [#8607] escape html in a few places
new b35009fc6 [#8607] check for email code mode when verifying email link
new 879aa18a6 [#8607] test for oembed output security
new 10b5ec9bd [#8607] move webhook send from requests to urlopen so our NoInternal handlers run automatically including on redirects
new f2ca4b4c1 [#8607] test for zipdir symlink handling
new 8ab239574 [#8607] add comments, add test
new b77f66002 [#8607] check app_config_id in update_forums
new 523be8e9c [#8607] move |safe to be directly on the string source
new 2363e20f9 [#8607] check project when loading role_id inputs
new f9d88e1d1 [#8607] check mailbox owner
new 36609a862 [#8607] check before mailbox sub too
new a1ab533db [#8607] check subscribe here too
new f02a89b9d [#8607] replace random with secrets in some places
new 9cc8c33b2 [#8607] safer pwd comparison
new c8d8c67db [#8607] perm checks in markdown_to_html and fix wiki tool check
new 9a32ca331 [#8607] check client_id when making bearer token
new 1d4f5fce0 [#8607] test for save_attachments (export) path handling
new 273ef2ff5 [#8607] harden attachment upload
new 3c245d4ca [#8607] add X-Content-Type-Options header
new b4264a414 [#8607] remove old pickle-based session support
new 46dd88095 [#8607] replace last pickle usage with our own bit of compatible code
new 8c5d5a16f [#8607] check subproject parent
new 475c9dc1f [#8607] check activity nbhd
new a47c0bce6 [#8607] various has_access improvements
new d96c2120d [#8607] be extra safe with display names
new 9157ab6f2 [#8607] svn tarball path safety
new 01f39777e [#8607] remove potential info disclosure
new d692742a1 [#8607] remove useless try/except/raise
new c209d7707 [#8607] ./run_tests QoL: show final message, allow trailing slash for tab-complete of suites
new 0059dc966 [#8607] solr: change ticket searches from **kw passthru to explicit
new ec1c4303b [#8607] solr: move **kw to specific params for all other tools using common search_app helper
new 058d5b94c [#8607] solr: strip local-params syntax
new d54ba3c6b [#8607] solr: syntax problems deserve an escaped retry; don't expose underlying error messages
The 33 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(allura) 07/33: [#8607] check app_config_id in update_forums
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit b77f66002d3adf3eae404988aef9a3874dd63db4
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 16:51:36 2026 -0400
[#8607] check app_config_id in update_forums
---
ForgeDiscussion/forgediscussion/forum_main.py | 5 ++-
.../tests/functional/test_forum_admin.py | 36 ++++++++++++++++++++++
2 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/ForgeDiscussion/forgediscussion/forum_main.py b/ForgeDiscussion/forgediscussion/forum_main.py
index 9ab89f4c6..2c70cd47b 100644
--- a/ForgeDiscussion/forgediscussion/forum_main.py
+++ b/ForgeDiscussion/forgediscussion/forum_main.py
@@ -322,7 +322,10 @@ def set_value(forum, name, val):
setattr(forum, name, val)
for f in forum:
- forum = DM.Forum.query.get(_id=ObjectId(str(f['id'])))
+ forum = DM.Forum.query.get(_id=ObjectId(str(f['id'])),
+ app_config_id=self.app.config._id)
+ if forum is None:
+ continue
if f.get('delete'):
forum.deleted = True
M.AuditLog.log('deleted forum "{}" from {}'.format(
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_forum_admin.py b/ForgeDiscussion/forgediscussion/tests/functional/test_forum_admin.py
index 72cf633d5..459f2c91a 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_forum_admin.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_forum_admin.py
@@ -18,9 +18,12 @@
import logging
import re
+from ming.odm import ThreadLocalODMSession
+from tg import tmpl_context as c
from alluratest.controller import TestController
from allura.lib import helpers as h
from allura import model as M
+from allura.tests import decorators as td
from forgediscussion import model as FM
@@ -320,3 +323,36 @@ def test_footer_monitoring_email(self):
0].kwargs['text'], email_tasks[0].kwargs['text']
assert 'a project admin can change settings at http://localhost/p/test/admin/discussion/forums' in email_tasks[
0].kwargs['text']
+
+ @td.with_tool('test2', 'Discussion', 'discussion')
+ def test_update_forums_rejects_foreign_forum(self):
+ # Create a forum in the test2 project's Discussion app (the victim).
+ h.set_context('test2', 'discussion', neighborhood='Projects')
+ victim_app_config_id = c.app.config._id
+ victim = FM.Forum(
+ app_config_id=victim_app_config_id,
+ shortname='victim',
+ name='Victim Forum',
+ description='original',
+ )
+ ThreadLocalODMSession.flush_all()
+ victim_id = victim._id
+
+ # Attacker is admin of project 'test' and posts to its update_forums with the victim's _id.
+ self.app.post('/admin/discussion/update_forums',
+ params={'forum-0.delete': 'on',
+ 'forum-0.id': str(victim_id),
+ 'forum-0.name': 'Pwned',
+ 'forum-0.shortname': 'pwned',
+ 'forum-0.description': 'pwned',
+ 'forum-0.monitoring_email': ''})
+
+ # Victim forum in test2 must be untouched.
+ ThreadLocalODMSession.close_all()
+ h.set_context('test2', 'discussion', neighborhood='Projects')
+ reloaded = FM.Forum.query.get(_id=victim_id)
+ assert reloaded is not None
+ assert reloaded.deleted is False, 'foreign forum was deleted via cross-project update_forums'
+ assert reloaded.name == 'Victim Forum', 'foreign forum was renamed via cross-project update_forums'
+ assert reloaded.description == 'original'
+ assert reloaded.app_config_id == victim_app_config_id
(allura) 10/33: [#8607] check mailbox owner
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit f9d88e1d11505e932b2eb1fc33d6e727072e258b
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 18:10:57 2026 -0400
[#8607] check mailbox owner
---
Allura/allura/controllers/auth.py | 5 +++--
Allura/allura/tests/functional/test_auth.py | 29 +++++++++++++++++++++++++++++
2 files changed, 32 insertions(+), 2 deletions(-)
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 4bc05f5b3..6ff0c7577 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -1378,8 +1378,9 @@ def update_subscriptions(self, subscriptions=None, email_format=None, **kw):
project_id=bson.ObjectId(s['project_id']),
app_config_id=bson.ObjectId(s['tool_id']))
else:
- if s['subscription_id'] is not None:
- s['subscription_id'].delete()
+ mbox = s['subscription_id']
+ if mbox is not None and mbox.user_id == c.user._id:
+ mbox.delete()
if email_format:
c.user.set_pref('email_format', email_format)
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 78027e116..347ec996e 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -984,6 +984,35 @@ def test_prefs_subscriptions_unsubscribe(self):
s = M.Mailbox.query.get(_id=s_id)
assert not s, "User still has subscription with Mailbox._id %s" % s_id
+ @td.with_user_project('test-admin')
+ def test_update_subscriptions_rejects_foreign_mailbox(self):
+ # Create a Mailbox owned by test-user-1 (the victim).
+ victim = M.User.by_username('test-user-1')
+ project = M.Project.query.get(shortname='test')
+ app_config = project.app_configs[0]
+ victim_mailbox = M.Mailbox(
+ user_id=victim._id,
+ project_id=project._id,
+ app_config_id=app_config._id)
+ ThreadLocalODMSession.flush_all()
+ victim_mailbox_id = victim_mailbox._id
+
+ # Attacker (test-admin) loads their own subscriptions form, then swaps in
+ # the victim's Mailbox _id and submits with subscribed unchecked.
+ resp = self.app.get('/auth/subscriptions/',
+ extra_environ=dict(username='test-admin'))
+ form = self._find_subscriptions_form(resp)
+ field_name = self._find_subscriptions_field(form, subscribed=True)
+ form.fields[field_name + '.subscription_id'][0].value = str(victim_mailbox_id)
+ form.fields[field_name + '.subscribed'][0].value = None
+ form.submit(extra_environ=dict(username='test-admin'))
+
+ # Victim's mailbox must still exist.
+ still_there = M.Mailbox.query.get(_id=victim_mailbox_id)
+ assert still_there is not None, \
+ "victim's Mailbox was deleted via cross-user update_subscriptions"
+ assert still_there.user_id == victim._id
+
def test_format_email(self):
self.app.get('/').follow() # establish session
self.app.post('/auth/subscriptions/update_subscriptions',
(allura) 30/33: [#8607] solr: change ticket searches from **kw passthru to explicit
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 0059dc966ecd2399a59932a2c84c4ca84ca01a47
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 22 16:55:29 2026 -0400
[#8607] solr: change ticket searches from **kw passthru to explicit
---
Allura/allura/lib/search.py | 14 +++++++--
ForgeTracker/forgetracker/model/ticket.py | 34 +++++++++++-----------
.../forgetracker/tests/functional/test_root.py | 1 +
.../forgetracker/tests/unit/test_ticket_model.py | 16 ++++++----
ForgeTracker/forgetracker/tracker_main.py | 33 +++++++++++----------
5 files changed, 58 insertions(+), 40 deletions(-)
diff --git a/Allura/allura/lib/search.py b/Allura/allura/lib/search.py
index 69531f5e9..147c30c47 100644
--- a/Allura/allura/lib/search.py
+++ b/Allura/allura/lib/search.py
@@ -161,11 +161,20 @@ def search(q, short_timeout=False, ignore_errors=True, **kw):
(match.group(1) if match else e))
-def search_artifact(atype, q, history=False, rows=10, short_timeout=False, filter=None, **kw):
+def search_artifact(atype, q, history=False, rows=10, short_timeout=False, filter=None,
+ sort: str | None = None, start: int | None = None, fl: str | None = None,
+ **kw):
"""Performs SOLR search.
Raises SearchError if SOLR returns an error.
+
+ :param kwargs: `fq` or various `facet.*` params can be passed through
"""
+
+ for k in kw:
+ if k not in ['fq', 'facet', 'facet.field', 'facet.limit', 'facet.sort', 'facet.mincount']:
+ raise ValueError(f'Unexpected kwarg {k} passed to search_artifact')
+
# first, grab an artifact and get the fields that it indexes
a = atype.query.find().first()
if a is None:
@@ -199,7 +208,8 @@ def search_artifact(atype, q, history=False, rows=10, short_timeout=False, filte
fq.append(' OR '.join(parts))
if not history:
fq.append('is_history_b:False')
- return search(q, fq=fq, rows=rows, short_timeout=short_timeout, ignore_errors=False, **kw)
+ return search(q, fq=fq, rows=rows, short_timeout=short_timeout, ignore_errors=False, sort=sort, start=start,
+ fl=fl, **kw)
def site_admin_search(model, q, field, **kw):
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index a2e51a334..9dac8202a 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -1223,7 +1223,7 @@ def __json__(self, posts_limit=None, is_export=False):
custom_fields=dict(self.custom_fields))
@classmethod
- def paged_query(cls, app_config, user, query, limit=None, page=0, sort=None, deleted=False, **kw):
+ def paged_query(cls, app_config, user, query, limit=None, page=0, sort=None, deleted=False):
"""
Query tickets, filtering for 'read' permission, sorting and paginating the result.
@@ -1253,21 +1253,18 @@ def paged_query(cls, app_config, user, query, limit=None, page=0, sort=None, del
return dict(
tickets=tickets,
- count=count, q=json.dumps(query), limit=limit, page=page, sort=sort,
- **kw)
+ count=count, q=json.dumps(query), limit=limit, page=page, sort=sort)
@classmethod
def paged_search(cls, app_config, user, q, limit=None, page=0, sort=None, show_deleted=False,
- filter=None, **kw):
+ filter=None):
"""Query tickets from Solr, filtering for 'read' permission, sorting and paginating the result.
See also paged_query which does a mongo search.
We do the sorting and skipping right in SOLR, before we ever ask
- Mongo for the actual tickets. Other keywords for
- search_artifact (e.g., history) or for SOLR are accepted through
- kw. The output is intended to be used directly in templates,
- e.g., exposed controller methods can just:
+ Mongo for the actual tickets. The output is intended to be used
+ directly in templates, e.g., exposed controller methods can just:
return paged_query(q, ...)
@@ -1290,8 +1287,7 @@ def paged_search(cls, app_config, user, q, limit=None, page=0, sort=None, show_d
try:
if q:
# also query for choices for filter options right away
- params = kw.copy()
- params.update(tsearch.FACET_PARAMS)
+ params = dict(tsearch.FACET_PARAMS)
if not show_deleted:
params['fq'] = ['deleted_b:False']
@@ -1331,15 +1327,17 @@ def paged_search(cls, app_config, user, q, limit=None, page=0, sort=None, show_d
count=count, q=q, limit=limit, page=page, sort=sort,
filter=filter,
filter_choices=tsearch.get_facets(matches),
- solr_error=solr_error, **kw)
+ solr_error=solr_error)
@classmethod
def paged_query_or_search(cls, app_config, user, query, search_query, filter,
- limit=None, page=0, sort=None, **kw):
+ limit=None, page=0, sort=None, deleted=False, show_deleted=False):
"""Switch between paged_query and paged_search based on filter.
- query - query in mongo syntax
- search_query - query in solr syntax
+ query - query in mongo syntax (used by paged_query path)
+ search_query - query in solr syntax (used by paged_search path)
+ deleted - value for mongo 'deleted' field (used by paged_query path)
+ show_deleted - whether deleted records should be included (used by paged_search path)
"""
solr_sort = None
if sort and ' ' in sort:
@@ -1348,15 +1346,17 @@ def paged_query_or_search(cls, app_config, user, query, search_query, filter,
solr_col = _mongo_col_to_solr_col(sort_split[0])
solr_sort = f'{solr_col} {sort_split[1]}'
if not filter:
- result = cls.paged_query(app_config, user, query, sort=sort, limit=limit, page=page, **kw)
+ result = cls.paged_query(app_config, user, query, sort=sort, limit=limit, page=page,
+ deleted=deleted)
t = cls.query.find().first()
if t:
search_query = cls.translate_query(search_query, t.index())
result['filter_choices'] = tsearch.query_filter_choices(
- search_query, fq=[] if kw.get('show_deleted', False) else ['deleted_b:False'])
+ search_query, fq=[] if show_deleted else ['deleted_b:False'])
else:
result = cls.paged_search(app_config, user, search_query, filter=filter,
- sort=solr_sort, limit=limit, page=page, **kw)
+ sort=solr_sort, limit=limit, page=page,
+ show_deleted=show_deleted)
result['sort'] = sort
result['url_sort'] = solr_sort if solr_sort else ''
diff --git a/ForgeTracker/forgetracker/tests/functional/test_root.py b/ForgeTracker/forgetracker/tests/functional/test_root.py
index 92259e040..acd307dc1 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -1378,6 +1378,7 @@ def test_search(self):
# 'filter' is special kwarg, don't let it cause problems
r = self.app.get('/p/test/bugs/search/?q=test&filter=blah')
+ r = self.app.get('/p/test/bugs/search/?q=test&defType=asdf')
def test_search_canonical(self):
self.new_ticket(summary='test first ticket')
diff --git a/ForgeTracker/forgetracker/tests/unit/test_ticket_model.py b/ForgeTracker/forgetracker/tests/unit/test_ticket_model.py
index dcd1f32e1..2afd810cc 100644
--- a/ForgeTracker/forgetracker/tests/unit/test_ticket_model.py
+++ b/ForgeTracker/forgetracker/tests/unit/test_ticket_model.py
@@ -374,18 +374,24 @@ def test_paged_query_or_search(self, query, search, tsearch):
app_cfg, user = mock.Mock(), mock.Mock()
mongo_query = 'mongo query'
solr_query = 'solr query'
- kw = {'kw1': 'test1', 'kw2': 'test2'}
+ deleted_val = {'$in': [False]}
filter = None
- Ticket.paged_query_or_search(app_cfg, user, mongo_query, solr_query, filter, **kw)
- query.assert_called_once_with(app_cfg, user, mongo_query, sort=None, limit=None, page=0, **kw)
+ Ticket.paged_query_or_search(app_cfg, user, mongo_query, solr_query, filter,
+ deleted=deleted_val, show_deleted=False)
+ # mongo path: 'deleted' is forwarded, 'show_deleted' is not
+ query.assert_called_once_with(app_cfg, user, mongo_query, sort=None, limit=None, page=0,
+ deleted=deleted_val)
assert tsearch.query_filter_choices.call_count == 1
assert tsearch.query_filter_choices.call_args[0][0] == 'solr query'
assert search.call_count == 0
query.reset_mock(), search.reset_mock(), tsearch.reset_mock()
filter = {'status': 'unread'}
- Ticket.paged_query_or_search(app_cfg, user, mongo_query, solr_query, filter, **kw)
- search.assert_called_once_with(app_cfg, user, solr_query, filter=filter, sort=None, limit=None, page=0, **kw)
+ Ticket.paged_query_or_search(app_cfg, user, mongo_query, solr_query, filter,
+ deleted=deleted_val, show_deleted=False)
+ # solr path: 'show_deleted' is forwarded, 'deleted' (mongo-only) is not
+ search.assert_called_once_with(app_cfg, user, solr_query, filter=filter,
+ sort=None, limit=None, page=0, show_deleted=False)
assert query.call_count == 0
assert tsearch.query_filter_choices.call_count == 0
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index b7ab8a64a..a3482cf70 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -731,22 +731,21 @@ def tags(self, term=None, **kw):
@expose('jinja:forgetracker:templates/tracker/index.html')
@validate(dict(deleted=validators.StringBool(if_empty=False),
filter=V.JsonConverter(if_empty={})))
- def index(self, limit=None, columns=None, page=0, sort='ticket_num desc', deleted=False, filter=None, **kw):
+ def index(self, q=None, limit=None, columns=None, page=0, sort='ticket_num desc',
+ deleted=False, filter=None):
show_deleted = [False]
if deleted and has_access(c.app, 'delete'):
show_deleted = [False, True]
elif deleted and not has_access(c.app, 'delete'):
deleted = False
- # it's just our original query mangled and sent back to us
- kw.pop('q', None)
result = TM.Ticket.paged_query_or_search(c.app.config, c.user,
c.app.globals.not_closed_mongo_query,
c.app.globals.not_closed_query,
filter,
sort=sort, limit=limit, page=page,
deleted={'$in': show_deleted},
- show_deleted=deleted, **kw)
+ show_deleted=deleted)
result['columns'] = columns or mongo_columns()
result[
@@ -838,7 +837,7 @@ def update_milestones(self, field_name=None, milestones=None, **kw):
@expose('jinja:forgetracker:templates/tracker/search.html')
@validate(validators=search_validators)
def search(self, q=None, query=None, project=None, columns=None, page=0, sort=None,
- deleted=False, filter=None, **kw):
+ deleted=False, filter=None, history=None, limit=None):
require_access(c.app, 'read')
if deleted and not has_access(c.app, 'delete'):
@@ -851,9 +850,9 @@ def search(self, q=None, query=None, project=None, columns=None, page=0, sort=No
bin = TM.Bin.query.find(
dict(app_config_id=c.app.config._id, terms=q)).first()
if project:
- redirect(c.project.url() + 'search?' + urlencode(dict(q=q, history=kw.get('history'))))
+ redirect(c.project.url() + 'search?' + urlencode(dict(q=q, history=history)))
result = TM.Ticket.paged_search(c.app.config, c.user, q, page=page, sort=sort,
- show_deleted=deleted, filter=filter, **kw)
+ show_deleted=deleted, filter=filter, limit=limit)
result['columns'] = columns or solr_columns()
result[
'sortable_custom_fields'] = c.app.globals.sortable_custom_fields_shown_in_search()
@@ -870,11 +869,13 @@ def search(self, q=None, query=None, project=None, columns=None, page=0, sort=No
@h.vardec
@expose()
@validate(validators=search_validators)
- def search_feed(self, q=None, query=None, project=None, page=0, sort=None, deleted=False, **kw):
+ def search_feed(self, q=None, query=None, project=None, page=0, sort=None,
+ deleted=False, limit=None, filter=None):
if query and not q:
q = query
result = TM.Ticket.paged_search(
- c.app.config, c.user, q, page=page, sort=sort, show_deleted=deleted, **kw)
+ c.app.config, c.user, q, page=page, sort=sort, show_deleted=deleted,
+ limit=limit, filter=filter)
response.headers['Content-Type'] = ''
response.content_type = 'application/xml'
d = dict(title='Ticket search results', link=h.absurl(c.app.url),
@@ -971,11 +972,11 @@ def save_ticket(self, ticket_form=None, **post_data):
limit=validators.Int(if_empty=10, if_invalid=10),
page=validators.Int(if_empty=0, if_invalid=0),
sort=v.UnicodeString(if_empty='ticket_num_i asc')))
- def edit(self, q=None, limit=None, page=None, sort=None, filter=None, **kw):
+ def edit(self, q=None, limit=None, page=None, sort=None, filter=None):
require_access(c.app, 'update')
result = TM.Ticket.paged_search(c.app.config, c.user, q, filter=filter,
sort=sort, limit=limit, page=page,
- show_deleted=False, **kw)
+ show_deleted=False)
# if c.app.globals.milestone_names is None:
# c.app.globals.milestone_names = ''
@@ -999,11 +1000,11 @@ def edit(self, q=None, limit=None, page=None, sort=None, filter=None, **kw):
limit=validators.Int(if_empty=10, if_invalid=10),
page=validators.Int(if_empty=0, if_invalid=0),
sort=v.UnicodeString(if_empty='ticket_num_i asc')))
- def move(self, q=None, limit=None, page=None, sort=None, filter=None, **kw):
+ def move(self, q=None, limit=None, page=None, sort=None, filter=None):
require_access(c.app, 'admin')
result = TM.Ticket.paged_search(c.app.config, c.user, q, filter=filter,
sort=sort, limit=limit, page=page,
- show_deleted=False, **kw)
+ show_deleted=False)
result['columns'] = solr_columns()
result[
@@ -1965,7 +1966,7 @@ def __init__(self, root, field, milestone):
filter=V.JsonConverter(if_empty={}),
deleted=validators.StringBool(if_empty=False)))
def index(self, q=None, columns=None, page=0, query=None, sort=None,
- deleted=False, filter=None, **kw):
+ deleted=False, filter=None, limit=None):
require_access(c.app, 'read')
show_deleted = [False]
if deleted and has_access(c.app, 'delete'):
@@ -1976,9 +1977,9 @@ def index(self, q=None, columns=None, page=0, query=None, sort=None,
result = TM.Ticket.paged_query_or_search(c.app.config, c.user,
self.mongo_query,
self.solr_query,
- filter, sort=sort, page=page,
+ filter, sort=sort, page=page, limit=limit,
deleted={'$in': show_deleted},
- show_deleted=deleted, **kw)
+ show_deleted=deleted)
result['columns'] = columns or mongo_columns()
result[
(allura) 03/33: [#8607] test for oembed output security
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 879aa18a693319fca93d2d083c8d25e97cb98cfb
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Wed May 13 13:15:04 2026 -0400
[#8607] test for oembed output security
---
Allura/allura/tests/test_globals.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index 09d40cb64..0c7331d02 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -348,12 +348,14 @@ def test_macro_include_permissions(self):
def test_macro_embed(self, oembed_fetch):
oembed_fetch.return_value = {
"html": '<iframe width="480" height="270" src="http://www.youtube.com/embed/kOLpSPEA72U?feature=oembed" '
- 'frameborder="0" allowfullscreen></iframe>)',
+ 'frameborder="0" allowfullscreen></iframe>'
+ '<script>alert(1)</script>)', # not realistic, but testing if the oembed endpoint got compromised
"title": "Nature's 3D Printer: MIND BLOWING Cocoon in Rainforest - Smarter Every Day 94",
}
r = g.markdown_wiki.convert('[[embed url=http://www.youtube.com/watch?v=kOLpSPEA72U]]')
assert ('<p><iframe height="270" '
- 'src="https://www.youtube-nocookie.com/embed/kOLpSPEA72U?feature=oembed" width="480"></iframe></p>' in
+ 'src="https://www.youtube-nocookie.com/embed/kOLpSPEA72U?feature=oembed" width="480"></iframe>'
+ '<script>alert(1)</script></p>' in
r.replace('\n', ''))
def test_macro_embed_video_gone(self):
(allura) 17/33: [#8607] test for save_attachments (export) path handling
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 1d4f5fce0036bc7c8c02b3c0dc7759787fc4235a
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue May 19 12:35:26 2026 -0400
[#8607] test for save_attachments (export) path handling
---
Allura/allura/tests/test_app.py | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/Allura/allura/tests/test_app.py b/Allura/allura/tests/test_app.py
index 913840315..adf41bf5c 100644
--- a/Allura/allura/tests/test_app.py
+++ b/Allura/allura/tests/test_app.py
@@ -21,6 +21,8 @@
import pytest
from formencode import validators as fev
from textwrap import dedent
+import tempfile
+import os.path
from alluratest.controller import setup_unit_test
from allura import app
@@ -171,3 +173,33 @@ def test_handle_artifact_unicode(self, qg):
for p in [p for p in msg['parts'] if p['payload'] is not None]:
# filter here mimics logic in `route_email`
a.handle_artifact_message(ticket, p)
+
+ def test_save_attachments_path_traversal_protection(self):
+ a = app.Application(c.project, c.app.config)
+ export_dir = tempfile.mkdtemp()
+
+ class FakeAttachment:
+ def __init__(self, filename, data):
+ self.filename = filename
+ self._data = data
+
+ def rfile(self):
+ from io import BytesIO
+ return BytesIO(self._data)
+
+ # Test with a malicious filename containing path traversal
+ malicious_name = '../../../etc/passwd'
+ attachments = [FakeAttachment(malicious_name, b'evil data')]
+ a.save_attachments(export_dir, attachments)
+
+ # The file should be written as a basename, not traverse directories
+ expected_file = os.path.join(export_dir, 'passwd')
+ assert os.path.exists(expected_file)
+
+ # Verify the traversal path was NOT created
+ traversal_path = os.path.join(export_dir, '..', '..', '..', 'etc')
+ assert not os.path.exists(traversal_path)
+
+ # Verify file contents
+ with open(expected_file, 'rb') as f:
+ assert f.read() == b'evil data'
(allura) 02/33: [#8607] check for email code mode when verifying email link
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit b35009fc6eec3b0b450356aa6fc42bb5dd91e917
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Wed May 13 12:14:42 2026 -0400
[#8607] check for email code mode when verifying email link
---
Allura/allura/controllers/auth.py | 5 ++++-
Allura/allura/tests/functional/test_auth.py | 32 ++++++++++++++++++++++++++++-
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index a500df93d..4bc05f5b3 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -498,7 +498,10 @@ def login_email_verify(self, token, return_to='/', **kwargs):
if not c.user.is_anonymous():
redirect(self._verify_return_to(return_to))
- if not session.get('multifactor-username'):
+ if not session.get('multifactor-username') or session.get('mode') != 'email_code':
+ session.pop('multifactor-username', None)
+ session.pop('mode', None)
+ session.save()
return dict(error='Your login session was disrupted.<br>Be sure to use the same browser when logging in and opening the email verification link.')
user = M.User.by_username(session['multifactor-username'])
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index e2e568370..78027e116 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -44,7 +44,7 @@
from allura.model.oauth import dummy_oauths
from allura.lib import plugin
from allura.lib import helpers as h
-from allura.lib.multifactor import TotpService, RecoveryCodeService
+from allura.lib.multifactor import TotpService, RecoveryCodeService, EmailCodeAuthenticationService
def unentity(s):
@@ -3218,6 +3218,36 @@ def test_login_totp_untrusted_source(self, trusted_login_source, send_system_mai
assert r.location == 'http://localhost/p/foo'
assert r.session.get('username') == 'test-admin'
+ @mock.patch.dict(config, {'auth.email_auth_code.enabled': True})
+ def test_login_email_verify_requires_email_code_stage(self):
+ # make sure an email link can't bypass MFA
+ # not sure how it could happen to have a valid email link before MFA, but make sure anyway.
+
+ self._init_totp()
+
+ self.app.extra_environ = {'disable_auth_magic': 'True'}
+ user = M.User.by_username('test-admin')
+ token = EmailCodeAuthenticationService().generate_token(user)
+
+ r = self.app.get('/auth/?return_to=/p/foo')
+ f = r.forms[0]
+ encoded = self.app.antispam_field_names(f)
+ f[encoded['username']] = 'test-admin'
+ f[encoded['password']] = 'foo'
+ r = f.submit()
+
+ assert r.location.endswith('/auth/multifactor?return_to=%2Fp%2Ffoo'), r
+ r = r.follow()
+ assert r.session.get('multifactor-username') == 'test-admin'
+ assert r.session.get('mode') is None
+
+ r = self.app.get(f'/auth/login_email_verify?token={token}&return_to=/p/foo', status=200)
+
+ assert 'Your login session was disrupted' in r.text
+ assert not r.session.get('username')
+ assert not r.session.get('multifactor-username')
+ assert not r.session.get('mode')
+
def test_login_rate_limit(self):
self._init_totp()
(allura) 13/33: [#8607] replace random with secrets in some places
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit f02a89b9d66d37817fc32055c28a894335727d83
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 18:36:31 2026 -0400
[#8607] replace random with secrets in some places
---
Allura/allura/lib/helpers.py | 4 ++--
Allura/allura/lib/multifactor.py | 2 +-
Allura/allura/lib/plugin.py | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index ecb6d063c..33d180790 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -30,7 +30,7 @@
import json
import logging
import string
-import random
+import secrets
from collections.abc import Iterable
from hashlib import sha1
from datetime import datetime, timedelta
@@ -489,7 +489,7 @@ def cryptographic_nonce(length=40):
def random_password(length=20, chars=string.ascii_uppercase + string.digits):
- return ''.join(random.choice(chars) for x in range(length))
+ return ''.join(secrets.choice(chars) for x in range(length))
def ago(start_time, show_date_after=7):
diff --git a/Allura/allura/lib/multifactor.py b/Allura/allura/lib/multifactor.py
index 1a46fe05f..5a7c4dedd 100644
--- a/Allura/allura/lib/multifactor.py
+++ b/Allura/allura/lib/multifactor.py
@@ -360,7 +360,7 @@ def get(cls):
def generate_one_code(self):
# for compatibility with Google PAM file, we only do digits
length = asint(config.get('auth.multifactor.recovery_code.length', 8))
- return ''.join([random.choice(string.digits) for i in range(length)])
+ return ''.join([secrets.choice(string.digits) for i in range(length)])
def regenerate_codes(self, user):
'''
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 9ad183eec..cd8291ff8 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -28,7 +28,7 @@
from contextlib import contextmanager
from urllib.parse import urlparse
from io import BytesIO
-from random import randint
+import secrets
from hashlib import sha256
from base64 import b64encode
from datetime import datetime, timedelta
@@ -601,7 +601,7 @@ def _encode_password_legacy_sha256(self, password: str, salt: str) -> str:
from allura import model as M
if salt is None:
- salt = ''.join(chr(randint(1, 0x7f))
+ salt = ''.join(secrets.choice(string.printable[:94])
for i in range(M.User.SALT_LEN))
hashpass = sha256((salt + password).encode('utf-8')).digest()
return 'sha256' + salt + six.ensure_text(b64encode(hashpass))
(allura) 16/33: [#8607] check client_id when making bearer token
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 9a32ca3312f437a5d93d4e66a54c75d5deac6de1
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Mon May 18 15:10:49 2026 -0400
[#8607] check client_id when making bearer token
---
Allura/allura/controllers/auth.py | 5 +++++
Allura/allura/tests/functional/test_auth.py | 33 +++++++++++++++++++++++++++++
2 files changed, 38 insertions(+)
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 385cc6c08..0917f19df 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -1577,6 +1577,11 @@ def generate_bearer_token(self, client_id):
"""
Manually generates an OAuth2 access token without needing to go through the OAuth2 flow.
"""
+ client = M.OAuth2ClientApp.query.get(client_id=client_id)
+ if client is None or client.user_id != c.user._id:
+ flash('Invalid client ID', 'error')
+ redirect('.')
+
M.OAuth2AccessToken(
client_id=client_id,
user_id=c.user._id,
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 31cb3e413..8338a2ecc 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -2586,6 +2586,39 @@ def test_authorization_code_no_duplicates(self, mock_client):
ac = M.OAuth2AuthorizationCode.query.find(dict(client_id=c.client_id)).all()
assert len(ac) == 1
+ @mock.patch.dict(config, {'auth.oauth2.enabled': True})
+ def test_generate_bearer_token_ownership_check(self):
+ user = M.User.by_username('test-admin')
+ M.OAuth2ClientApp(
+ client_id='client_12345',
+ client_secret='98765',
+ user_id=user._id,
+ name='testoauth2',
+ description='test client',
+ response_type='code',
+ redirect_uris=['https://localhost/']
+ )
+ ThreadLocalODMSession.flush_all()
+
+ self.app.get('/').follow()
+
+ # As test-user (not the owner), should be rejected
+ r = self.app.get('/auth/oauth/')
+ form = [f for f in r.forms.values() if f.action == 'generate_bearer_token'][0]
+ r = form.submit(extra_environ={'username': 'test-user'})
+ assert 'Invalid client ID' in self.webflash(r)
+ assert M.OAuth2AccessToken.query.get(client_id='client_12345') is None
+
+ # As test-admin (the owner), should succeed
+ r = self.app.get('/auth/oauth/')
+ form = [f for f in r.forms.values() if f.action == 'generate_bearer_token'][0]
+ r = form.submit()
+ assert '' == self.webflash(r)
+ token = M.OAuth2AccessToken.query.get(client_id='client_12345')
+ assert token is not None
+ assert token.is_bearer is True
+ assert token.user_id == user._id
+
class TestOAuthRequestToken(TestController):
(allura) 23/33: [#8607] check activity nbhd
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 475c9dc1f3daed84b49de9b907b281819ff4775a
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Wed May 20 18:12:10 2026 -0400
[#8607] check activity nbhd
---
ForgeActivity/forgeactivity/main.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/ForgeActivity/forgeactivity/main.py b/ForgeActivity/forgeactivity/main.py
index 3cbe91dec..0fb81eb2d 100644
--- a/ForgeActivity/forgeactivity/main.py
+++ b/ForgeActivity/forgeactivity/main.py
@@ -257,6 +257,11 @@ def delete_item(self, activity_id, **kwargs):
activity = Activity.query.get(_id=ObjectId(activity_id))
if not activity:
raise exc.HTTPGone
+ # At least one party of the activity must live under the current neighborhood
+ nbhd_prefix = c.project.neighborhood.url_prefix
+ if not any((p.get('activity_url') or '').startswith(nbhd_prefix)
+ for p in (activity.actor, activity.obj, activity.target) if p):
+ raise exc.HTTPForbidden
# find other copies of this activity on other user/projects timelines
# but only within a small time window, so we can do efficient searching
activity_ts = activity._id.generation_time
(allura) 32/33: [#8607] solr: strip local-params syntax
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 058d5b94c7e09cf90117698e2d65850c41ae5549
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue May 26 14:59:41 2026 -0400
[#8607] solr: strip local-params syntax
---
Allura/allura/lib/search.py | 29 ++++++++++++++++++++++++++++-
Allura/allura/tests/unit/test_solr.py | 29 ++++++++++++++++++++++++++++-
2 files changed, 56 insertions(+), 2 deletions(-)
diff --git a/Allura/allura/lib/search.py b/Allura/allura/lib/search.py
index 6dd6693db..a2371ad7b 100644
--- a/Allura/allura/lib/search.py
+++ b/Allura/allura/lib/search.py
@@ -146,8 +146,36 @@ def inject_user(q, user=None):
return q.replace('$USER', '"%s"' % user.username) if q else q
+_LOCAL_PARAMS_RE = re.compile(r'\{![^}]*\}')
+
+
+def strip_local_params(q):
+ '''Neutralize Solr local-params
+
+ Solr 7.2+ mitigates https://solr.apache.org/guide/solr/latest/upgrade-notes/major-changes-in-solr-8.html#solr-7-2
+ But clean them all out just in case
+
+ Loops until idempotent: a single regex pass can leave a *new* `{!...}` in
+ the residue when the original had nested braces, e.g.
+ `{{!lucene}!type=dismax v='*:*'}` strips to `{!type=dismax v='*:*'}`
+ '''
+ if not q:
+ return q
+ orig = q
+ total = 0
+ while True:
+ q, n = _LOCAL_PARAMS_RE.subn('', q)
+ if not n:
+ break
+ total += n
+ if total:
+ log.warning(f'Stripped Solr local-params block(s) from query: {orig[:200]!r}')
+ return q
+
+
def search(q, short_timeout=False, ignore_errors=True, **kw):
q = inject_user(q)
+ q = strip_local_params(q)
try:
if short_timeout:
return g.solr_short_timeout.search(q, **kw)
@@ -231,7 +259,6 @@ def site_admin_search(model, q, field, **kw):
# use parens to group all the parts of the query with the field
# escaping spaces with '\ ' isn't sufficient for display_name_t since its stored as text_general (why??)
# and wouldn't handle foo@bar.com split on @ either
- # This should work, but doesn't for unknown reasons: q = u'{!term f=%s}%s' % (field, q)
q = q.replace(':', r'\:') # Must escape the colon for IPv6 addresses
q = obj.translate_query(f'{field}:({q})', fields)
kw['q.op'] = 'AND' # so that all terms within the () are required
diff --git a/Allura/allura/tests/unit/test_solr.py b/Allura/allura/tests/unit/test_solr.py
index 06c45f285..6b5548df7 100644
--- a/Allura/allura/tests/unit/test_solr.py
+++ b/Allura/allura/tests/unit/test_solr.py
@@ -26,7 +26,7 @@
from allura.tests import decorators as td
from alluratest.controller import setup_basic_test
from allura.lib.solr import Solr, escape_solr_arg
-from allura.lib.search import search_app, SearchIndexable
+from allura.lib.search import search, search_app, SearchIndexable, strip_local_params
class TestSolr:
@@ -163,6 +163,33 @@ def test_solarize_html_in_text(self):
assert self.obj.solarize() == dict(text='<script>a(1)</script>')
+class TestStripLocalParams:
+
+ @pytest.mark.parametrize('q,expected', [
+ ('foo bar', 'foo bar'),
+ ('', ''),
+ (None, None),
+ ('{!type=lucene}foo', 'foo'),
+ ('{!type=lucene q.op=OR}*:*', '*:*'),
+ (' {!type=lucene}foo', ' foo'),
+ ('{!}rest', 'rest'),
+ ('text:{!type=lucene}foo', 'text:foo'), # later in query
+ ('{!a}foo{!b}bar', 'foobar'), # multiple
+ ("{{!lucene}!type=dismax v='*:*'}", ''), # nested
+ ('{{{!a}!b}!c}rest', 'rest'), # really nested
+ ])
+ def test_strip(self, q, expected):
+ assert strip_local_params(q) == expected
+
+ @mock.patch('allura.lib.search.g')
+ @mock.patch('allura.lib.search.c')
+ def test_search_strips_local_params_before_pysolr(self, c, g):
+ c.user.username = 'tester'
+ search('{!type=lucene q.op=OR}*:*')
+ # pysolr.search must receive the query without the local-params prefix
+ g.solr.search.assert_called_once_with('*:*')
+
+
class TestSearch_app:
def setup_method(self, method):
(allura) 14/33: [#8607] safer pwd comparison
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 9cc8c33b29b2bd6a837751968955f3c0c84b49ae
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Sat May 16 11:16:00 2026 -0400
[#8607] safer pwd comparison
---
Allura/allura/lib/plugin.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index cd8291ff8..bcd03109e 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -21,6 +21,7 @@
from __future__ import annotations
import re
import os
+import hmac
import logging
import subprocess
import string
@@ -698,7 +699,7 @@ def _validate_password(self, user: M.User, password) -> bool:
# legacy. 'allura_sha256' is a custom name for our old logic
salt = str(user.password[6:6 + user.SALT_LEN])
check = self._encode_password_legacy_sha256(password, salt)
- return check == user.password
+ return hmac.compare_digest(check, user.password)
else:
# https://passlib.readthedocs.io/en/stable/narr/context-tutorial.html#deprecation-hash-migration
crypt = CryptContext(
(allura) 11/33: [#8607] check before mailbox sub too
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 36609a8622417f682d9c4b3ecad47311b13155b0
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 18:16:58 2026 -0400
[#8607] check before mailbox sub too
---
Allura/allura/controllers/auth.py | 10 +++++----
Allura/allura/tests/functional/test_auth.py | 32 +++++++++++++++++++++++++++++
2 files changed, 38 insertions(+), 4 deletions(-)
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 6ff0c7577..385cc6c08 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -1373,10 +1373,12 @@ def index(self, **kw):
def update_subscriptions(self, subscriptions=None, email_format=None, **kw):
for s in subscriptions:
if s['subscribed']:
- if s['tool_id'] and s['project_id']:
- M.Mailbox.subscribe(
- project_id=bson.ObjectId(s['project_id']),
- app_config_id=bson.ObjectId(s['tool_id']))
+ if s['tool_id']:
+ app_config = M.AppConfig.query.get(_id=bson.ObjectId(s['tool_id']))
+ if app_config and has_access(app_config, 'read'):
+ M.Mailbox.subscribe(
+ project_id=app_config.project_id,
+ app_config_id=app_config._id)
else:
mbox = s['subscription_id']
if mbox is not None and mbox.user_id == c.user._id:
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 347ec996e..31cb3e413 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -984,6 +984,38 @@ def test_prefs_subscriptions_unsubscribe(self):
s = M.Mailbox.query.get(_id=s_id)
assert not s, "User still has subscription with Mailbox._id %s" % s_id
+ @td.with_user_project('test-admin')
+ @patch('allura.controllers.auth.has_access')
+ def test_update_subscriptions_subscribe_requires_read_access(self, has_access_mock):
+ # Subscribe must be rejected when the user lacks read access to the tool.
+ has_access_mock.side_effect = lambda obj, *a, **kw: not isinstance(obj, M.AppConfig)
+ project = M.Project.query.get(shortname='test')
+ app_config = project.app_configs[0]
+ attacker = M.User.by_username('test-admin')
+ M.Mailbox.query.remove(dict(
+ user_id=attacker._id,
+ app_config_id=app_config._id,
+ artifact_index_id=None))
+ ThreadLocalODMSession.flush_all()
+
+ self.app.get('/').follow()
+ self.app.post('/auth/subscriptions/update_subscriptions',
+ params={
+ 'subscriptions-0.subscribed': 'on',
+ 'subscriptions-0.tool_id': str(app_config._id),
+ 'subscriptions-0.project_id': str(project._id),
+ 'subscriptions-0.subscription_id': '',
+ 'subscriptions-0.topic': '',
+ 'subscriptions-0.artifact_index_id': '',
+ '_csrf_token': self.app.cookies['_csrf_token'],
+ })
+
+ mbox = M.Mailbox.query.get(
+ user_id=attacker._id,
+ app_config_id=app_config._id,
+ artifact_index_id=None)
+ assert mbox is None
+
@td.with_user_project('test-admin')
def test_update_subscriptions_rejects_foreign_mailbox(self):
# Create a Mailbox owned by test-user-1 (the victim).
(allura) 20/33: [#8607] remove old pickle-based session support
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit b4264a414cc598cd8b737fe789396e8ba837aabb
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue May 19 12:43:14 2026 -0400
[#8607] remove old pickle-based session support
---
Allura/allura/config/middleware.py | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index d1cd4cdff..7a6cd0683 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -20,7 +20,6 @@
import ast
import importlib
import mimetypes
-import pickle
import re
import warnings
@@ -28,7 +27,6 @@
from paste.deploy.converters import asbool, aslist, asint
from tg.support.registry import RegistryManager
from beaker.middleware import SessionMiddleware
-from beaker.util import PickleSerializer
from paste.exceptions.errormiddleware import ErrorMiddleware
from werkzeug.debug import DebuggedApplication
@@ -84,12 +82,6 @@ def make_app(global_conf: dict, **app_conf):
return _make_core_app(root, global_conf, **app_conf)
-class BeakerPickleSerializerWithLatin1(PickleSerializer):
- def loads(self, data_string):
- # need latin1 to decode py2 timestamps in py https://docs.python.org/3/library/pickle.html#pickle.Unpickler
- return pickle.loads(data_string, encoding='latin1') # noqa: S301
-
-
def _make_core_app(root, global_conf: dict, **app_conf):
"""
Set allura up with the settings found in the PasteDeploy configuration
@@ -142,7 +134,6 @@ def _make_core_app(root, global_conf: dict, **app_conf):
re.escape('Session options should start with session. instead of session_.'),
DeprecationWarning)
app = SessionMiddleware(app, config,
- original_format_data_serializer=BeakerPickleSerializerWithLatin1(),
session_class=JWTCookieSession)
# Handle "Remember me" functionality
app = RememberLoginMiddleware(app, config)
(allura) 21/33: [#8607] replace last pickle usage with our own bit of compatible code
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 46dd8809561f6a46fd4ab6c1bfbddc88ff742703
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Wed May 20 16:22:54 2026 -0400
[#8607] replace last pickle usage with our own bit of compatible code
---
Allura/allura/model/index.py | 33 ++++++++++++++++++--
Allura/allura/model/repo_refresh.py | 5 ++-
Allura/allura/tests/unit/test_index.py | 56 ++++++++++++++++++++++++++++++++++
3 files changed, 88 insertions(+), 6 deletions(-)
diff --git a/Allura/allura/model/index.py b/Allura/allura/model/index.py
index bd0e54241..b88998baf 100644
--- a/Allura/allura/model/index.py
+++ b/Allura/allura/model/index.py
@@ -16,12 +16,12 @@
# under the License.
import re
+import importlib
import logging
from itertools import groupby
import typing
from ming.odm.property import FieldProperty
-from pickle import dumps, loads
from collections import defaultdict
from urllib.parse import unquote
@@ -47,6 +47,33 @@
log = logging.getLogger(__name__)
+def _dump_cls(cls: type) -> bytes:
+ # byte-for-byte equivalent of pickle.dumps(cls, protocol=2) for a top-level class:
+ # \x80\x02 protocol header, GLOBAL(c) <module>\n<qualname>\n, BINPUT(q) \x00, STOP(.)
+ return b'\x80\x02c' + cls.__module__.encode('utf-8') + b'\n' + cls.__qualname__.encode('utf-8') + b'\nq\x00.'
+
+
+def _load_cls(data) -> type:
+ # parses what pickle wrote for a class reference, without using pickle.
+ # handles protocol 0 (c<module>\n<class>\np<n>\n.) and protocol 2 (\x80\x02 ... q\x00.)
+ b = bytes(data)
+ if b[:2] == b'\x80\x02':
+ b = b[2:]
+ if b[:1] != b'c':
+ raise ValueError(f'unrecognized class reference encoding: {bytes(data)!r}')
+ rest = b[1:]
+ module_name, sep, rest = rest.partition(b'\n')
+ if not sep:
+ raise ValueError(f'malformed class reference: {bytes(data)!r}')
+ class_name, sep, _ = rest.partition(b'\n')
+ if not sep:
+ raise ValueError(f'malformed class reference: {bytes(data)!r}')
+ obj = importlib.import_module(module_name.decode('utf-8'))
+ for part in class_name.decode('utf-8').split('.'):
+ obj = getattr(obj, part)
+ return obj
+
+
class ArtifactReference(MappedClass):
class __mongometa__:
session = main_orm_session
@@ -77,7 +104,7 @@ def from_artifact(cls, artifact):
obj = cls(
_id=artifact.index_id(),
artifact_reference=dict(
- cls=bson.Binary(dumps(artifact.__class__, protocol=2)),
+ cls=bson.Binary(_dump_cls(artifact.__class__)),
project_id=artifact.app_config.project_id,
app_config_id=artifact.app_config._id,
artifact_id=artifact._id))
@@ -92,7 +119,7 @@ def artifact(self):
'''Look up the artifact referenced'''
aref = self.artifact_reference
try:
- cls = loads(bytes(aref.cls)) # noqa: S301
+ cls = _load_cls(aref.cls)
with h.push_context(aref.project_id):
return cls.query.get(_id=aref.artifact_id)
except Exception:
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index b2ff40f88..f1744f212 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -16,7 +16,6 @@
# under the License.
import logging
-from pickle import dumps
import bson
import tg
@@ -30,7 +29,7 @@
from allura.lib import utils
from allura.lib.search import find_shortlinks
from allura.model.repository import Commit, CommitDoc
-from allura.model.index import ArtifactReference, Shortlink
+from allura.model.index import ArtifactReference, Shortlink, _dump_cls
from allura.model.auth import User
from allura.model.timeline import TransientActor
@@ -143,7 +142,7 @@ def refresh_commit_repos(all_commit_ids, repo):
ref = ArtifactReference(
_id=index_id,
artifact_reference=dict(
- cls=bson.Binary(dumps(Commit, protocol=2)),
+ cls=bson.Binary(_dump_cls(Commit)),
project_id=repo.app.config.project_id,
app_config_id=repo.app.config._id,
artifact_id=oid),
diff --git a/Allura/allura/tests/unit/test_index.py b/Allura/allura/tests/unit/test_index.py
new file mode 100644
index 000000000..e89b48211
--- /dev/null
+++ b/Allura/allura/tests/unit/test_index.py
@@ -0,0 +1,56 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import bson
+import pytest
+
+from allura.model.index import _dump_cls, _load_cls
+from allura.model.repository import Commit
+
+
+def test_dump_cls():
+ assert _dump_cls(Commit) == b'\x80\x02callura.model.repository\nCommit\nq\x00.'
+
+
+def test_roundtrip():
+ assert _load_cls(_dump_cls(Commit)) is Commit
+
+
+def test_load_cls_pickle_protocol_2():
+ data = b'\x80\x02callura.model.repository\nCommit\nq\x00.'
+ assert _load_cls(data) is Commit
+
+
+def test_load_cls_pickle_protocol_0():
+ data = bson.Binary(b'callura.model.repository\nCommit\np0\n.')
+ assert _load_cls(data) is Commit
+
+
+def test_load_cls_legacy_repo_module():
+ # legacy data that still may be in mongo records:
+ # the pre-rename module path allura.model.repo (now a backward-compat shim but still must load)
+ data = bson.Binary(b'callura.model.repo\nCommit\np1\n.')
+ assert _load_cls(data) is Commit
+
+
+def test_load_cls_rejects_unknown_prefix():
+ with pytest.raises(ValueError):
+ _load_cls(b'xnot a pickle')
+
+
+def test_load_cls_rejects_truncated():
+ with pytest.raises(ValueError):
+ _load_cls(b'callura.model.repository')
(allura) 22/33: [#8607] check subproject parent
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 8c5d5a16f25b9eb5aabad986847ee13e2b5de23d
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Wed May 20 17:26:21 2026 -0400
[#8607] check subproject parent
---
Allura/allura/ext/admin/admin_main.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index cf97d090e..f436e5f22 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -613,6 +613,7 @@ def _update_mounts(self, subproject=None, tool=None, new=None, **kw):
new_app = None
for sp in subproject:
p = M.Project.query.get(shortname=sp['shortname'],
+ parent_id=c.project._id,
neighborhood_id=c.project.neighborhood_id)
if sp.get('delete'):
require_access(c.project, 'admin')
(allura) 01/33: [#8607] escape html in a few places
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 324d4de114540ade281c22651f08b766cdc350cf
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue May 12 16:43:57 2026 -0400
[#8607] escape html in a few places
---
Allura/allura/templates/app_admin_permissions.html | 2 +-
Allura/allura/templates/site_admin_new_projects.html | 2 +-
Allura/allura/templates/site_admin_task_new.html | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/Allura/allura/templates/app_admin_permissions.html b/Allura/allura/templates/app_admin_permissions.html
index 7433edec4..4cf9303d3 100644
--- a/Allura/allura/templates/app_admin_permissions.html
+++ b/Allura/allura/templates/app_admin_permissions.html
@@ -103,7 +103,7 @@
current_block_list.find('ul').append(
'<li><label><input type="checkbox" value="' + data.user_id + '"name="user_id">'
- + data.username + (data.reason ? ' (' + data.reason + ')' : '') + '</label></li>'
+ + escape_html(data.username) + (data.reason ? ' (' + escape_html(data.reason) + ')' : '') + '</label></li>'
);
// show 'Block List' button
current_block_list.parent('li').show();
diff --git a/Allura/allura/templates/site_admin_new_projects.html b/Allura/allura/templates/site_admin_new_projects.html
index 8970ff2e3..1b954936b 100644
--- a/Allura/allura/templates/site_admin_new_projects.html
+++ b/Allura/allura/templates/site_admin_new_projects.html
@@ -170,7 +170,7 @@
flash(response.message, 'success');
form.remove();
var td = el.closest('tr').find('td:first');
- td.html('<p><strong>Notes:</strong>' + textarea.val() + '</p>');
+ td.html('<p><strong>Notes:</strong>' + html_escape(textarea.val()) + '</p>');
el.remove();
}).fail(function (response){
flash(response.message, 'error');
diff --git a/Allura/allura/templates/site_admin_task_new.html b/Allura/allura/templates/site_admin_task_new.html
index 3ee645d41..4ef47e223 100644
--- a/Allura/allura/templates/site_admin_task_new.html
+++ b/Allura/allura/templates/site_admin_task_new.html
@@ -117,10 +117,10 @@
$task.parent().siblings('.error').remove();
$('pre.doc').empty();
if (data.error) {
- $task.parent().after('<span class="error">' + data.error + '</span>');
+ $task.parent().after('<span class="error">' + escape_html(data.error) + '</span>');
}
if (data.doc) {
- $('pre.doc').html(data.doc);
+ $('pre.doc').text(data.doc);
}
});
});
(allura) 05/33: [#8607] test for zipdir symlink handling
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit f2ca4b4c1d5b05523025be067b44cd1400dd84ed
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 11:29:00 2026 -0400
[#8607] test for zipdir symlink handling
---
Allura/allura/tests/unit/test_repo.py | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/Allura/allura/tests/unit/test_repo.py b/Allura/allura/tests/unit/test_repo.py
index 6d177a2f0..2a9e66893 100644
--- a/Allura/allura/tests/unit/test_repo.py
+++ b/Allura/allura/tests/unit/test_repo.py
@@ -16,10 +16,12 @@
# under the License.
import datetime
+import zipfile
import pytest
from mock import patch, Mock, MagicMock, call
+import tg
from tg import tmpl_context as c
from allura import model as M
@@ -280,6 +282,34 @@ def test_exception_logged(self, tg, popen):
assert "STDOUT: 1" in emsg
assert "STDERR: 2" in emsg
+ def test_symlink_target_content_not_embedded(self, tmp_path):
+ # ensure the `-y` flag stores symlinks as symlinks rather than dereferencing them
+
+ secret = tmp_path / 'secret.txt'
+ secret.write_text('SECRET-CONTENT-MUST-NOT-LEAK')
+
+ src = tmp_path / 'src'
+ src.mkdir()
+ (src / 'regular.txt').write_text('hello')
+ (src / 'leak').symlink_to(secret)
+
+ out = tmp_path / 'out.zip'
+ try:
+ zipdir(str(src), str(out))
+ except FileNotFoundError as not_found_e:
+ if tg.config.get('scm.repos.tarball.zip_binary') in str(not_found_e):
+ pytest.skip('zip binary is missing')
+
+ assert out.exists()
+ with zipfile.ZipFile(str(out), 'r') as z:
+ names = z.namelist()
+ assert 'src/regular.txt' in names
+ assert 'src/leak' in names
+ for name in names:
+ data = z.read(name)
+ assert b'SECRET-CONTENT-MUST-NOT-LEAK' not in data, \
+ f'symlink target content leaked into {name!r}: {data!r}'
+
class TestPrefixPathsUnion:
(allura) 04/33: [#8607] move webhook send from requests to urlopen so our NoInternal handlers run automatically including on redirects
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 10b5ec9bd21751d6ad048803d4fcc20d0774638a
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Wed May 13 17:24:19 2026 -0400
[#8607] move webhook send from requests to urlopen so our NoInternal handlers run automatically including on redirects
---
Allura/allura/tests/test_webhooks.py | 70 ++++++++++++++++++++----------------
Allura/allura/webhooks.py | 44 +++++++++++------------
2 files changed, 61 insertions(+), 53 deletions(-)
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 87213094b..43b4a2095 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -23,6 +23,8 @@
from mock import Mock, MagicMock, patch, call
import pytest
from formencode import Invalid
+
+from allura.tests.test_helpers import httpbin_domain
from ming.odm import session
from tg import tmpl_context as c
from tg import config
@@ -461,9 +463,10 @@ def test_log_msg(self):
self.h.log_msg('OK') ==
'OK: repo-push http://httpbin.org/post /adobe/adobe-1/src/')
response = Mock(
- status_code=500,
- text='that is why',
+ status=500,
+ reason='that is why',
headers={'Content-Type': 'application/json'})
+ response.read.return_value = b'that is why'
assert (
self.h.log_msg('Error', response=response) ==
"Error: repo-push http://httpbin.org/post /adobe/adobe-1/src/ 500 "
@@ -474,31 +477,34 @@ def test_send_webhook_task(self, swh):
send_webhook(self.wh._id, self.payload)
swh.assert_called_once_with(self.wh, self.payload)
- @patch('allura.webhooks.requests', autospec=True)
+ @patch('urllib.request.Request', autospec=True)
+ @patch('urllib.request.urlopen', autospec=True)
@patch('allura.webhooks.log', autospec=True)
- def test_send(self, log, requests):
- requests.post.return_value = Mock(status_code=200)
+ def test_send(self, log, urlopen, Request):
+ urlopen.return_value = Mock(status=200)
self.h.sign = Mock(return_value='sha1=abc')
self.h.send()
headers = {'content-type': 'application/json',
'User-Agent': 'Allura Webhook (https://allura.apache.org/)',
'X-Allura-Signature': 'sha1=abc'}
- requests.post.assert_called_once_with(
+ Request.assert_called_once_with(
self.wh.hook_url,
- data=json.dumps(self.payload),
+ data=json.dumps(self.payload).encode('utf-8'),
headers=headers,
- timeout=30)
+ )
+ urlopen.assert_called_once_with(Request.return_value, timeout=30)
log.info.assert_called_once_with(
'Webhook successfully sent: {} {} {}'.format(
self.wh.type, self.wh.hook_url, self.wh.app_config.url()))
@patch('allura.webhooks.time', autospec=True)
- @patch('allura.webhooks.requests', autospec=True)
+ @patch('urllib.request.urlopen', autospec=True)
@patch('allura.webhooks.log', autospec=True)
- def test_send_error_response_status(self, log, requests, time):
- requests.post.return_value = Mock(status_code=500)
+ def test_send_error_response_status(self, log, urlopen, time):
+ urlopen.return_value = Mock(status=500, headers='Content-Type: text/plain')
+ urlopen.return_value.read.return_value = b'internal error'
self.h.send()
- assert requests.post.call_count == 4 # initial call + 3 retries
+ assert urlopen.call_count == 4 # initial call + 3 retries
assert (time.sleep.call_args_list ==
[call(60), call(120), call(240)])
assert log.info.call_args_list == [
@@ -508,31 +514,30 @@ def test_send_error_response_status(self, log, requests, time):
call('Retrying webhook in %s seconds', 240)]
assert log.error.call_count == 4
log.error.assert_called_with(
- 'Webhook send error: {} {} {} {} {} {}'.format(
+ "Webhook send error: {} {} {} 500 internal error Content-Type: text/plain".format(
self.wh.type, self.wh.hook_url,
- self.wh.app_config.url(),
- requests.post.return_value.status_code,
- requests.post.return_value.text,
- requests.post.return_value.headers))
+ self.wh.app_config.url()
+ )
+ )
@patch('allura.webhooks.time', autospec=True)
- @patch('allura.webhooks.requests', autospec=True)
+ @patch('urllib.request.urlopen', autospec=True)
@patch('allura.webhooks.log', autospec=True)
- def test_send_error_no_retries(self, log, requests, time):
- requests.post.return_value = Mock(status_code=500)
+ def test_send_error_no_retries(self, log, urlopen, time):
+ urlopen.return_value = Mock(status=500, headers='Content-Type: text/plain')
+ urlopen.return_value.read.return_value = b'internal error'
with h.push_config(config, **{'webhook.retry': ''}):
self.h.send()
- assert requests.post.call_count == 1
+ assert urlopen.call_count == 1
assert time.call_count == 0
log.info.assert_called_once_with('Retrying webhook in: %s', [])
assert log.error.call_count == 1
log.error.assert_called_with(
- 'Webhook send error: {} {} {} {} {} {}'.format(
+ "Webhook send error: {} {} {} 500 internal error Content-Type: text/plain".format(
self.wh.type, self.wh.hook_url,
- self.wh.app_config.url(),
- requests.post.return_value.status_code,
- requests.post.return_value.text,
- requests.post.return_value.headers))
+ self.wh.app_config.url()
+ )
+ )
@pytest.mark.parametrize('url', [
'http://localhost/hook',
@@ -540,13 +545,18 @@ def test_send_error_no_retries(self, log, requests, time):
'http://10.0.0.1/hook',
'http://192.168.1.1/hook',
])
- @patch('allura.webhooks.requests', autospec=True)
@patch('allura.webhooks.log', autospec=True)
- def test_send_ssrf_private_url_blocked(self, log, requests, url):
+ def test_send_ssrf_private_url_blocked(self, log, url):
+ result = self.h._send(url, json.dumps(self.payload), {})
+ assert result is False
+ log.exception.assert_called_once()
+
+ @patch('allura.webhooks.log', autospec=True)
+ def test_send_redir_private_url(self, log):
+ url = f'https://{httpbin_domain}/redirect-to?url=http://localhost'
result = self.h._send(url, json.dumps(self.payload), {})
assert result is False
- requests.post.assert_not_called()
- log.error.assert_called_once()
+ log.exception.assert_called_once()
class TestRepoPushWebhookSender(TestWebhookBase):
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index ed5a16ebb..c281316e5 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -22,8 +22,9 @@
import time
import socket
import ssl
+import urllib.request
+import urllib.error
-import requests
from bson import ObjectId
from tg import expose, validate, redirect, flash, config
from tg.decorators import with_trailing_slash, without_trailing_slash
@@ -354,11 +355,15 @@ def log_msg(self, msg, response=None):
self.webhook.hook_url,
self.webhook.app_config.url())
if response is not None:
- message = '{} {} {} {}'.format(
- message,
- response.status_code,
- response.text,
- response.headers)
+ try:
+ message = '{} {} {} {}'.format(
+ message,
+ response.status,
+ response.read(200).decode('utf-8', 'ignore'),
+ response.headers)
+ except Exception:
+ log.warning('could not get response details during webhook error logging', exc_info=True)
+ message = '{} {}'.format(message, response)
return message
def send(self):
@@ -379,27 +384,20 @@ def send(self):
def _send(self, url, data, headers) -> bool:
try:
- v.NonPrivateUrl().to_python(url)
- except Invalid as e:
- log.error(self.log_msg(f'Webhook send error: resolved to private address: {e!r}'))
- return False
- try:
- r = requests.post(
- url,
- data=data,
- headers=headers,
- timeout=self.timeout)
- except (requests.exceptions.RequestException,
- socket.timeout,
- ssl.SSLError):
+ req = urllib.request.Request(url, data=data.encode('utf-8'), headers=headers) # noqa: S310
+ r = urllib.request.urlopen(req, timeout=self.timeout) # noqa: S310
+ except urllib.error.HTTPError as e:
+ r = e
+ except (OSError, Invalid):
log.exception(self.log_msg('Webhook send error'))
return False
- if r.status_code >= 200 and r.status_code < 300:
+
+ if 200 <= r.status < 300:
log.info(self.log_msg('Webhook successfully sent'))
return True
- else:
- log.error(self.log_msg('Webhook send error', response=r))
- return False
+
+ log.error(self.log_msg('Webhook send error', response=r))
+ return False
@task()
(allura) 33/33: [#8607] solr: syntax problems deserve an escaped retry; don't expose underlying error messages
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit d54ba3c6bf39ff2651759de1c52c9f8ca330a5a5
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue May 26 15:12:53 2026 -0400
[#8607] solr: syntax problems deserve an escaped retry; don't expose underlying error messages
---
Allura/allura/lib/search.py | 26 ++++++++++++++++++--------
Allura/allura/tests/unit/test_solr.py | 15 +++++++++++++++
2 files changed, 33 insertions(+), 8 deletions(-)
diff --git a/Allura/allura/lib/search.py b/Allura/allura/lib/search.py
index a2371ad7b..4e832261b 100644
--- a/Allura/allura/lib/search.py
+++ b/Allura/allura/lib/search.py
@@ -173,20 +173,30 @@ def strip_local_params(q):
return q
-def search(q, short_timeout=False, ignore_errors=True, **kw):
+def search(q, short_timeout=False, ignore_errors=True, search_fn=None, **kw):
q = inject_user(q)
q = strip_local_params(q)
- try:
+ if not search_fn:
if short_timeout:
- return g.solr_short_timeout.search(q, **kw)
+ search_fn = g.solr_short_timeout.search
else:
- return g.solr.search(q, **kw)
- except (SolrError, OSError) as e:
+ search_fn = g.solr.search
+ try:
+ # try once with opportunity to retry
+ try:
+ return search_fn(q, **kw)
+ except SolrError:
+ escaped_q = escape_solr_arg(q)
+ if q != escaped_q:
+ # retry if escaping could make a difference
+ return search_fn(escaped_q, **kw)
+ else:
+ raise
+ except (SolrError, OSError):
+ # fatal error
log.exception('Error in solr search')
if not ignore_errors:
- match = re.search(r'<pre>(.*)</pre>', str(e))
- raise SearchError('Error running search query: %s' %
- (match.group(1) if match else e))
+ raise SearchError('Error running search')
def search_artifact(atype, q, history=False, rows=10, short_timeout=False, filter=None,
diff --git a/Allura/allura/tests/unit/test_solr.py b/Allura/allura/tests/unit/test_solr.py
index 6b5548df7..e25581ffd 100644
--- a/Allura/allura/tests/unit/test_solr.py
+++ b/Allura/allura/tests/unit/test_solr.py
@@ -189,6 +189,21 @@ def test_search_strips_local_params_before_pysolr(self, c, g):
# pysolr.search must receive the query without the local-params prefix
g.solr.search.assert_called_once_with('*:*')
+ @mock.patch('allura.lib.search.g')
+ @mock.patch('allura.lib.search.c')
+ def test_search_retries_with_escaped_query_on_solr_error(self, c, g):
+ # a query like 'git --log' is invalid Solr syntax; on the first
+ # SolrError, search() should retry with the escaped form
+ c.user.username = 'tester'
+ g.solr.search.side_effect = [SolrError('bad syntax'), 'ok result']
+
+ result = search('git --log')
+
+ assert result == 'ok result'
+ assert g.solr.search.call_count == 2
+ assert g.solr.search.call_args_list[0] == mock.call('git --log')
+ assert g.solr.search.call_args_list[1] == mock.call(r'git \-\-log')
+
class TestSearch_app:
(allura) 09/33: [#8607] check project when loading role_id inputs
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 2363e20f9ebdf74c5c744bf57aae0f96fb9b078c
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 18:03:43 2026 -0400
[#8607] check project when loading role_id inputs
---
Allura/allura/app.py | 4 +--
Allura/allura/ext/admin/admin_main.py | 43 +++++++++++++++++++---------
Allura/allura/tests/functional/test_admin.py | 16 +++++++++++
3 files changed, 47 insertions(+), 16 deletions(-)
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index ac602fe50..78798f7f5 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -1062,7 +1062,7 @@ def update(self, card=None, **kw):
del_group_ids.append(str(acl['role_id']))
def get_role(_id):
- return model.ProjectRole.query.get(_id=ObjectId(_id))
+ return model.ProjectRole.query.get(_id=ObjectId(_id), project_id=c.project.root_project._id)
groups = list(map(get_role, group_ids))
new_groups = list(map(get_role, new_group_ids))
del_groups = list(map(get_role, del_group_ids))
@@ -1077,7 +1077,7 @@ def group_names(groups):
group_names(groups + new_groups),
self.app.config.options['mount_point']))
- role_ids = list(map(ObjectId, group_ids + new_group_ids))
+ role_ids = [g._id for g in groups + new_groups if g is not None]
self.app.config.acl += [
model.ACE.allow(r, perm) for r in role_ids]
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index 62b8132c7..cf97d090e 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -31,6 +31,7 @@
from tg.decorators import with_trailing_slash, without_trailing_slash
from webob import exc
from bson import ObjectId
+from bson.errors import InvalidId
from ming.odm.odmsession import ThreadLocalODMSession
from ming.odm import session
@@ -1059,14 +1060,18 @@ def update(self, card=None, **kw):
role_ids = list(map(ObjectId, group_ids + new_group_ids))
permissions[perm] = role_ids
c.project.acl = []
+
+ def role_names(roles):
+ return ','.join(sorted(pr.name for pr in roles))
+
for perm, role_ids in permissions.items():
- def role_names(ids): return ','.join(sorted(
- pr.name for pr in M.ProjectRole.query.find(dict(_id={'$in': ids}))))
old_role_ids = old_permissions.get(perm, [])
+ roles = M.ProjectRole.query.find(dict(_id={'$in': role_ids}, project_id=c.project.root_project._id)).all()
if old_role_ids != role_ids:
+ old_roles = M.ProjectRole.query.find(dict(_id={'$in': old_role_ids})).all() # no project_id check needed for old
M.AuditLog.log('updated "%s" permissions: "%s" => "%s"',
- perm, role_names(old_role_ids), role_names(role_ids))
- c.project.acl += [M.ACE.allow(rid, perm) for rid in role_ids]
+ perm, role_names(old_roles), role_names(roles))
+ c.project.acl += [M.ACE.allow(role._id, perm) for role in roles]
g.post_event('project_updated')
redirect('.')
@@ -1160,22 +1165,32 @@ def index(self, **kw):
return dict(roles=roles, permissions_by_role=permissions_by_role,
auth_role=auth_role, anon_role=anon_role)
+ def _role_for_current_project(self, role_id):
+ try:
+ role_oid = ObjectId(role_id)
+ except (InvalidId, TypeError):
+ return None
+ return M.ProjectRole.query.get(
+ _id=role_oid,
+ project_id=c.project.root_project._id)
+
@without_trailing_slash
@expose('json:')
@require_post()
@h.vardec
def change_perm(self, role_id, permission, allow="true", **kw):
+ role = self._role_for_current_project(role_id)
+ if not role:
+ return dict(error='Could not find group with id %s' % role_id)
if allow == "true":
- M.AuditLog.log('granted permission %s to group %s', permission,
- M.ProjectRole.query.get(_id=ObjectId(role_id)).name)
- c.project.acl.append(M.ACE.allow(ObjectId(role_id), permission))
+ M.AuditLog.log('granted permission %s to group %s', permission, role.name)
+ c.project.acl.append(M.ACE.allow(role._id, permission))
else:
admin_group_id = str(M.ProjectRole.by_name('Admin')._id)
if admin_group_id == role_id and permission == 'admin':
return dict(error='You cannot remove the admin permission from the admin group.')
- M.AuditLog.log('revoked permission %s from group %s', permission,
- M.ProjectRole.query.get(_id=ObjectId(role_id)).name)
- c.project.acl.remove(M.ACE.allow(ObjectId(role_id), permission))
+ M.AuditLog.log('revoked permission %s from group %s', permission, role.name)
+ c.project.acl.remove(M.ACE.allow(role._id, permission))
g.post_event('project_updated')
return self._map_group_permissions()
@@ -1186,7 +1201,7 @@ def change_perm(self, role_id, permission, allow="true", **kw):
def add_user(self, role_id, username, **kw):
if not username or username == '*anonymous':
return dict(error='You must choose a user to add.')
- group = M.ProjectRole.query.get(_id=ObjectId(role_id))
+ group = self._role_for_current_project(role_id)
user = M.User.query.get(username=username.strip(), pending=False)
if not group:
@@ -1209,12 +1224,12 @@ def add_user(self, role_id, username, **kw):
@require_post()
@h.vardec
def remove_user(self, role_id, username, **kw):
- group = M.ProjectRole.query.get(_id=ObjectId(role_id))
+ group = self._role_for_current_project(role_id)
+ if not group:
+ return dict(error='Could not find group with id %s' % role_id)
user = M.User.by_username(username.strip())
if group.name == 'Admin' and len(group.users_with_role()) == 1:
return dict(error='You must have at least one user with the Admin role.')
- if not group:
- return dict(error='Could not find group with id %s' % role_id)
if not user:
return dict(error='User %s not found' % username)
user_role = M.ProjectRole.by_user(user)
diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index 49c590ba5..a87eb6d42 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -923,6 +923,22 @@ def test_permission_inherit(self):
assert {'text': 'Does not have permission create',
'has': 'no', 'name': 'create'} in r.json[anon_id]
+ def test_add_user_rejects_foreign_role(self):
+ # role_id from a different project must not be usable to grant membership in this project
+ h.set_context('test2', neighborhood='Projects')
+ foreign_role_id = M.ProjectRole.by_name('Admin')._id
+ h.set_context('test', neighborhood='Projects')
+
+ self.app.post('/admin/groups/add_user', params={
+ 'role_id': str(foreign_role_id),
+ 'username': 'test-user-1'})
+
+ h.set_context('test', neighborhood='Projects')
+ user = M.User.by_username('test-user-1')
+ user_role = M.ProjectRole.by_user(user)
+ assert user_role is None or foreign_role_id not in user_role.roles, \
+ 'foreign role_id was added to user.roles'
+
def test_admin_extension_sidebar(self):
class FooSettingsController:
(allura) 24/33: [#8607] various has_access improvements
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit a47c0bce603c41e3898b02e30a5b5d9399fab2da
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Thu May 21 14:30:26 2026 -0400
[#8607] various has_access improvements
---
Allura/allura/lib/security.py | 13 +++++++++-
Allura/allura/tests/test_tasks.py | 30 ++++++++++++++++++++++
.../forgetracker/tests/functional/test_rest.py | 7 +++++
ForgeTracker/forgetracker/tests/test_app.py | 20 +++++++++++++++
ForgeTracker/forgetracker/tracker_main.py | 21 +++++++++------
5 files changed, 82 insertions(+), 9 deletions(-)
diff --git a/Allura/allura/lib/security.py b/Allura/allura/lib/security.py
index 10805f093..1b2b1fd71 100644
--- a/Allura/allura/lib/security.py
+++ b/Allura/allura/lib/security.py
@@ -307,7 +307,9 @@ def debug_obj(obj) -> str:
def has_access(obj, permission: str, user: M.User | None = None, project: M.Project | None = None, roles=None) -> bool:
'''Return whether the given user has the permission name on the given object.
- - First, all the roles for a user in the given project context are computed.
+ - For individual artifacts, check for project 'read' permission
+
+ - Then all the roles for a user in the given project context are computed.
- If the given object's ACL contains a DENY for this permission on this
user's project role, return False and deny access. TODO: make ACL order
@@ -367,6 +369,15 @@ def has_access(obj, permission: str, user: M.User | None = None, project: M.Proj
else:
project = getattr(obj, 'project', None) or c.project
project = project.root_project
+
+ # check for project 'read' so that calls outside normal HTTP /p/foo/ dispatch
+ # (e.g. REST, email, notifications etc) will get the same checks as ProjectController._check_security.
+ # (note AppConfig.parent_security_context() returns None so the ACL chaining stops at the app)
+ # Skip for Project/Neighborhood else we'd recurse on this very call.
+ if (not isinstance(obj, (M.Project, M.Neighborhood))
+ and not has_access(project, 'read', user=user)):
+ return False
+
roles: RoleCache = cred.user_roles(user_id=user._id, project_id=project._id).reaching_roles
# TODO: move deny logic into loop below; see ticket [#6715]
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index 5dfc88822..953a44ac1 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -544,6 +544,36 @@ def test_email_posting_disabled(self):
message)
assert hm.call_count == 0
+ @td.with_tool('test', 'Tickets', 'bugs')
+ def test_receive_email_denied_without_project_read(self):
+ # A non-member sending email to a private project's tracker must be rejected,
+ # even though the tracker's own ACL grants *authenticated 'post'.
+ import forgetracker
+ from forgetracker import model as TM
+ # Ensure ticket #1 exists so the per-ticket has_access check would otherwise pass.
+ with h.push_config(c, user=M.User.by_username('test-admin')):
+ TM.Ticket.new(form_fields=dict(summary='public-ish'))
+ ThreadLocalODMSession.flush_all()
+
+ project = M.Project.query.get(shortname='test')
+ was_private = project.private
+ project.private = True
+ ThreadLocalODMSession.flush_all()
+ try:
+ sender = M.User.by_username('test-user-2')
+ assert sender, 'test-user-2 fixture missing'
+ with mock.patch.object(forgetracker.tracker_main.ForgeTrackerApp, 'handle_message') as hm:
+ mail_tasks.route_email(
+ '0.0.0.0',
+ sender.email_addresses[0] if sender.email_addresses else 'test-user-2@example.com',
+ ['1@bugs.test.p.in.localhost'],
+ 'Hello, world!')
+ assert hm.call_count == 0, \
+ 'route_email called handle_message despite sender lacking project read'
+ finally:
+ project.private = was_private
+ ThreadLocalODMSession.flush_all()
+
class TestUserNotificationTasks(TestController):
def setup_method(self, method):
diff --git a/ForgeTracker/forgetracker/tests/functional/test_rest.py b/ForgeTracker/forgetracker/tests/functional/test_rest.py
index 8092e2fcb..5727fabc5 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_rest.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_rest.py
@@ -26,6 +26,7 @@
from alluratest.controller import TestRestApiBase
from forgetracker import model as TM
+from ming.odm import ThreadLocalODMSession
class TestTrackerApiBase(TestRestApiBase):
@@ -152,6 +153,12 @@ def test_ticket_index_noauth(self):
assert (ticket_config.options.get('TicketMonitoringEmail') ==
'test@localhost')
+ def test_ticket_index_private_project_denied(self):
+ project = M.Project.query.get(shortname='test')
+ project.private = True
+ ThreadLocalODMSession.flush_all()
+ self.api_get('/rest/p/test/bugs/', user='*anonymous', status=[401, 403])
+
@td.with_tool('test', 'Tickets', 'dummy')
def test_move_ticket_redirect(self):
p = M.Project.query.get(shortname='test')
diff --git a/ForgeTracker/forgetracker/tests/test_app.py b/ForgeTracker/forgetracker/tests/test_app.py
index d94a5e6d2..2dd9ca49e 100644
--- a/ForgeTracker/forgetracker/tests/test_app.py
+++ b/ForgeTracker/forgetracker/tests/test_app.py
@@ -66,6 +66,26 @@ def test_inbound_email_no_match(self):
post = M.Post.query.get(_id=message_id)
assert post is None
+ @td.with_tracker
+ def test_has_access_checks_per_ticket_acl(self):
+ # private ticket gets DENY_ALL for everyone except Developer + reporter
+ ticket = TM.Ticket.new()
+ ticket.summary = 'private ticket'
+ ticket.private = True
+ ThreadLocalODMSession.flush_all()
+
+ admin = M.User.by_username('test-admin') # Admin/Developer
+ non_dev = M.User.by_username('test-user') # *authenticated, not Developer
+
+ assert c.app.has_access(admin, str(ticket.ticket_num))
+ assert not c.app.has_access(non_dev, str(ticket.ticket_num))
+
+ @td.with_tracker
+ def test_has_access_rejects_unknown_ticket(self):
+ non_dev = M.User.by_username('test-user')
+ assert not c.app.has_access(non_dev, '99999')
+ assert not c.app.has_access(non_dev, 'not-a-number')
+
@td.with_tracker
def test_uninstall(self):
t = TM.Ticket.new()
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index 7cc17780c..b7ab8a64a 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -272,20 +272,25 @@ def __init__(self, project, config):
def globals(self):
return TM.Globals.query.get(app_config_id=self.config._id)
+ def _ticket_for_email_topic(self, topic):
+ try:
+ return TM.Ticket.query.get(
+ app_config_id=self.config._id,
+ ticket_num=int(topic))
+ except (ValueError, TypeError):
+ return None
+
def has_access(self, user, topic):
- return has_access(c.app, 'post', user)
+ ticket = self._ticket_for_email_topic(topic)
+ if ticket is None:
+ return False
+ return has_access(ticket, 'post', user)
def handle_message(self, topic, message):
log.info('Message from %s (%s)',
topic, self.config.options.mount_point)
log.info('Headers are: %s', message['headers'])
- try:
- ticket = TM.Ticket.query.get(
- app_config_id=self.config._id,
- ticket_num=int(topic))
- except Exception:
- log.exception('Error getting ticket %s', topic)
- return
+ ticket = self._ticket_for_email_topic(topic)
if not ticket:
log.info('No such ticket num: %s', topic)
elif ticket.discussion_disabled:
(allura) 29/33: [#8607] ./run_tests QoL: show final message, allow trailing slash for tab-complete of suites
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit c209d7707aa77f10198743528253bc67d0c1bdb3
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 22 16:55:05 2026 -0400
[#8607] ./run_tests QoL: show final message, allow trailing slash for tab-complete of suites
---
run_tests | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/run_tests b/run_tests
index d51f436a0..e69bb2fd9 100755
--- a/run_tests
+++ b/run_tests
@@ -215,10 +215,17 @@ def parse_args():
parser.add_argument(
'-p', help='List of packages to run tests on. Default: all',
dest='packages', choices=get_packages(), default=get_packages(),
+ type=lambda s: s.rstrip('/'),
nargs='+')
return parser.parse_known_args()
if __name__ == "__main__":
ret_codes = run_tests_in_parallel(*parse_args())
- sys.exit(any(ret_codes))
+ num_failed = sum(1 for code in ret_codes if code)
+ print()
+ if num_failed:
+ print(f'FAILURE(s) in {num_failed} suite(s) above')
+ sys.exit(1)
+ else:
+ print('ALL PASSED')
(allura) 27/33: [#8607] remove potential info disclosure
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 01f39777e60a903526b439e02cf9d9b9f216005a
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 22 15:49:07 2026 -0400
[#8607] remove potential info disclosure
---
Allura/allura/controllers/repository.py | 7 ++++++-
Allura/allura/controllers/rest.py | 2 +-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index 111ba510a..3cd059f91 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -45,6 +45,7 @@
from allura.lib import widgets as w
from allura.lib.decorators import require_post, memorable_forget
from allura.lib.diff import HtmlSideBySideDiff
+from allura.lib.exceptions import ForgeError
from allura.lib.security import require_access, require_authenticated, has_access
from allura.lib.widgets import form_fields as ffw
from allura.lib.widgets.repo import SCMLogWidget, SCMRevisionWidget, SCMTreeWidget
@@ -150,9 +151,13 @@ def fork(self, project_id=None, mount_point=None, mount_label=None, **kw):
redirect(to_project.url() + mount_point + '/')
except exc.HTTPRedirection:
raise
- except Exception as ex:
+ except ForgeError as ex:
flash(str(ex), 'error')
redirect(six.ensure_text(request.referer or '/'))
+ except Exception as ex:
+ log.exception('Error forking repo')
+ flash('Error forking repo', 'error')
+ redirect(six.ensure_text(request.referer or '/'))
@property
def mr_widget(self):
diff --git a/Allura/allura/controllers/rest.py b/Allura/allura/controllers/rest.py
index 15a109c40..7613ac4fb 100644
--- a/Allura/allura/controllers/rest.py
+++ b/Allura/allura/controllers/rest.py
@@ -422,7 +422,7 @@ def _authenticate(self):
request.environ.get('HTTP_X_FORWARDED_PROTO') == 'https',
debug)):
request.environ['tg.status_code_redirect'] = True
- raise exc.HTTPUnauthorized('HTTPS is required to use bearer tokens %s' % request.environ)
+ raise exc.HTTPUnauthorized('HTTPS is required to use bearer tokens')
access_token = M.OAuthAccessToken.query.get(api_key=access_token)
if not (access_token and access_token.is_bearer):
request.environ['tg.status_code_redirect'] = True
(allura) 12/33: [#8607] check subscribe here too
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit a1ab533dbd4a6b8a93ae20fbef8ba5f7ce0a7def
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 18:20:20 2026 -0400
[#8607] check subscribe here too
---
Allura/allura/controllers/discuss.py | 2 ++
Allura/allura/tests/functional/test_discuss.py | 16 ++++++++++++
.../forgediscussion/tests/functional/test_forum.py | 29 ++++++++++++++++++++++
3 files changed, 47 insertions(+)
diff --git a/Allura/allura/controllers/discuss.py b/Allura/allura/controllers/discuss.py
index d4199eef0..9340916f9 100644
--- a/Allura/allura/controllers/discuss.py
+++ b/Allura/allura/controllers/discuss.py
@@ -109,6 +109,8 @@ def subscribe(self, **kw):
threads = kw.pop('threads', [])
for t in threads:
thread = self.M.Thread.query.get(_id=t['_id'])
+ if thread is None or not has_access(thread, 'read'):
+ continue
if t.get('subscription'):
thread.subscribe()
else:
diff --git a/Allura/allura/tests/functional/test_discuss.py b/Allura/allura/tests/functional/test_discuss.py
index e3d05e85e..7592c870e 100644
--- a/Allura/allura/tests/functional/test_discuss.py
+++ b/Allura/allura/tests/functional/test_discuss.py
@@ -82,6 +82,22 @@ def test_subscribe_unsubscribe(self):
r = self.app.post('/wiki/_discuss/subscribe', params=params)
assert not self._is_subscribed(user, thread)
+ @patch('allura.controllers.discuss.has_access')
+ def test_subscribe_requires_read_access(self, has_access_mock):
+ # Without read access to the thread, subscribe must be a no-op.
+ has_access_mock.side_effect = lambda obj, *a, **kw: not isinstance(obj, M.Thread)
+ user = M.User.by_username('test-admin')
+ thread_id = self._thread_id()
+ thread = M.Thread.query.get(_id=thread_id)
+ M.Mailbox.query.remove(dict(user_id=user._id, app_config_id=thread.app_config_id))
+
+ assert not self._is_subscribed(user, thread)
+ params = {
+ 'threads-0._id': thread_id,
+ 'threads-0.subscription': 'on'}
+ self.app.post('/wiki/_discuss/subscribe', params=params)
+ assert not self._is_subscribed(user, thread)
+
@patch('allura.controllers.discuss.g.spam_checker.check')
@patch('allura.controllers.discuss.g.spam_checker.submit_spam')
def test_post(self, submit_spam, check_spam):
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py b/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
index 7c7035735..1ce90c12c 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
@@ -682,6 +682,35 @@ def test_thread(self):
assert 'zzz' in r.html.find('div', {'class': 'display_post'}).text
assert 'Last edit: Test Admin ' in r.html.find('div', {'class': 'display_post'}).text
+ @mock.patch('allura.controllers.discuss.has_access')
+ def test_forum_subscribe_requires_read_access(self, has_access_mock):
+ # Posting to <forum>/subscribe with a thread the user can't read must be a no-op.
+ has_access_mock.side_effect = lambda obj, *a, **kw: not isinstance(obj, FM.ForumThread)
+ # Create a thread in testforum so there's something to subscribe to.
+ r = self.app.get('/discussion/create_topic/')
+ f = r.html.find('form', {'action': '/p/test/discussion/save_new_topic'})
+ params = dict()
+ for field in f.find_all('input'):
+ if field.has_attr('name'):
+ params[field['name']] = field.get('value') or ''
+ params[f.find('textarea')['name']] = 'body'
+ params[f.find('select')['name']] = 'testforum'
+ params[f.find('input', {'style': 'width: 90%'})['name']] = 'Locked Topic'
+ self.app.post('/discussion/save_new_topic', params=params).follow()
+
+ h.set_context('test', 'discussion', neighborhood='Projects')
+ thread = FM.ForumThread.query.get(subject='Locked Topic')
+ assert thread is not None
+ user = M.User.by_username('test-admin')
+ M.Mailbox.query.remove(dict(user_id=user._id, app_config_id=thread.app_config_id))
+
+ self.app.post('/discussion/testforum/subscribe',
+ params={'threads-0._id': thread._id,
+ 'threads-0.subscription': 'on'})
+ subscribed = M.Mailbox.query.get(
+ user_id=user._id, artifact_index_id=thread.index_id())
+ assert subscribed is None
+
def test_subscription_controls(self):
r = self.app.get('/discussion/create_topic/')
f = r.html.find('form', {'action': '/p/test/discussion/save_new_topic'})
(allura) 15/33: [#8607] perm checks in markdown_to_html and fix wiki tool check
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit c8d8c67db579873949bf1adfd5cc9fd620789fd3
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Mon May 18 13:27:01 2026 -0400
[#8607] perm checks in markdown_to_html and fix wiki tool check
---
Allura/allura/controllers/newforge.py | 14 +++++++---
Allura/allura/tests/functional/test_newforge.py | 34 ++++++++++++++++++++++---
Allura/allura/tests/functional/test_root.py | 3 ++-
3 files changed, 44 insertions(+), 7 deletions(-)
diff --git a/Allura/allura/controllers/newforge.py b/Allura/allura/controllers/newforge.py
index 68351fd3f..ef93940ae 100644
--- a/Allura/allura/controllers/newforge.py
+++ b/Allura/allura/controllers/newforge.py
@@ -23,9 +23,11 @@
from webob import exc
from tg import app_globals as g
+from tg import tmpl_context as c
from allura.lib import helpers as h
from allura.lib import utils
from allura.lib.exceptions import ForgeError
+from allura.lib.security import require_access
class NewForgeController:
@@ -35,16 +37,22 @@ class NewForgeController:
@expose()
@without_trailing_slash
- def markdown_to_html(self, markdown, neighborhood=None, project=None, app=None):
+ def markdown_to_html(self, markdown, neighborhood, project, app):
"""Convert markdown to html."""
if neighborhood is None or project is None:
raise exc.HTTPBadRequest()
try:
h.set_context(project, app, neighborhood=neighborhood)
- except ForgeError:
+ except ForgeError: # no project or nbhd found
raise exc.HTTPBadRequest()
- if app == 'wiki':
+ if c.app is None:
+ raise exc.HTTPBadRequest()
+
+ require_access(c.project, 'read')
+ require_access(c.app, 'read')
+
+ if c.app.tool_label.lower() == 'wiki':
html = g.markdown_wiki.convert(markdown)
else:
html = g.markdown.convert(markdown)
diff --git a/Allura/allura/tests/functional/test_newforge.py b/Allura/allura/tests/functional/test_newforge.py
index de5432585..c52fbf37d 100644
--- a/Allura/allura/tests/functional/test_newforge.py
+++ b/Allura/allura/tests/functional/test_newforge.py
@@ -17,6 +17,7 @@
import logging
from urllib.parse import quote
+from ming.odm import ThreadLocalODMSession
from testfixtures import LogCapture
from allura.tests import TestController
@@ -30,19 +31,46 @@ class TestNewForgeController(TestController):
def test_markdown_to_html(self):
n = M.Neighborhood.query.get(name='Projects')
r = self.app.get(
- '/nf/markdown_to_html?markdown=*aaa*bb[wiki:Home]&project=test&app=bugs&neighborhood=%s' % n._id, validate_chunk=True)
+ '/nf/markdown_to_html?markdown=*aaa*bb[wiki:Home]&project=test&app=wiki&neighborhood=%s' % n._id, validate_chunk=True)
assert '<p><em>aaa</em>bb<a class="alink" href="/p/test/wiki/Home/">[wiki:Home]</a></p>' in r, r
# this happens to trigger an error
bad_markdown = '<foo {bar}>'
- r = self.app.get('/nf/markdown_to_html?markdown=%s&project=test&app=bugs&neighborhood=%s' %
+ r = self.app.get('/nf/markdown_to_html?markdown=%s&project=test&app=wiki&neighborhood=%s' %
(quote(bad_markdown), n._id))
r.mustcontain('The markdown supplied could not be parsed correctly.')
r.mustcontain('<pre><foo {bar}></pre>')
- r = self.app.get('/nf/markdown_to_html?markdown=*aaa*bb[wiki:Home]&project=test&app=bugs&neighborhood=bogus',
+ r = self.app.get('/nf/markdown_to_html?markdown=*aaa*bb[wiki:Home]&project=test&app=wiki&neighborhood=bogus',
status=400)
+ @td.with_wiki
+ def test_markdown_to_html_private_project(self):
+ # Make the test project private by removing anonymous read access
+ p = M.Project.query.get(shortname='test')
+ p.acl = [ace for ace in p.acl
+ if not (ace.access == M.ACE.ALLOW
+ and ace.permission == 'read'
+ and ace.role_id == M.ProjectRole.anonymous(p)._id)]
+ ThreadLocalODMSession.flush_all()
+
+ n = M.Neighborhood.query.get(name='Projects')
+ # Authenticated user with access should still work
+ r = self.app.get(
+ '/nf/markdown_to_html?markdown=*hello*&project=test&app=wiki&neighborhood=%s' % n._id,
+ extra_environ=dict(username='test-admin'))
+ assert '<p><em>hello</em></p>' in r, r
+
+ # test-user and *anonymous get different errors, but that's ok, neither work:
+ self.app.get(
+ '/nf/markdown_to_html?markdown=*hello*&project=test&app=wiki&neighborhood=%s' % n._id,
+ extra_environ=dict(username='test-user'),
+ status=403)
+ self.app.get(
+ '/nf/markdown_to_html?markdown=*hello*&project=test&app=wiki&neighborhood=%s' % n._id,
+ extra_environ=dict(username='*anonymous'),
+ status=302)
+
def test_markdown_syntax(self):
with LogCapture(level=logging.INFO) as logs:
r = self.app.get('/nf/markdown_syntax')
diff --git a/Allura/allura/tests/functional/test_root.py b/Allura/allura/tests/functional/test_root.py
index 1c0b8c8f7..b3ca1139b 100644
--- a/Allura/allura/tests/functional/test_root.py
+++ b/Allura/allura/tests/functional/test_root.py
@@ -91,11 +91,12 @@ def test_neighborhood(self):
assert cat_links[0].find('a').get('href') == '/browse/clustering'
assert cat_links[0].find('a').find('span').string == 'Clustering'
+ @td.with_wiki
def test_validation(self):
# this is not configured ON currently, so adding an individual test to get coverage of the validator itself
with mock.patch.dict(os.environ, ALLURA_VALIDATION='all'):
self.app.get('/neighborhood')
- self.app.get('/nf/markdown_to_html?markdown=aaa&project=test&app=bugs&neighborhood=%s'
+ self.app.get('/nf/markdown_to_html?markdown=aaa&project=test&app=wiki&neighborhood=%s'
% M.Neighborhood.query.get(name='Projects')._id,
validate_chunk=True)
(allura) 18/33: [#8607] harden attachment upload
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 273ef2ff5b5b31957577a2a9a561046476224968
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue May 19 12:36:38 2026 -0400
[#8607] harden attachment upload
---
Allura/allura/app.py | 2 +-
Allura/allura/lib/helpers.py | 10 ++++++++++
Allura/allura/lib/utils.py | 2 +-
Allura/allura/model/attachments.py | 2 +-
Allura/allura/model/discuss.py | 2 +-
Allura/allura/tests/model/test_filesystem.py | 23 +++++++++++++++++++++++
ForgeFiles/forgefiles/model/files.py | 2 +-
ForgeTracker/forgetracker/model/ticket.py | 2 +-
ForgeWiki/forgewiki/model/wiki.py | 2 +-
9 files changed, 40 insertions(+), 7 deletions(-)
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 78798f7f5..22762f33e 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -808,7 +808,7 @@ def save_attachments(self, path, attachments):
for attachment in attachments:
attachment_path = os.path.join(
path,
- os.path.basename(attachment.filename)
+ h.safe_filename(attachment.filename)
)
with open(attachment_path.encode('utf8', 'replace'), 'wb') as fl:
fl.write(attachment.rfile().read())
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 33d180790..07204583f 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -272,6 +272,16 @@ def encodings():
return _attempt_encodings(s, encodings())
+def safe_filename(filename, fallback='unnamed'):
+ # strip slashes and backslashes and null bytes for good measure
+ # (os.path.basename on Linux treats backslashes as regular characters)
+ filename = really_unicode(filename).replace('\\', '/').replace('\x00', '')
+ filename = os.path.basename(filename)
+ if filename in ('', '.', '..'):
+ filename = fallback
+ return filename
+
+
def find_user(email):
from allura import model as M
return M.User.by_email_address(email)
diff --git a/Allura/allura/lib/utils.py b/Allura/allura/lib/utils.py
index a7b416be8..7dd73f4ed 100644
--- a/Allura/allura/lib/utils.py
+++ b/Allura/allura/lib/utils.py
@@ -506,7 +506,7 @@ def serve_file(fp, filename, content_type, last_modified=None,
if etag:
etag_cache(etag)
tg.response.headers['Content-Type'] = ''
- tg.response.content_type = str(content_type)
+ tg.response.content_type = content_type.split('\n', 1)[0].split('\r', 1)[0] # enforce single line, no header splitting
tg.response.cache_expires = cache_expires or asint(
tg.config.get('files_expires_header_secs', 60 * 60))
tg.response.last_modified = last_modified
diff --git a/Allura/allura/model/attachments.py b/Allura/allura/model/attachments.py
index cc91468f6..06d6bcaf6 100644
--- a/Allura/allura/model/attachments.py
+++ b/Allura/allura/model/attachments.py
@@ -67,7 +67,7 @@ def metadata_for(cls, artifact):
@classmethod
def save_attachment(cls, filename, fp, content_type=None, **kwargs):
- filename = h.really_unicode(filename)
+ filename = h.safe_filename(filename)
thumbnail_meta = dict(type="thumbnail", app_config_id=c.app.config._id)
thumbnail_meta.update(kwargs)
original_meta = dict(type="attachment", app_config_id=c.app.config._id)
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index 6c31a8abc..028c2cb43 100644
--- a/Allura/allura/model/discuss.py
+++ b/Allura/allura/model/discuss.py
@@ -204,7 +204,7 @@ def attachment_for_export(self, page):
str(self.artifact._id),
self._id,
page.slug,
- os.path.basename(attach.filename))
+ h.safe_filename(attach.filename))
) for attach in page.attachments]
def attachments_for_json(self, page):
diff --git a/Allura/allura/tests/model/test_filesystem.py b/Allura/allura/tests/model/test_filesystem.py
index 844a7b7b2..802ad8ffb 100644
--- a/Allura/allura/tests/model/test_filesystem.py
+++ b/Allura/allura/tests/model/test_filesystem.py
@@ -214,6 +214,29 @@ def test_attachment_name_encoding(self):
assert not isinstance(attachment, tuple) # tuple is for (img, thumb) pairs
assert attachment.filename == 'Strukturpr\xfcfung.dvi'
+ def test_attachment_filename_path_traversal_sanitization(self):
+ c.app.config._id = None
+ attachment = M.BaseAttachment.save_attachment(
+ '../../../etc/passwd', BytesIO(b'evil data'),
+ save_original=True)
+ assert attachment.filename == 'passwd'
+
+ def test_attachment_filename_windows_path_sanitization(self):
+ # backslashes are not separators on Linux but are on Windows, so a zip
+ # entry like '..\\..\\evil.exe' would zip-slip on Windows extraction
+ c.app.config._id = None
+ attachment = M.BaseAttachment.save_attachment(
+ '..\\..\\..\\Windows\\evil.exe', BytesIO(b'evil data'),
+ save_original=True)
+ assert attachment.filename == 'evil.exe'
+
+ def test_attachment_filename_dot_sanitization(self):
+ c.app.config._id = None
+ for bad in ('..', '.', '', 'foo/', 'bar\\'):
+ attachment = M.BaseAttachment.save_attachment(
+ bad, BytesIO(b'data'), save_original=True)
+ assert attachment.filename == 'unnamed'
+
def _assert_content(self, f, content):
result = f.rfile().read()
assert result == content, result
diff --git a/ForgeFiles/forgefiles/model/files.py b/ForgeFiles/forgefiles/model/files.py
index ee421a4b9..7d285aaa1 100755
--- a/ForgeFiles/forgefiles/model/files.py
+++ b/ForgeFiles/forgefiles/model/files.py
@@ -213,7 +213,7 @@ def readme(self):
@classmethod
def save_attachment(cls, filename, fp, content_type=None, **kwargs):
- filename = h.really_unicode(filename)
+ filename = h.safe_filename(filename)
original_meta = dict(type="project_file", app_config_id=c.app.config._id, project_id=c.project._id)
original_meta.update(kwargs)
fp.seek(0)
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index bbec87215..a2e51a334 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -1187,7 +1187,7 @@ def attachments_for_export(self):
path=os.path.join(
self.app_config.options.mount_point,
str(self._id),
- os.path.basename(attach.filename))) for attach in self.attachments]
+ h.safe_filename(attach.filename))) for attach in self.attachments]
def attachments_for_json(self):
return [dict(bytes=attach.length,
diff --git a/ForgeWiki/forgewiki/model/wiki.py b/ForgeWiki/forgewiki/model/wiki.py
index 712857eaf..91da0ca5e 100644
--- a/ForgeWiki/forgewiki/model/wiki.py
+++ b/ForgeWiki/forgewiki/model/wiki.py
@@ -145,7 +145,7 @@ def attachments_for_export(self):
path=os.path.join(
self.app_config.options.mount_point,
str(self._id),
- os.path.basename(attach.filename))) for attach in self.attachments]
+ h.safe_filename(attach.filename))) for attach in self.attachments]
def attachments_for_json(self):
return [dict(bytes=attach.length,
(allura) 25/33: [#8607] be extra safe with display names
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit d96c2120d2d99319c397f8932aa1b4df97acdae8
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Thu May 21 18:09:57 2026 -0400
[#8607] be extra safe with display names
---
Allura/allura/controllers/auth.py | 5 +++++
Allura/allura/lib/validators.py | 9 +++++++++
Allura/allura/lib/widgets/forms.py | 2 +-
Allura/allura/tests/functional/test_auth.py | 22 ++++++++++++++++++++++
4 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 0917f19df..2db61175c 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -806,6 +806,11 @@ def update(self, preferences=None, **kw):
if not preferences.get('display_name'):
flash("Display Name cannot be empty.", 'error')
redirect('.')
+ try:
+ preferences['display_name'] = V.DisplayName(not_empty=True).to_python(preferences['display_name'])
+ except fe.Invalid as e:
+ flash(str(e), 'error')
+ redirect('.')
old = c.user.get_pref('display_name')
c.user.set_pref('display_name', preferences['display_name'])
if old != preferences['display_name']:
diff --git a/Allura/allura/lib/validators.py b/Allura/allura/lib/validators.py
index 814fa920b..e9d81fa91 100644
--- a/Allura/allura/lib/validators.py
+++ b/Allura/allura/lib/validators.py
@@ -133,6 +133,15 @@ class UnicodeString(fev.UnicodeString):
String = UnicodeString if str is str else fev.ByteString
+class DisplayName(UnicodeString):
+ def _convert_to_python(self, value, state):
+ # prevent < and line breaks just to be sure HTML and email outputs are safe
+ value = super()._convert_to_python(value, state)
+ if value and re.search(r'[<\r\n]', value):
+ raise fev.Invalid('Display Name cannot contain < or line breaks', value, state)
+ return value
+
+
class Ming(fev.FancyValidator):
def __init__(self, cls, **kw):
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index 59cd1c0f0..2d0e38391 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -752,7 +752,7 @@ def fields(self):
ew.TextField(
name='display_name',
label='Displayed Name',
- validator=V.UnicodeString(not_empty=True)),
+ validator=V.DisplayName(not_empty=True)),
username,
]
if asbool(config.get('auth.require_email_addr', False)):
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 8338a2ecc..d323137bf 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -806,6 +806,19 @@ def test_prefs(self, gen_message_id, sendsimplemail):
},
extra_environ=dict(username='test-admin'))
+ @td.with_user_project('test-admin')
+ def test_prefs_display_name_rejects_unsafe_chars(self):
+ self.app.get('/').follow()
+ for bad in ['Admin <script>', 'Admin\nBcc: evil@x.com', 'Admin\r\nfoo']:
+ r = self.app.post('/auth/preferences/update',
+ params={'preferences.display_name': bad,
+ '_csrf_token': self.app.cookies['_csrf_token'],
+ },
+ extra_environ=dict(username='test-admin'))
+ assert 'cannot contain' in self.webflash(r)
+ user = M.User.query.get(username='test-admin')
+ assert user.get_pref('display_name') == 'Test Admin'
+
@td.with_user_project('test-admin')
@patch('allura.tasks.mail_tasks.sendsimplemail')
@patch('allura.lib.helpers.gen_message_id')
@@ -1096,6 +1109,15 @@ def test_create_account(self):
_csrf_token=self.app.cookies['_csrf_token']), antispam=True,
status=302)
+ def test_create_account_display_name_rejects_unsafe_chars(self):
+ self.app.get('/').follow()
+ r = self.app.post('/auth/save_new',
+ params=dict(username='aaa', pw='12345678', pw2='12345678',
+ display_name='Bob <ev...@x.com>',
+ _csrf_token=self.app.cookies['_csrf_token']))
+ assert 'cannot contain' in r
+ assert M.User.query.get(username='aaa') is None
+
def test_create_account_require_email(self):
self.app.get('/').follow() # establish session
with h.push_config(config, **{'auth.require_email_addr': 'false'}):
(allura) 06/33: [#8607] add comments, add test
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 8ab23957448e85b1549bb240728d4239c933fb43
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 13:05:16 2026 -0400
[#8607] add comments, add test
---
Allura/allura/lib/widgets/resources/js/combobox.js | 2 +-
Allura/allura/tests/functional/test_home.py | 9 +++++++
Allura/allura/tests/test_helpers.py | 31 ++++++++++++++++++++++
3 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/Allura/allura/lib/widgets/resources/js/combobox.js b/Allura/allura/lib/widgets/resources/js/combobox.js
index 782147620..9c6762bf6 100644
--- a/Allura/allura/lib/widgets/resources/js/combobox.js
+++ b/Allura/allura/lib/widgets/resources/js/combobox.js
@@ -140,7 +140,7 @@
input.autocomplete('instance')._renderItem = function(ul, item) {
return $('<li>')
.data('item.autocomplete', item)
- .append('<a>' + item.label + '</a>')
+ .append('<a>' + item.label + '</a>') // escape_html not needed, its already used above
.appendTo(ul);
};
diff --git a/Allura/allura/tests/functional/test_home.py b/Allura/allura/tests/functional/test_home.py
index b98c5ba7d..8f4047320 100644
--- a/Allura/allura/tests/functional/test_home.py
+++ b/Allura/allura/tests/functional/test_home.py
@@ -191,6 +191,15 @@ def test_users(self):
}]
assert j['options'] == expected
+ def test_users_json_endpoint_escaped(self):
+ # escaping also tested by test_json_encoding_directly but that is more low-level this is more functional
+
+ M.User.by_username('test-admin').display_name = 'Test Admin <b>foo</b>'
+ ThreadLocalODMSession.flush_all()
+ r = self.app.get('/p/test/users')
+ assert r.json['options'][0]['label'] == 'Test Admin <b>foo</b> (test-admin)' # JSON interpreted ok
+ assert 'Test Admin \\u003Cb>foo\\u003C/b> (test-admin)' in r.text # raw value is escaped
+
def test_members(self):
nbhd = M.Neighborhood.query.get(name='Projects')
self.app.post('/admin/groups/create', params={'name': 'B_role'})
diff --git a/Allura/allura/tests/test_helpers.py b/Allura/allura/tests/test_helpers.py
index 9d8f64c22..b442842d1 100644
--- a/Allura/allura/tests/test_helpers.py
+++ b/Allura/allura/tests/test_helpers.py
@@ -271,6 +271,27 @@ def test_render_any_markup_plain():
'readme.txt', '<b>blah</b>\n<script>alert(1)</script>\nfoo') ==
'<pre><b>blah</b>\n<script>alert(1)</script>\nfoo</pre>')
+def test_render_any_markup_plain_code_mode_inline():
+ assert (
+ h.render_any_markup(
+ 'readme.txt', '<b>blah</b>\n<script>alert(1)</script>\nfoo',
+ code_mode=True, linenumbers_style=h.INLINE) ==
+ '<div class="codehilite"><pre><span id="l1" class="code_block"><span class="lineno">1</span> '
+ '<b>blah</b></span><span id="l2" class="code_block"><span class="lineno">2</span> '
+ '<script>alert(1)</script></span><span id="l3" class="code_block"><span class="lineno">3</span> '
+ 'foo</span></pre></div>')
+
+def test_render_any_markup_plain_code_mode_table():
+ assert (
+ h.render_any_markup(
+ 'readme.txt', '<b>blah</b>\n<script>alert(1)</script>\nfoo',
+ code_mode=True, linenumbers_style=h.TABLE) ==
+ '<table class="codehilitetable"><tbody><tr><td class="linenos"><div class="linenodiv"><pre>1\n'
+ '2\n'
+ '3</pre></div></td><td class="code"><div class="codehilite"><pre><span id="l1" class="code_block"><b>blah</b>\n'
+ '</span><span id="l2" class="code_block"><script>alert(1)</script>\n'
+ '</span><span id="l3" class="code_block">foo</span></pre></div></td></tr></tbody></table>'
+ )
def test_render_any_markup_formatting():
assert (str(h.render_any_markup('README.md', '### foo\n'
@@ -280,6 +301,16 @@ def test_render_any_markup_formatting():
'<script></span>alert(1)<span class="nt">'
'</script></span>bar\n</code></pre></div>\n</div>')
+def test_render_any_markup_rst():
+ assert (str(h.render_any_markup('README.rst', 'foo\n===\n\nbar\n<script>alert(1)</script>')) ==
+ '<div class="document">\n'
+ '<div class="section" id="foo">\n'
+ '<h1>foo</h1>\n'
+ '<p>bar\n'
+ '<script>alert(1)</script></p>\n'
+ '</div>\n'
+ '</div>\n')
+
def test_render_any_markdown_encoding():
# send encoded content in, make sure it converts it to actual unicode object which Markdown lib needs
(allura) 28/33: [#8607] remove useless try/except/raise
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit d692742a1bd9f8f5fc66c9c8a19076ba5a94d465
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 22 15:49:24 2026 -0400
[#8607] remove useless try/except/raise
---
.../forgediscussion/controllers/root.py | 28 +++++++---------------
1 file changed, 9 insertions(+), 19 deletions(-)
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index 2c985d8f6..bac7196ce 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -319,15 +319,10 @@ def validate_import(self, doc=None, username_mapping=None, **kw):
require_access(c.project, 'admin')
if username_mapping is None:
username_mapping = {}
- try:
- doc = json.loads(doc)
- warnings, doc = import_support.validate_import(
- doc, username_mapping)
- return dict(warnings=warnings, errors=[])
- except Exception as e:
- raise
- log.exception(e)
- return dict(status=False, errors=[repr(e)])
+ doc = json.loads(doc)
+ warnings, doc = import_support.validate_import(
+ doc, username_mapping)
+ return dict(warnings=warnings, errors=[])
@expose('json:')
def perform_import(
@@ -339,16 +334,11 @@ def perform_import(
if not c.api_token.can_import_forum():
log.error('Import capability is not enabled for %s', c.project.shortname)
raise exc.HTTPForbidden(detail='Import is not allowed')
- try:
- doc = json.loads(doc)
- username_mapping = json.loads(username_mapping)
- warnings = import_support.perform_import(
- doc, username_mapping, default_username, create_users)
- return dict(warnings=warnings, errors=[])
- except Exception as e:
- raise
- log.exception(e)
- return dict(status=False, errors=[str(e)])
+ doc = json.loads(doc)
+ username_mapping = json.loads(username_mapping)
+ warnings = import_support.perform_import(
+ doc, username_mapping, default_username, create_users)
+ return dict(warnings=warnings, errors=[])
class ForumRestController(BaseController):
(allura) 26/33: [#8607] svn tarball path safety
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 9157ab6f2fc1adfd4f0527eb43977407a371fd24
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Thu May 21 18:12:33 2026 -0400
[#8607] svn tarball path safety
---
ForgeSVN/forgesvn/model/svn.py | 3 ++-
ForgeSVN/forgesvn/tests/model/test_repository.py | 8 ++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/ForgeSVN/forgesvn/model/svn.py b/ForgeSVN/forgesvn/model/svn.py
index 278f4bf4b..7b20053ea 100644
--- a/ForgeSVN/forgesvn/model/svn.py
+++ b/ForgeSVN/forgesvn/model/svn.py
@@ -681,7 +681,8 @@ def get_changes(self, oid):
def _tarball_path_clean(self, path, rev=None):
if path:
- return path.strip('/')
+ parts = [p for p in path.strip('/').split('/') if p and p != '..']
+ return '/'.join(parts)
else:
trunk_exists = svn_path_exists('file://{}{}/{}'.format(self._repo.fs_path, self._repo.name, 'trunk'), rev)
if trunk_exists:
diff --git a/ForgeSVN/forgesvn/tests/model/test_repository.py b/ForgeSVN/forgesvn/tests/model/test_repository.py
index cf3fdfddc..9254a9ef7 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -492,6 +492,14 @@ def test_tarball_paths(self):
ignore_errors=True)
shutil.rmtree(tarball_path, ignore_errors=True)
+ def test_tarball_path_clean_strips_traversal(self):
+ clean = self.repo._impl._tarball_path_clean
+ assert clean('trunk/../../other-repo') == 'trunk/other-repo'
+ assert clean('/../etc/passwd/') == 'etc/passwd'
+ assert clean('..') == ''
+ assert clean('trunk//sub') == 'trunk/sub'
+ assert clean('trunk/sub') == 'trunk/sub'
+
def test_is_empty(self):
assert not self.repo.is_empty()
with TempDirectory() as d:
(allura) 08/33: [#8607] move |safe to be directly on the string source
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 523be8e9c75412bc9df95e66a50d45ce70d63a80
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Fri May 15 17:53:43 2026 -0400
[#8607] move |safe to be directly on the string source
---
Allura/allura/ext/admin/templates/project_admin.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Allura/allura/ext/admin/templates/project_admin.html b/Allura/allura/ext/admin/templates/project_admin.html
index fafdc2628..ba3e45b7f 100644
--- a/Allura/allura/ext/admin/templates/project_admin.html
+++ b/Allura/allura/ext/admin/templates/project_admin.html
@@ -62,7 +62,7 @@
},
{
"title": "Start coding",
- "content": "Commit your code to your repo. Or, if you have code somewhere else, <a href='ext/import/'>import it</a> or add link to it (add an 'External Link' tool).",
+ "content": "Commit your code to your repo. Or, if you have code somewhere else, <a href='ext/import/'>import it</a> or add link to it (add an 'External Link' tool)."|safe,
"target": ".tool-git-32,.tool-hg-32,.tool-svn-32",
"placement": "bottom",
"xOffset": -17,
@@ -88,7 +88,7 @@
{% for step in tour.steps %}
<div class="tour-step-info show-for-large">
<h2>{{ step.title }}</h2>
- <p>{{ step.content|safe }}
+ <p>{{ step.content }}
</p>
<div class="hopscotch-bubble not-really-hopscotch-bubble">
<button class="tour-show hopscotch-nav-button next" data-tour-id="{{ loop.index0 }}">Show Me</button>
(allura) 19/33: [#8607] add X-Content-Type-Options header
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 3c245d4ca6d4a83c6afeff2eb64d37cf9f222953
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue May 19 17:28:11 2026 -0400
[#8607] add X-Content-Type-Options header
---
Allura/allura/lib/custom_middleware.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/Allura/allura/lib/custom_middleware.py b/Allura/allura/lib/custom_middleware.py
index a2c672b9e..5767f4ea7 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -566,6 +566,9 @@ def __call__(self, environ, start_response):
resp.headers.add('Cross-Origin-Embedder-Policy', self.config['cross_origin_embedder_policy'])
if self.config.get('cross_origin_resource_policy'):
resp.headers.add('Cross-Origin-Resource-Policy', self.config['cross_origin_resource_policy'])
+
+ resp.headers.add('X-Content-Type-Options', 'nosniff')
+
return resp(environ, start_response)
(allura) 31/33: [#8607] solr: move **kw to specific params for all other tools using common search_app helper
Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8607
in repository https://gitbox.apache.org/repos/asf/allura.git
commit ec1c4303b5bf7e009f3d91b8b955a82690ce5fe3
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Tue May 26 13:07:13 2026 -0400
[#8607] solr: move **kw to specific params for all other tools using common search_app helper
---
Allura/allura/controllers/search.py | 17 +++++++-------
Allura/allura/ext/search/search_main.py | 19 +++++++--------
Allura/allura/lib/search.py | 18 +++++++--------
Allura/allura/tests/unit/test_solr.py | 8 +++----
ForgeBlog/forgeblog/main.py | 27 +++++++++++-----------
ForgeChat/forgechat/main.py | 19 ++++++++-------
.../forgediscussion/controllers/root.py | 21 ++++++++---------
ForgeShortUrl/forgeshorturl/main.py | 23 +++++++++---------
ForgeWiki/forgewiki/wiki_main.py | 25 ++++++++++----------
9 files changed, 89 insertions(+), 88 deletions(-)
diff --git a/Allura/allura/controllers/search.py b/Allura/allura/controllers/search.py
index 31c5f1e48..a0dc62a5d 100644
--- a/Allura/allura/controllers/search.py
+++ b/Allura/allura/controllers/search.py
@@ -41,15 +41,16 @@ class SearchController(BaseController):
@validate(dict(q=V.UnicodeString(),
history=V.StringBool(if_empty=False)))
@with_trailing_slash
- def index(self, q=None, history=False, **kw):
+ def index(self, q=None, history=False, limit=None, page=0, sort='score desc'):
c.search_results = W.search_results
- search_params = kw
- search_params.update({
- 'q': q,
- 'history': history,
- 'app': False,
- })
- d = search_app(**search_params)
+ d = search_app(
+ q=q,
+ history=history,
+ app=False,
+ limit=limit,
+ page=page,
+ sort=sort,
+ )
d['search_comments_disable'] = True
d['hide_app_project_switcher'] = True
return d
diff --git a/Allura/allura/ext/search/search_main.py b/Allura/allura/ext/search/search_main.py
index 72c0c58e0..b95f17a60 100644
--- a/Allura/allura/ext/search/search_main.py
+++ b/Allura/allura/ext/search/search_main.py
@@ -70,22 +70,23 @@ class SearchController(BaseController):
@validate(dict(q=V.UnicodeString(),
history=V.StringBool(if_empty=False)))
@with_trailing_slash
- def index(self, q=None, history=None, **kw):
+ def index(self, q=None, history=None, limit=None, page=0, sort='score desc'):
c.search_results = SearchResults()
c.help_modal = SearchHelp(comments=False)
pids = [c.project._id] + [p._id for p in c.project.subprojects]
project_match = ' OR '.join(map(str, pids))
- search_params = kw
- search_params.update({
- 'q': q,
- 'history': history,
- 'app': False,
- 'fq': [
+ d = search_app(
+ q=q,
+ history=history,
+ app=False,
+ fq=[
'project_id_s:(%s)' % project_match,
'-deleted_b:true',
],
- })
- d = search_app(**search_params)
+ limit=limit,
+ page=page,
+ sort=sort,
+ )
d['search_comments_disable'] = True
d['hide_app_project_switcher'] = True
return d
diff --git a/Allura/allura/lib/search.py b/Allura/allura/lib/search.py
index 147c30c47..6dd6693db 100644
--- a/Allura/allura/lib/search.py
+++ b/Allura/allura/lib/search.py
@@ -239,7 +239,9 @@ def site_admin_search(model, q, field, **kw):
return search(q, fq=fq, ignore_errors=False, **kw)
-def search_app(q='', fq=None, app=True, **kw):
+def search_app(q='', fq=None, app: bool = True, history: bool = False, project: bool = False,
+ search_comments: bool = False, limit=None, page=0, default=25,
+ allowed_types: list | None = None, parser=None, sort='score desc'):
"""Helper for app/project search.
Uses dismax query parser. Matches on `title` and `text`. Handles paging, sorting, etc
@@ -247,19 +249,15 @@ def search_app(q='', fq=None, app=True, **kw):
from allura.model import ArtifactReference
from allura.lib.security import has_access
- history = kw.pop('history', None)
- if app and kw.pop('project', False):
+ if app and project:
# Used from app's search controller. If `project` is True, redirect to
# 'entire project search' page
redirect(c.project.url() + 'search/?' +
urlencode(dict(q=q, history=history)))
- search_comments = kw.pop('search_comments', None)
- limit = kw.pop('limit', None)
- page = kw.pop('page', 0)
- default = kw.pop('default', 25)
- allowed_types = kw.pop('allowed_types', [])
- parser = kw.pop('parser', None)
- sort = kw.pop('sort', 'score desc')
+ if allowed_types is None:
+ allowed_types = []
+ if app and not allowed_types:
+ raise ValueError('app search requires allowed_types')
fq = fq if fq else []
search_error = None
results = []
diff --git a/Allura/allura/tests/unit/test_solr.py b/Allura/allura/tests/unit/test_solr.py
index 83d6b167f..06c45f285 100644
--- a/Allura/allura/tests/unit/test_solr.py
+++ b/Allura/allura/tests/unit/test_solr.py
@@ -177,10 +177,10 @@ def test_basic(self, req, url_fn):
req.path = '/test/search'
url_fn.side_effect = ['the-score-url', 'the-date-url']
with h.push_context('test', 'wiki', neighborhood='Projects'):
- resp = search_app(q='foo bar')
+ resp = search_app(q='foo bar', allowed_types=['WikiPage', 'WikiPage Snapshot'])
assert resp == dict(
q='foo bar',
- history=None,
+ history=False,
results=[],
count=0,
limit=25,
@@ -216,11 +216,11 @@ def test_escape_solr_text(self, req, url_fn, solr_search):
results.__len__ = lambda self: len(results.docs)
solr_search.return_value = results
with h.push_context('test', 'wiki', neighborhood='Projects'):
- resp = search_app(q='foo bar')
+ resp = search_app(q='foo bar', allowed_types=['WikiPage', 'WikiPage Snapshot'])
assert resp == dict(
q='foo bar',
- history=None,
+ history=False,
count=2,
limit=25,
page=0,
diff --git a/ForgeBlog/forgeblog/main.py b/ForgeBlog/forgeblog/main.py
index 15614b7ec..9dce9b057 100644
--- a/ForgeBlog/forgeblog/main.py
+++ b/ForgeBlog/forgeblog/main.py
@@ -296,21 +296,22 @@ def index(self, page=0, limit=None, **kw):
history=validators.StringBool(if_empty=False),
search_comments=validators.StringBool(if_empty=False),
project=validators.StringBool(if_empty=False)))
- def search(self, q=None, history=None, search_comments=None, project=None, limit=None, page=0, **kw):
+ def search(self, q=None, history=None, search_comments=None, project=None,
+ limit=None, page=0, sort='score desc', parser=None):
c.search_results = W.search_results
c.help_modal = W.help_modal
- search_params = kw
- search_params.update({
- 'q': q or '',
- 'history': history,
- 'search_comments': search_comments,
- 'project': project,
- 'limit': limit,
- 'page': page,
- 'allowed_types': ['Blog Post', 'Blog Post Snapshot'],
- 'fq': ['state_s:published']
- })
- return search_app(**search_params)
+ return search_app(
+ q=q or '',
+ history=history,
+ search_comments=search_comments,
+ project=project,
+ limit=limit,
+ page=page,
+ sort=sort,
+ parser=parser,
+ allowed_types=['Blog Post', 'Blog Post Snapshot'],
+ fq=['state_s:published'],
+ )
@expose('jinja:forgeblog:templates/blog/edit_post.html')
@without_trailing_slash
diff --git a/ForgeChat/forgechat/main.py b/ForgeChat/forgechat/main.py
index 8d7ccdeec..b09d0b31d 100644
--- a/ForgeChat/forgechat/main.py
+++ b/ForgeChat/forgechat/main.py
@@ -151,21 +151,20 @@ def index(self, **kw):
@expose('jinja:forgechat:templates/chat/search.html')
@validate(dict(q=v.UnicodeString(if_empty=None),
project=validators.StringBool(if_empty=False)))
- def search(self, q=None, project=None, limit=None, page=0, **kw):
+ def search(self, q=None, project=None, limit=None, page=0, sort='score desc'):
c.search_results = SearchResults()
c.help_modal = SearchHelp(comments=False, history=False,
fields={'sender_t': 'username',
'text': '"Message text"',
})
- search_params = kw
- search_params.update({
- 'q': q or '',
- 'project': project,
- 'limit': limit,
- 'page': page,
- 'allowed_types': ['Chat Message'],
- })
- d = search_app(**search_params)
+ d = search_app(
+ q=q or '',
+ project=project,
+ limit=limit,
+ page=page,
+ sort=sort,
+ allowed_types=['Chat Message'],
+ )
d['search_comments_disable'] = True
d['search_history_disable'] = True
return d
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index bac7196ce..5caaa9c4d 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -163,19 +163,18 @@ def save_new_topic(self, subject=None, text=None, forum=None, subscribe=False, *
project=validators.StringBool(if_empty=False),
limit=validators.Int(if_empty=None, if_invalid=None),
page=validators.Int(if_empty=0, if_invalid=0)))
- def search(self, q=None, history=None, project=None, limit=None, page=0, **kw):
+ def search(self, q=None, history=None, project=None, limit=None, page=0, sort='score desc'):
c.search_results = self.W.search_results
c.help_modal = self.W.search_help
- search_params = kw
- search_params.update({
- 'q': q or '',
- 'history': history,
- 'project': project,
- 'limit': limit,
- 'page': page,
- 'allowed_types': ['Post', 'Post Snapshot', 'Discussion', 'Thread'],
- })
- d = search_app(**search_params)
+ d = search_app(
+ q=q or '',
+ history=history,
+ project=project,
+ limit=limit,
+ page=page,
+ sort=sort,
+ allowed_types=['Post', 'Post Snapshot', 'Discussion', 'Thread'],
+ )
d['search_comments_disable'] = True
return d
diff --git a/ForgeShortUrl/forgeshorturl/main.py b/ForgeShortUrl/forgeshorturl/main.py
index 72dbaf277..904a5bc89 100644
--- a/ForgeShortUrl/forgeshorturl/main.py
+++ b/ForgeShortUrl/forgeshorturl/main.py
@@ -160,20 +160,21 @@ def index(self, page=0, limit=None, **kw):
@expose('jinja:forgeshorturl:templates/search.html')
@validate(dict(q=v.UnicodeString(if_empty=None),
project=validators.StringBool(if_empty=False)))
- def search(self, q=None, project=None, limit=None, page=0, **kw):
+ def search(self, q=None, project=None, limit=None, page=0, sort='score desc'):
c.search_results = W.search_results
c.help_modal = W.search_help
- search_params = kw
- search_params.update({
- 'q': q or '',
- 'project': project,
- 'limit': limit,
- 'page': page,
- 'allowed_types': ['ShortUrl'],
- })
+ fq = None
if not has_access(c.app, 'view_private'):
- search_params['fq'] = ['private_b:False']
- d = search_app(**search_params)
+ fq = ['private_b:False']
+ d = search_app(
+ q=q or '',
+ project=project,
+ limit=limit,
+ page=page,
+ sort=sort,
+ allowed_types=['ShortUrl'],
+ fq=fq,
+ )
d['search_comments_disable'] = True
d['search_history_disable'] = True
d['url_len'] = len(ShortUrl.build_short_url(c.app, short_name=''))
diff --git a/ForgeWiki/forgewiki/wiki_main.py b/ForgeWiki/forgewiki/wiki_main.py
index 4843b4758..d93d56feb 100644
--- a/ForgeWiki/forgewiki/wiki_main.py
+++ b/ForgeWiki/forgewiki/wiki_main.py
@@ -438,21 +438,22 @@ def new_page(self, title):
history=validators.StringBool(if_empty=False),
search_comments=validators.StringBool(if_empty=False),
project=validators.StringBool(if_empty=False)))
- def search(self, q=None, history=None, search_comments=None, project=None, limit=None, page=0, **kw):
+ def search(self, q=None, history=None, search_comments=None, project=None,
+ limit=None, page=0, sort='score desc', parser=None):
'local wiki search'
c.search_results = W.search_results
c.help_modal = W.help_modal
- search_params = kw
- search_params.update({
- 'q': q or '',
- 'history': history,
- 'search_comments': search_comments,
- 'project': project,
- 'limit': limit,
- 'page': page,
- 'allowed_types': ['WikiPage', 'WikiPage Snapshot'],
- })
- return search_app(**search_params)
+ return search_app(
+ q=q or '',
+ history=history,
+ search_comments=search_comments,
+ project=project,
+ limit=limit,
+ page=page,
+ sort=sort,
+ parser=parser,
+ allowed_types=['WikiPage', 'WikiPage Snapshot'],
+ )
@with_trailing_slash
@expose('jinja:forgewiki:templates/wiki/browse.html')