@@ -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)" ,
0 commit comments