Skip to content

Commit c8ef643

Browse files
committed
Address review: trim verbose comments, add not-found regression test
1 parent 6556c4c commit c8ef643

2 files changed

Lines changed: 32 additions & 27 deletions

File tree

config/graphql/label_mutations.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -238,29 +238,22 @@ def mutate(
238238
obj_id = None
239239

240240
try:
241-
# Permission check FIRST — runs before any field validation
242-
# so a non-owner can't distinguish "reached validation" from
243-
# "denied by permission" via different error messages
244-
# (defense-in-depth; consistent with the IDOR-safe deny
245-
# pattern documented in
246-
# ``docs/permissioning/consolidated_permissioning_guide.md``).
241+
# Permission check runs before validation so a non-owner cannot
242+
# distinguish "reached validation" from "denied" via different
243+
# error messages (IDOR mitigation — see
244+
# docs/permissioning/consolidated_permissioning_guide.md).
247245
labelset = LabelSet.objects.get(pk=from_global_id(labelset_id)[1])
248246
if not user_has_permission_for_obj(
249247
info.context.user,
250248
labelset,
251249
PermissionTypes.UPDATE,
252250
include_group_permissions=True,
253251
):
254-
# Generic deny path — same message and code path as not-found
255252
raise LabelSet.DoesNotExist()
256253

257-
# ``text`` is GraphQL-optional so without this guard the model
258-
# default ("Text Label") would silently apply, producing
259-
# surprising labels for clients that forgot to pass a value
260-
# or passed whitespace. Django's ``blank=False`` is
261-
# form-only and NOT enforced by ``objects.create()``. The
262-
# ``not (text and text.strip())`` form covers ``None``,
263-
# empty string, and whitespace-only in one expression.
254+
# Reject blank text explicitly: Django's ``blank=False`` is
255+
# form-only and ``objects.create()`` would silently apply the
256+
# "Text Label" model default.
264257
if not (text and text.strip()):
265258
return CreateLabelForLabelsetMutation(
266259
obj=None,
@@ -269,10 +262,6 @@ def mutate(
269262
ok=False,
270263
)
271264

272-
# Validate color format (defense in depth). ``color=""`` is
273-
# normalised to ``None`` so the model default ("#FFFFFF")
274-
# applies, matching the silent-default behaviour clients
275-
# already get from omitting the argument.
276265
if color == "":
277266
color = None
278267
is_valid_color, color_error = validate_color(color)
@@ -282,10 +271,8 @@ def mutate(
282271
)
283272

284273
logger.debug("CreateLabelForLabelsetMutation - mutate / Labelset", labelset)
285-
# Drop None and empty-string values so model field defaults
286-
# apply (description, color, icon, text are NOT NULL with
287-
# sensible defaults). Empty strings would bypass those
288-
# defaults at the DB level and write a row with blank values.
274+
# Drop None/"" so model field defaults apply rather than
275+
# writing blank values at the DB level.
289276
create_kwargs = {
290277
k: v
291278
for k, v in {
@@ -316,8 +303,7 @@ def mutate(
316303
logger.debug("Done")
317304

318305
except LabelSet.DoesNotExist:
319-
# Legitimate auth rejection or genuine 404 — log without a stack
320-
# trace to avoid polluting logs with what looks like real errors.
306+
# Auth rejection or genuine 404 — warn without stack trace.
321307
logger.warning(
322308
"CreateLabelForLabelsetMutation: labelset not found or "
323309
"permission denied (labelset_id=%s)",
@@ -369,15 +355,13 @@ def mutate(
369355
PermissionTypes.UPDATE,
370356
include_group_permissions=True,
371357
):
372-
# Generic deny path — same message and code path as not-found
373358
raise LabelSet.DoesNotExist()
374359
labelset.annotation_labels.remove(*label_pks)
375360
ok = True
376361
message = "Success"
377362

378363
except LabelSet.DoesNotExist:
379-
# Legitimate auth rejection or genuine 404 — log without a stack trace
380-
# to avoid polluting logs with what looks like real errors.
364+
# Auth rejection or genuine 404 — warn without stack trace.
381365
logger.warning(
382366
"RemoveLabelsFromLabelsetMutation: labelset not found or "
383367
"permission denied (labelset_id=%s)",

opencontractserver/tests/test_label_mutations.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,24 @@ def test_permission_denied_message_does_not_leak_validation_state(
429429
# request was structurally valid but rejected by permission.
430430
self.assertNotIn("blank", data["message"].lower())
431431
self.assertIn("does not exist", data["message"].lower())
432+
433+
def test_nonexistent_labelset_returns_same_not_found_message(self) -> None:
434+
"""A valid relay-encoded ID for a PK that does not exist must
435+
produce the same not-found response as a permission denial,
436+
pinning the IDOR-safe behaviour against future refactors.
437+
"""
438+
variables = self._create_variables("Should Not Be Created")
439+
variables["labelsetId"] = to_global_id("LabelSetType", 999_999)
440+
441+
result = self.client.execute(
442+
CREATE_LABEL_FOR_LABELSET_MUTATION,
443+
variables=variables,
444+
)
445+
446+
self.assertIsNone(result.get("errors"))
447+
data = result["data"]["createAnnotationLabelForLabelset"]
448+
self.assertFalse(data["ok"])
449+
self.assertIn("does not exist", data["message"].lower())
450+
self.assertFalse(
451+
AnnotationLabel.objects.filter(text="Should Not Be Created").exists()
452+
)

0 commit comments

Comments
 (0)