Skip to content

Commit 052e9e1

Browse files
committed
Correctly validate moderation status
SchemaNode serialize method is executed before validation so invalid value where not validated by "OneOf". Remove the serialize method and convert the values to the enum in the views. This change uncovered a couple other issues: - Moderation status should be required on update status endpoint - Correct expose a message when annotation_updated is outdated
1 parent b277be4 commit 052e9e1

File tree

7 files changed

+19
-13
lines changed

7 files changed

+19
-13
lines changed

h/schemas/api/group.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
class FilterGroupAnnotationsSchema(colander.Schema):
2929
"""Schema for validating filter-group-annotations API data."""
3030

31-
moderation_status = ModerationStatusNode(missing=None)
31+
moderation_status = ModerationStatusNode(missing=None, default=None)
3232

3333

3434
class GroupAPISchema(JSONSchema):

h/schemas/api/moderation.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,16 @@
33
import colander
44
from pyramid import httpexceptions
55

6-
from h.models.annotation import ModerationStatus
7-
86

97
class ModerationStatusNode(colander.SchemaNode):
108
schema_type = colander.String
119
validator = colander.OneOf(["APPROVED", "PENDING", "DENIED", "SPAM"])
1210

13-
def deserialize(self, cstruct):
14-
return ModerationStatus(cstruct) if cstruct else None
15-
1611

1712
class ChangeAnnotationModerationStatusSchema(colander.Schema):
1813
"""Schema for validating change-annotation-moderation-status API data."""
1914

20-
moderation_status = ModerationStatusNode()
15+
moderation_status = ModerationStatusNode(missing=colander.required)
2116
annotation_updated = colander.SchemaNode(
2217
colander.DateTime(),
2318
description="Sentinel value to avoid conflicting updates. It should match the current value of annotation.updated",
@@ -33,5 +28,5 @@ def validator(self, _node, cstruct):
3328
annotation_updated = self._annotation.updated.replace(tzinfo=UTC).timestamp()
3429
if annotation_updated != cstruct["annotation_updated"].timestamp():
3530
raise httpexceptions.HTTPConflict(
36-
explanation="The annotation has been updated since the moderation status was set."
31+
detail="The annotation has been updated since the moderation status was set."
3732
)

h/views/api/group_annotations.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from h.models import Annotation
2+
from h.models.annotation import ModerationStatus
23
from h.schemas.api.group import FilterGroupAnnotationsSchema
34
from h.schemas.pagination import Pagination
45
from h.schemas.util import validate_query_params
@@ -23,7 +24,11 @@ def list_annotations(context: GroupContext, request):
2324
group = context.group
2425
annotation_json_service = request.find_service(name="annotation_json")
2526

26-
moderation_status_filter = params["moderation_status"]
27+
moderation_status_filter = (
28+
ModerationStatus(params["moderation_status"])
29+
if params.get("moderation_status")
30+
else None
31+
)
2732
query = AnnotationReadService.annotation_search_query(
2833
groupid=group.pubid, moderation_status=moderation_status_filter
2934
)

h/views/api/moderation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def change_annotation_moderation_status(context, request):
5252
params = validate_json(
5353
ChangeAnnotationModerationStatusSchema(context.annotation), request
5454
)
55-
status = params["moderation_status"]
55+
status = ModerationStatus(params["moderation_status"])
5656
request.find_service(name="annotation_moderation").set_status(
5757
context.annotation, status, request.user
5858
)

tests/functional/api/moderation_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_it_returns_http_404_if_annotation_is_in_world_group(
7878
# The current user does not have moderation rights on the world group
7979
assert res.status_code == 404
8080

81-
def test_it_returns_http_404_if_no_authn(self, app, group_annotation):
81+
def test_it_returns_http_404_if_no_auth(self, app, group_annotation):
8282
res = app.delete(
8383
f"/api/annotations/{group_annotation.id}/hide",
8484
expect_errors=True,

tests/unit/h/schemas/api/group_test.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import pytest
22
from webob.multidict import MultiDict
33

4-
from h.models.annotation import ModerationStatus
54
from h.models.group import (
65
AUTHORITY_PROVIDED_ID_MAX_LENGTH,
76
GROUP_DESCRIPTION_MAX_LENGTH,
@@ -203,7 +202,7 @@ def test_it(self, schema):
203202

204203
validated_data = validate_query_params(schema, data)
205204

206-
assert validated_data == {"moderation_status": ModerationStatus.PENDING}
205+
assert validated_data == {"moderation_status": "PENDING"}
207206

208207
def test_it_when_empty(self, schema):
209208
data = MultiDict({})

tests/unit/h/views/api/group_annotations_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from sqlalchemy import func, select
55

66
from h.models import Annotation
7+
from h.schemas.base import ValidationError
78
from h.traversal import (
89
GroupContext,
910
)
@@ -40,6 +41,12 @@ def test_it(
4041
],
4142
}
4243

44+
def test_it_with_wrong_filter(self, context, pyramid_request):
45+
pyramid_request.params["moderation_status"] = "wrong"
46+
47+
with pytest.raises(ValidationError):
48+
list_annotations(context, pyramid_request)
49+
4350
@pytest.fixture
4451
def context(self, factories):
4552
return create_autospec(

0 commit comments

Comments
 (0)