summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorHunternif <hunternif@gmail.com>2025-02-16 01:27:21 +0000
committerHunternif <hunternif@gmail.com>2025-02-16 01:27:21 +0000
commita859d448b2573bab7990521aa4dcb823ba576801 (patch)
tree451bfd74d6001afd6e7179c7d52a5bb30b8452ab /server
parent760fce7d2834a240781a55df9d96afb0e45fac3d (diff)
parentfb763ada0fb5027f29f003f7dc3261c6b87f30a7 (diff)
Merge branch 'priv-view-unsafe-posts' into hunternif
Diffstat (limited to 'server')
-rw-r--r--server/config.yaml.dist2
-rw-r--r--server/szurubooru/api/post_api.py2
-rw-r--r--server/szurubooru/search/configs/post_search_config.py498
-rw-r--r--server/szurubooru/tests/api/test_post_retrieving.py74
-rw-r--r--server/szurubooru/tests/search/configs/test_post_search_config.py39
5 files changed, 373 insertions, 242 deletions
diff --git a/server/config.yaml.dist b/server/config.yaml.dist
index 5dddd72..9a41295 100644
--- a/server/config.yaml.dist
+++ b/server/config.yaml.dist
@@ -97,9 +97,11 @@ 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
+ '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 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])