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

  1. Submit the view with is_locked=false (or omit it) and have a project admin/owner perform the lock.
  2. Grant the requesting user the required project permission (e.g. manage view locks / project admin role).
  3. Strip is_locked from the payload for non-privileged clients before submission.
  4. Check permission client-side (user_can_manage_view_lock equivalent) and hide/disable lock controls.
  5. 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

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


AI-assisted analysis of HumanSignal/label-studio@0b49e9b539 (2026-08-29). Data as JSON: /api/errors/4ad4a7e1309524e4. Report an issue: GitHub.