From 778db7d510ccb7d772d4ed6677325f784ed96f0b Mon Sep 17 00:00:00 2001 From: Hunternif Date: Sat, 15 Feb 2025 23:55:21 +0000 Subject: [PATCH 1/4] server: add privilege posts:view:unsafe --- server/config.yaml.dist | 1 + server/szurubooru/api/post_api.py | 2 ++ .../tests/api/test_post_retrieving.py | 26 ++++++++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/server/config.yaml.dist b/server/config.yaml.dist index 193aac3a..fa1b86e6 100644 --- a/server/config.yaml.dist +++ b/server/config.yaml.dist @@ -100,6 +100,7 @@ privileges: 'posts:reverse_search': regular 'posts:view': anonymous 'posts:view:featured': anonymous + 'posts:view:unsafe': regular 'posts:edit:content': power 'posts:edit:flags': regular 'posts:edit:notes': regular diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index daba7f7e..7883f5e9 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -114,6 +114,8 @@ def create_snapshots_for_post( def get_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: auth.verify_privilege(ctx.user, "posts:view") post = _get_post(params) + if post.safety == model.Post.SAFETY_UNSAFE: + auth.verify_privilege(ctx.user, "posts:view:unsafe") return _serialize_post(ctx, post) diff --git a/server/szurubooru/tests/api/test_post_retrieving.py b/server/szurubooru/tests/api/test_post_retrieving.py index ac984c24..a40ab0e9 100644 --- a/server/szurubooru/tests/api/test_post_retrieving.py +++ b/server/szurubooru/tests/api/test_post_retrieving.py @@ -14,6 +14,7 @@ def inject_config(config_injector): "privileges": { "posts:list": model.User.RANK_REGULAR, "posts:view": model.User.RANK_REGULAR, + "posts:view:unsafe": model.User.RANK_REGULAR, }, } ) @@ -73,7 +74,10 @@ def test_trying_to_use_special_tokens_without_logging_in( ): config_injector( { - "privileges": {"posts:list": "anonymous"}, + "privileges": { + "posts:list": "anonymous", + "posts:list:unsafe": "regular", + }, } ) with pytest.raises(errors.SearchError): @@ -125,3 +129,23 @@ def test_trying_to_retrieve_single_without_privileges( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), {"post_id": 999}, ) + + +def test_trying_to_retrieve_unsafe_without_privileges( + user_factory, context_factory, post_factory, config_injector +): + config_injector( + { + "privileges": { + "posts:view": "anonymous", + "posts:view:unsafe": "regular", + }, + } + ) + db.session.add(post_factory(id=1, safety=model.Post.SAFETY_UNSAFE)) + db.session.flush() + with pytest.raises(errors.AuthError): + api.post_api.get_post( + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), + {"post_id": 1}, + ) From fb763ada0fb5027f29f003f7dc3261c6b87f30a7 Mon Sep 17 00:00:00 2001 From: Hunternif Date: Sun, 16 Feb 2025 01:22:59 +0000 Subject: [PATCH 2/4] server: add privilege posts:list:unsafe, filter unsafe posts for users without privilege --- server/config.yaml.dist | 1 + .../search/configs/post_search_config.py | 27 +++++++++---- .../search/configs/test_post_search_config.py | 39 +++++++++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/server/config.yaml.dist b/server/config.yaml.dist index fa1b86e6..b55afad4 100644 --- a/server/config.yaml.dist +++ b/server/config.yaml.dist @@ -97,6 +97,7 @@ privileges: 'posts:create:anonymous': regular 'posts:create:identified': regular 'posts:list': anonymous + 'posts:list:unsafe': regular 'posts:reverse_search': regular 'posts:view': anonymous 'posts:view:featured': anonymous diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index 8d4672d4..8c2b5b9f 100644 --- a/server/szurubooru/search/configs/post_search_config.py +++ b/server/szurubooru/search/configs/post_search_config.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Optional, Tuple import sqlalchemy as sa from szurubooru import db, errors, model -from szurubooru.func import util +from szurubooru.func import auth, util from szurubooru.search import criteria, tokens from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( @@ -150,6 +150,15 @@ def _category_filter( return query.filter(expr) +def _safety_filter( + query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool +) -> SaQuery: + assert criterion + return search_util.create_str_filter( + model.Post.safety, _safety_transformer + )(query, criterion, negated) + + class PostSearchConfig(BaseSearchConfig): def __init__(self) -> None: self.user = None # type: Optional[model.User] @@ -208,6 +217,15 @@ class PostSearchConfig(BaseSearchConfig): return db.session.query(model.Post) def finalize_query(self, query: SaQuery) -> SaQuery: + if self.user and not auth.has_privilege(self.user, "posts:list:unsafe"): + # exclude unsafe posts: + query = _safety_filter( + query, + criteria.PlainCriterion( + model.Post.SAFETY_UNSAFE, model.Post.SAFETY_UNSAFE + ), + negated=True, + ) return query.order_by(model.Post.post_id.desc()) @property @@ -363,12 +381,7 @@ class PostSearchConfig(BaseSearchConfig): model.Post.last_feature_time ), ), - ( - ["safety", "rating"], - search_util.create_str_filter( - model.Post.safety, _safety_transformer - ), - ), + (["safety", "rating"], _safety_filter), (["note-text"], _note_filter), ( ["flag"], diff --git a/server/szurubooru/tests/search/configs/test_post_search_config.py b/server/szurubooru/tests/search/configs/test_post_search_config.py index b86fa273..e726f319 100644 --- a/server/szurubooru/tests/search/configs/test_post_search_config.py +++ b/server/szurubooru/tests/search/configs/test_post_search_config.py @@ -3,6 +3,12 @@ from datetime import datetime import pytest from szurubooru import db, errors, model, search +from szurubooru.func import cache + + +@pytest.fixture(autouse=True) +def purge_cache(): + cache.purge() @pytest.fixture @@ -915,3 +921,36 @@ def test_search_by_tag_category( ) db.session.flush() verify_unpaged(input, expected_post_ids) + + +def test_filter_unsafe_without_privilege( + auth_executor, + verify_unpaged, + post_factory, + config_injector, +): + config_injector( + { + "privileges": { + "posts:list:unsafe": model.User.RANK_REGULAR, + } + } + ) + post1 = post_factory(id=1) + post2 = post_factory(id=2, safety=model.Post.SAFETY_SKETCHY) + post3 = post_factory(id=3, safety=model.Post.SAFETY_UNSAFE) + db.session.add_all([post1, post2, post3]) + db.session.flush() + user = auth_executor() + user.rank = model.User.RANK_ANONYMOUS + verify_unpaged("", [1, 2]) + verify_unpaged("safety:safe", [1]) + verify_unpaged("safety:safe,sketchy", [1, 2]) + verify_unpaged("safety:safe,sketchy,unsafe", [1, 2]) + # adjust user's rank and retry + user.rank = model.User.RANK_REGULAR + cache.purge() + verify_unpaged("", [1, 2, 3]) + verify_unpaged("safety:safe", [1]) + verify_unpaged("safety:safe,sketchy", [1, 2]) + verify_unpaged("safety:safe,sketchy,unsafe", [1, 2, 3]) From 7e09f39cb9c6ad5ffb20411906c13aca74343617 Mon Sep 17 00:00:00 2001 From: Hunternif Date: Sun, 16 Feb 2025 01:59:39 +0000 Subject: [PATCH 3/4] server: fix tests for unsafe posts --- .../tests/api/test_post_retrieving.py | 1 + .../search/configs/test_post_search_config.py | 18 +++++++++--------- .../szurubooru/tests/search/test_executor.py | 11 ++++++++++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/server/szurubooru/tests/api/test_post_retrieving.py b/server/szurubooru/tests/api/test_post_retrieving.py index a40ab0e9..4daf11f0 100644 --- a/server/szurubooru/tests/api/test_post_retrieving.py +++ b/server/szurubooru/tests/api/test_post_retrieving.py @@ -15,6 +15,7 @@ def inject_config(config_injector): "posts:list": model.User.RANK_REGULAR, "posts:view": model.User.RANK_REGULAR, "posts:view:unsafe": model.User.RANK_REGULAR, + "posts:list:unsafe": model.User.RANK_REGULAR, }, } ) diff --git a/server/szurubooru/tests/search/configs/test_post_search_config.py b/server/szurubooru/tests/search/configs/test_post_search_config.py index e726f319..299b2ed2 100644 --- a/server/szurubooru/tests/search/configs/test_post_search_config.py +++ b/server/szurubooru/tests/search/configs/test_post_search_config.py @@ -60,7 +60,15 @@ def executor(): @pytest.fixture -def auth_executor(executor, user_factory): +def auth_executor(executor, user_factory, config_injector): + config_injector( + { + "privileges": { + "posts:list:unsafe": model.User.RANK_REGULAR, + } + } + ) + def wrapper(): auth_user = user_factory() db.session.add(auth_user) @@ -927,15 +935,7 @@ def test_filter_unsafe_without_privilege( auth_executor, verify_unpaged, post_factory, - config_injector, ): - config_injector( - { - "privileges": { - "posts:list:unsafe": model.User.RANK_REGULAR, - } - } - ) post1 = post_factory(id=1) post2 = post_factory(id=2, safety=model.Post.SAFETY_SKETCHY) post3 = post_factory(id=3, safety=model.Post.SAFETY_UNSAFE) diff --git a/server/szurubooru/tests/search/test_executor.py b/server/szurubooru/tests/search/test_executor.py index 4530beec..5c52f724 100644 --- a/server/szurubooru/tests/search/test_executor.py +++ b/server/szurubooru/tests/search/test_executor.py @@ -2,10 +2,19 @@ import unittest.mock import pytest -from szurubooru import search +from szurubooru import search, model from szurubooru.func import cache +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector( + { + "privileges": {"posts:list:unsafe": model.User.RANK_REGULAR}, + } + ) + + def test_retrieving_from_cache(): config = unittest.mock.MagicMock() with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( From 928f949e9e21bd399f26405d7343b4c5161dc193 Mon Sep 17 00:00:00 2001 From: Eva Date: Tue, 25 Mar 2025 17:52:58 +0100 Subject: [PATCH 4/4] server: prevent cache key collision Since search queries get cached, when a search performed by a privileged user is repeated by an unprivileged user, they will receive a listing that erroneously includes unsafe posts. The same is true the other way around, a tag search that is first performed by an anonymous user will cause any hidden posts for that query to not show up for the logged in user. This is because the initial search claims the cache key. --- server/szurubooru/search/configs/post_search_config.py | 5 +++++ server/szurubooru/search/executor.py | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index 8c2b5b9f..9898231c 100644 --- a/server/szurubooru/search/configs/post_search_config.py +++ b/server/szurubooru/search/configs/post_search_config.py @@ -228,6 +228,11 @@ class PostSearchConfig(BaseSearchConfig): ) return query.order_by(model.Post.post_id.desc()) + + @property + def can_list_unsafe(self) -> bool: + return self.user and auth.has_privilege(self.user, "posts:list:unsafe") + @property def id_column(self) -> SaColumn: return model.Post.post_id diff --git a/server/szurubooru/search/executor.py b/server/szurubooru/search/executor.py index a5ef9625..992f1dac 100644 --- a/server/szurubooru/search/executor.py +++ b/server/szurubooru/search/executor.py @@ -93,7 +93,10 @@ class Executor: if token.name == "random": disable_eager_loads = True - key = (id(self.config), hash(search_query), offset, limit) + + can_list_unsafe = getattr(self.config, "can_list_unsafe", False) + + key = (id(self.config), hash(search_query), offset, limit, can_list_unsafe) if cache.has(key): return cache.get(key)