HumanSignal/label-studio · error · PermissionDenied
LOCK_PERMISSION_MESSAGE
Error message
LOCK_PERMISSION_MESSAGE
What it means
ViewSerializer.create raises PermissionDenied(LOCK_PERMISSION_MESSAGE) when a new view is created with is_locked=True but the requesting user lacks the project-level permission checked by user_can_manage_view_lock (view_lock_project). Locking a tab is a privileged action reserved for users who can manage view locks.
Source
Thrown at label_studio/data_manager/serializers.py:462
self._create_filters(filter_group=filter_group, filters_data=filters_data)
validated_data['filter_group_id'] = filter_group.id
# Trust caller / perform_create order (Max+1). Never use count(), which
# collides when order values have gaps. If order is still absent, assign
# Max+1 for project-scoped views only (DatasetView has no project).
if 'order' not in validated_data:
project = view_lock_project(validated_data)
if project is not None:
max_order = View.objects.filter(project=project).aggregate(Max('order'))['order__max']
validated_data['order'] = (max_order if max_order is not None else -1) + 1
if validated_data.get('is_locked'):
request = self.context.get('request')
user = getattr(request, 'user', None)
if not user_can_manage_view_lock(user, view_lock_project(validated_data)):
raise PermissionDenied(LOCK_PERMISSION_MESSAGE)
validated_data['locked_by'] = user
validated_data['locked_at'] = timezone.now()
view = self.Meta.model.objects.create(**validated_data)
return view
def update(self, instance, validated_data):
with transaction.atomic():
request = self.context.get('request')
user = getattr(request, 'user', None)
is_locked = validated_data.pop('is_locked', serializers.empty)
filter_group_data = validated_data.pop('filter_group', None)
instance = self.Meta.model.objects.select_for_update().get(pk=instance.pk)
if is_locked is not serializers.empty and bool(is_locked) != bool(instance.is_locked):
if not user_can_manage_view_lock(user, view_lock_project(instance)):
raise PermissionDenied(LOCK_PERMISSION_MESSAGE)View on GitHub (pinned to 0b49e9b539)
Solutions
- Submit the view with is_locked=false (or omit it) and have a project admin/owner perform the lock.
- Grant the requesting user the required project permission (e.g. manage view locks / project admin role).
- Strip is_locked from the payload for non-privileged clients before submission.
- Check permission client-side (user_can_manage_view_lock equivalent) and hide/disable lock controls.
- Adapt automation/templates to not carry over locked_by/is_locked when cloning views.
Example fix
// before
requests.post('/api/dm/views', json={"project": pid, "is_locked": True})
// after
requests.post('/api/dm/views', json={"project": pid, "is_locked": False}) # lock later as admin Defensive patterns
Strategy: try-catch
Validate before calling
def can_lock_view(user, project):
from data_manager.helpers import user_can_manage_view_lock, view_lock_project
return bool(user) and user_can_manage_view_lock(user, view_lock_project({'project': project})) Type guard
def may_set_locked(user, view_payload: dict) -> bool:
return not view_payload.get('is_locked') or can_lock_view(user, view_payload.get('project')) Try / catch
from rest_framework.exceptions import PermissionDenied
try:
view = create_view(payload)
except PermissionDenied:
payload['is_locked'] = False
view = create_view(payload) # or surface 'only managers can lock views' Prevention
- Do not include is_locked in payloads from non-privileged clients
- Sync client role info with backend permissions before enabling lock UI
- Clone views without copying lock metadata
- Alert on 403s from view creation to catch role regressions
When it happens
Trigger: POST /api/dm/views (or any call path through _create_filters/create) with validated_data['is_locked'] = True by an annotator or other non-privileged user.
Common situations: Frontend sends the full view snapshot including a locked flag copied from another user's view; role downgrades (user lost manage-lock rights) while a client still submits locked views; scripts/automation creating views with is_locked set from a template.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Prediction validation failed ({len(validation_errors)} error
- "url" is not found in request data
- "url" must be 2048 characters or fewer
- load_tasks: No data found in DATA or in FILES
- {item} contains invalid "task" field: task ID {task_id} not
AI-assisted analysis of HumanSignal/label-studio@0b49e9b539 (2026-08-29).
Data as JSON: /api/errors/4ad4a7e1309524e4.
Report an issue: GitHub.