diff options
Diffstat (limited to 'server/szurubooru')
| -rw-r--r-- | server/szurubooru/api/post_api.py | 2 | ||||
| -rw-r--r-- | server/szurubooru/search/configs/post_search_config.py | 498 | ||||
| -rw-r--r-- | server/szurubooru/tests/api/test_post_retrieving.py | 74 | ||||
| -rw-r--r-- | server/szurubooru/tests/search/configs/test_post_search_config.py | 39 |
4 files changed, 371 insertions, 242 deletions
diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index 4a629fe..4e89f54 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -117,6 +117,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/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index 9869843..9065539 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 metrics, util +from szurubooru.func import auth, metrics, util from szurubooru.search import criteria, tokens from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( @@ -169,15 +169,17 @@ def _create_metric_num_filter(name: str): pm = sa.orm.aliased(model.PostMetric) expr = t.name == name expr = expr & search_util.apply_num_criterion_to_column( - pm.value, criterion, search_util.float_transformer) + pm.value, criterion, search_util.float_transformer + ) if negated: expr = ~expr ret = ( - query - .join(pm, pm.post_id == model.Post.post_id) + query.join(pm, pm.post_id == model.Post.post_id) .join(t, t.tag_id == pm.tag_id) - .filter(expr)) + .filter(expr) + ) return ret + return wrapper @@ -189,13 +191,15 @@ def _metric_presence_filter( assert criterion t = sa.orm.aliased(model.TagName) tag_name_filter = search_util.apply_str_criterion_to_column( - t.name, criterion) + t.name, criterion + ) pm = sa.orm.aliased(model.PostMetric) subquery = ( db.session.query(pm.post_id) .join(t, t.tag_id == pm.tag_id) .filter(tag_name_filter) - .subquery()) + .subquery() + ) post_filter = model.Post.post_id.in_(subquery) if negated: post_filter = ~post_filter @@ -210,7 +214,8 @@ def _create_metric_sort_column(metric_name: str): .filter(pm.post_id == model.Post.post_id) .join(t, t.tag_id == pm.tag_id) .filter(t.name == metric_name) - .as_scalar()) + .as_scalar() + ) return ret @@ -242,6 +247,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] @@ -306,6 +320,17 @@ 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 @@ -324,234 +349,267 @@ class PostSearchConfig(BaseSearchConfig): @property def named_filters(self) -> Dict[str, Filter]: - filters = {"metric-" + name: _create_metric_num_filter(name) - for name in self.all_metric_names} - filters.update(util.unalias_dict( - [ - (["id"], search_util.create_num_filter(model.Post.post_id)), - ( - ["tag"], - search_util.create_subquery_filter( - model.Post.post_id, - model.PostTag.post_id, - model.TagName.name, - search_util.create_str_filter, - lambda subquery: subquery.join(model.Tag).join( - model.TagName + filters = { + "metric-" + name: _create_metric_num_filter(name) + for name in self.all_metric_names + } + filters.update( + util.unalias_dict( + [ + ( + ["id"], + search_util.create_num_filter(model.Post.post_id), + ), + ( + ["tag"], + search_util.create_subquery_filter( + model.Post.post_id, + model.PostTag.post_id, + model.TagName.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.Tag).join( + model.TagName + ), ), ), - ), - (["metric"], _metric_presence_filter), - (["score"], search_util.create_num_filter(model.Post.score)), - (["uploader", "upload", "submit"], _user_filter), - ( - ["comment"], - search_util.create_subquery_filter( - model.Post.post_id, - model.Comment.post_id, - model.User.name, - search_util.create_str_filter, - lambda subquery: subquery.join(model.User), + (["metric"], _metric_presence_filter), + ( + ["score"], + search_util.create_num_filter(model.Post.score), ), - ), - ( - ["fav"], - search_util.create_subquery_filter( - model.Post.post_id, - model.PostFavorite.post_id, - model.User.name, - search_util.create_str_filter, - lambda subquery: subquery.join(model.User), + (["uploader", "upload", "submit"], _user_filter), + ( + ["comment"], + search_util.create_subquery_filter( + model.Post.post_id, + model.Comment.post_id, + model.User.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.User), + ), ), - ), - (["liked"], _create_score_filter(1)), - (["disliked"], _create_score_filter(-1)), - ( - ["source"], - search_util.create_str_filter( - model.Post.source, _source_transformer + ( + ["fav"], + search_util.create_subquery_filter( + model.Post.post_id, + model.PostFavorite.post_id, + model.User.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.User), + ), ), - ), - ( - ["tag-count"], - search_util.create_num_filter(model.Post.tag_count), - ), - ( - ["comment-count"], - search_util.create_num_filter(model.Post.comment_count), - ), - ( - ["fav-count"], - search_util.create_num_filter(model.Post.favorite_count), - ), - ( - ["note-count"], - search_util.create_num_filter(model.Post.note_count), - ), - ( - ["relation-count"], - search_util.create_num_filter(model.Post.relation_count), - ), - ( - ["feature-count"], - search_util.create_num_filter(model.Post.feature_count), - ), - ( - ["type"], - search_util.create_str_filter( - model.Post.type, _type_transformer + (["liked"], _create_score_filter(1)), + (["disliked"], _create_score_filter(-1)), + ( + ["source"], + search_util.create_str_filter( + model.Post.source, _source_transformer + ), ), - ), - ( - ["content-checksum", "sha1"], - search_util.create_str_filter(model.Post.checksum), - ), - ( - ["md5"], - search_util.create_str_filter(model.Post.checksum_md5), - ), - ( - ["file-size"], - search_util.create_num_filter(model.Post.file_size), - ), - ( - ["image-width", "width"], - search_util.create_num_filter(model.Post.canvas_width), - ), - ( - ["image-height", "height"], - search_util.create_num_filter(model.Post.canvas_height), - ), - ( - ["image-area", "area"], - search_util.create_num_filter(model.Post.canvas_area), - ), - ( - ["image-aspect-ratio", "image-ar", "aspect-ratio", "ar"], - search_util.create_num_filter( - model.Post.canvas_aspect_ratio, - transformer=search_util.float_transformer, + ( + ["tag-count"], + search_util.create_num_filter(model.Post.tag_count), ), - ), - ( - ["creation-date", "creation-time", "date", "time"], - search_util.create_date_filter(model.Post.creation_time), - ), - ( - [ - "last-edit-date", - "last-edit-time", - "edit-date", - "edit-time", - ], - search_util.create_date_filter(model.Post.last_edit_time), - ), - ( - ["comment-date", "comment-time"], - search_util.create_date_filter( - model.Post.last_comment_creation_time + ( + ["comment-count"], + search_util.create_num_filter( + model.Post.comment_count + ), ), - ), - ( - ["fav-date", "fav-time"], - search_util.create_date_filter( - model.Post.last_favorite_time + ( + ["fav-count"], + search_util.create_num_filter( + model.Post.favorite_count + ), ), - ), - ( - ["feature-date", "feature-time"], - search_util.create_date_filter( - model.Post.last_feature_time + ( + ["note-count"], + search_util.create_num_filter(model.Post.note_count), ), - ), - ( - ["safety", "rating"], - search_util.create_str_filter( - model.Post.safety, _safety_transformer + ( + ["relation-count"], + search_util.create_num_filter( + model.Post.relation_count + ), ), - ), - (["note-text"], _note_filter), - ( - ["flag"], - search_util.create_str_filter( - model.Post.flags_string, _flag_transformer + ( + ["feature-count"], + search_util.create_num_filter( + model.Post.feature_count + ), ), - ), - (["pool"], _pool_filter), - (["similar"], _similar_filter), - (["category"], _category_filter), - ] - )) + ( + ["type"], + search_util.create_str_filter( + model.Post.type, _type_transformer + ), + ), + ( + ["content-checksum", "sha1"], + search_util.create_str_filter(model.Post.checksum), + ), + ( + ["md5"], + search_util.create_str_filter(model.Post.checksum_md5), + ), + ( + ["file-size"], + search_util.create_num_filter(model.Post.file_size), + ), + ( + ["image-width", "width"], + search_util.create_num_filter(model.Post.canvas_width), + ), + ( + ["image-height", "height"], + search_util.create_num_filter( + model.Post.canvas_height + ), + ), + ( + ["image-area", "area"], + search_util.create_num_filter(model.Post.canvas_area), + ), + ( + [ + "image-aspect-ratio", + "image-ar", + "aspect-ratio", + "ar", + ], + search_util.create_num_filter( + model.Post.canvas_aspect_ratio, + transformer=search_util.float_transformer, + ), + ), + ( + ["creation-date", "creation-time", "date", "time"], + search_util.create_date_filter( + model.Post.creation_time + ), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + search_util.create_date_filter( + model.Post.last_edit_time + ), + ), + ( + ["comment-date", "comment-time"], + search_util.create_date_filter( + model.Post.last_comment_creation_time + ), + ), + ( + ["fav-date", "fav-time"], + search_util.create_date_filter( + model.Post.last_favorite_time + ), + ), + ( + ["feature-date", "feature-time"], + search_util.create_date_filter( + model.Post.last_feature_time + ), + ), + (["safety", "rating"], _safety_filter), + (["note-text"], _note_filter), + ( + ["flag"], + search_util.create_str_filter( + model.Post.flags_string, _flag_transformer + ), + ), + (["pool"], _pool_filter), + (["similar"], _similar_filter), + (["category"], _category_filter), + ] + ) + ) return filters @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: - filters = {"metric-" + name: - (_create_metric_sort_column(name), self.SORT_ASC) - for name in self.all_metric_names} - filters.update(util.unalias_dict( - [ - ( - ["random"], - (sa.sql.expression.func.random(), self.SORT_NONE), - ), - (["id"], (model.Post.post_id, self.SORT_DESC)), - (["score"], (model.Post.score, self.SORT_DESC)), - (["tag-count"], (model.Post.tag_count, self.SORT_DESC)), - ( - ["comment-count"], - (model.Post.comment_count, self.SORT_DESC), - ), - (["fav-count"], (model.Post.favorite_count, self.SORT_DESC)), - (["note-count"], (model.Post.note_count, self.SORT_DESC)), - ( - ["relation-count"], - (model.Post.relation_count, self.SORT_DESC), - ), - ( - ["feature-count"], - (model.Post.feature_count, self.SORT_DESC), - ), - (["file-size"], (model.Post.file_size, self.SORT_DESC)), - ( - ["image-width", "width"], - (model.Post.canvas_width, self.SORT_DESC), - ), - ( - ["image-height", "height"], - (model.Post.canvas_height, self.SORT_DESC), - ), - ( - ["image-area", "area"], - (model.Post.canvas_area, self.SORT_DESC), - ), - ( - ["creation-date", "creation-time", "date", "time"], - (model.Post.creation_time, self.SORT_DESC), - ), - ( - [ - "last-edit-date", - "last-edit-time", - "edit-date", - "edit-time", - ], - (model.Post.last_edit_time, self.SORT_DESC), - ), - ( - ["comment-date", "comment-time"], - (model.Post.last_comment_creation_time, self.SORT_DESC), - ), - ( - ["fav-date", "fav-time"], - (model.Post.last_favorite_time, self.SORT_DESC), - ), - ( - ["feature-date", "feature-time"], - (model.Post.last_feature_time, self.SORT_DESC), - ), - ] - )) + filters = { + "metric-" + name: (_create_metric_sort_column(name), self.SORT_ASC) + for name in self.all_metric_names + } + filters.update( + util.unalias_dict( + [ + ( + ["random"], + (sa.sql.expression.func.random(), self.SORT_NONE), + ), + (["id"], (model.Post.post_id, self.SORT_DESC)), + (["score"], (model.Post.score, self.SORT_DESC)), + (["tag-count"], (model.Post.tag_count, self.SORT_DESC)), + ( + ["comment-count"], + (model.Post.comment_count, self.SORT_DESC), + ), + ( + ["fav-count"], + (model.Post.favorite_count, self.SORT_DESC), + ), + (["note-count"], (model.Post.note_count, self.SORT_DESC)), + ( + ["relation-count"], + (model.Post.relation_count, self.SORT_DESC), + ), + ( + ["feature-count"], + (model.Post.feature_count, self.SORT_DESC), + ), + (["file-size"], (model.Post.file_size, self.SORT_DESC)), + ( + ["image-width", "width"], + (model.Post.canvas_width, self.SORT_DESC), + ), + ( + ["image-height", "height"], + (model.Post.canvas_height, self.SORT_DESC), + ), + ( + ["image-area", "area"], + (model.Post.canvas_area, self.SORT_DESC), + ), + ( + ["creation-date", "creation-time", "date", "time"], + (model.Post.creation_time, self.SORT_DESC), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + (model.Post.last_edit_time, self.SORT_DESC), + ), + ( + ["comment-date", "comment-time"], + ( + model.Post.last_comment_creation_time, + self.SORT_DESC, + ), + ), + ( + ["fav-date", "fav-time"], + (model.Post.last_favorite_time, self.SORT_DESC), + ), + ( + ["feature-date", "feature-time"], + (model.Post.last_feature_time, self.SORT_DESC), + ), + ] + ) + ) return filters @property diff --git a/server/szurubooru/tests/api/test_post_retrieving.py b/server/szurubooru/tests/api/test_post_retrieving.py index b64074c..b2d5c8e 100644 --- a/server/szurubooru/tests/api/test_post_retrieving.py +++ b/server/szurubooru/tests/api/test_post_retrieving.py @@ -16,6 +16,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, }, } ) @@ -75,7 +76,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): @@ -129,24 +133,28 @@ def test_trying_to_retrieve_single_without_privileges( ) -@pytest.mark.parametrize("query,expected_id", [ - ("sort:id,asc", 2), - ("sort:id,asc id:2..", 2), - ("sort:id,desc id:2..", 3), - ("sort:id,asc id:3..", 3), - ("sort:id,desc id:3..", 3), - ("sort:id id:4..", None), - ("sort:tag-count", 3), - ("sort:tag-count,asc id:..2", 1), - ("sort:tag-count,desc id:..2", 2), -]) +@pytest.mark.parametrize( + "query,expected_id", + [ + ("sort:id,asc", 2), + ("sort:id,asc id:2..", 2), + ("sort:id,desc id:2..", 3), + ("sort:id,asc id:3..", 3), + ("sort:id,desc id:3..", 3), + ("sort:id id:4..", None), + ("sort:tag-count", 3), + ("sort:tag-count,asc id:..2", 1), + ("sort:tag-count,desc id:..2", 2), + ], +) def test_median( - query, - expected_id, - post_factory, - tag_factory, - context_factory, - user_factory): + query, + expected_id, + post_factory, + tag_factory, + context_factory, + user_factory, +): tag1 = tag_factory() tag2 = tag_factory() tag3 = tag_factory() @@ -155,16 +163,38 @@ def test_median( post3 = post_factory(id=3, tags=[tag1, tag2]) db.session.add_all([tag1, tag2, tag3, post1, post2, post3]) db.session.flush() - with patch("szurubooru.func.comments.serialize_comment"), \ - patch("szurubooru.func.users.serialize_micro_user"), \ - patch("szurubooru.func.posts.files.has"): + with patch("szurubooru.func.comments.serialize_comment"), patch( + "szurubooru.func.users.serialize_micro_user" + ), patch("szurubooru.func.posts.files.has"): response = api.post_api.get_posts_median( context_factory( params={"query": query}, - user=user_factory(rank=model.User.RANK_REGULAR))) + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) if not expected_id: assert response["total"] == 0 assert len(response["results"]) == 0 else: assert response["total"] == 1 assert response["results"][0]["id"] == expected_id + + +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}, + ) 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 6ab5d52..cdd60ec 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 @@ -1074,3 +1080,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]) |