{"record":{"id":"4ad4a7e1309524e4","repo":"HumanSignal/label-studio","slug":"lock-permission-message","errorCode":null,"errorMessage":"LOCK_PERMISSION_MESSAGE","messagePattern":"LOCK_PERMISSION_MESSAGE","errorType":"validation","errorClass":"PermissionDenied","httpStatus":403,"severity":"error","filePath":"label_studio/data_manager/serializers.py","lineNumber":462,"sourceCode":"\n                self._create_filters(filter_group=filter_group, filters_data=filters_data)\n\n                validated_data['filter_group_id'] = filter_group.id\n\n            # Trust caller / perform_create order (Max+1). Never use count(), which\n            # collides when order values have gaps. If order is still absent, assign\n            # Max+1 for project-scoped views only (DatasetView has no project).\n            if 'order' not in validated_data:\n                project = view_lock_project(validated_data)\n                if project is not None:\n                    max_order = View.objects.filter(project=project).aggregate(Max('order'))['order__max']\n                    validated_data['order'] = (max_order if max_order is not None else -1) + 1\n\n            if validated_data.get('is_locked'):\n                request = self.context.get('request')\n                user = getattr(request, 'user', None)\n                if not user_can_manage_view_lock(user, view_lock_project(validated_data)):\n                    raise PermissionDenied(LOCK_PERMISSION_MESSAGE)\n                validated_data['locked_by'] = user\n                validated_data['locked_at'] = timezone.now()\n            view = self.Meta.model.objects.create(**validated_data)\n\n            return view\n\n    def update(self, instance, validated_data):\n        with transaction.atomic():\n            request = self.context.get('request')\n            user = getattr(request, 'user', None)\n            is_locked = validated_data.pop('is_locked', serializers.empty)\n            filter_group_data = validated_data.pop('filter_group', None)\n\n            instance = self.Meta.model.objects.select_for_update().get(pk=instance.pk)\n\n            if is_locked is not serializers.empty and bool(is_locked) != bool(instance.is_locked):\n                if not user_can_manage_view_lock(user, view_lock_project(instance)):\n                    raise PermissionDenied(LOCK_PERMISSION_MESSAGE)","sourceCodeStart":444,"sourceCodeEnd":480,"githubUrl":"https://github.com/HumanSignal/label-studio/blob/0b49e9b53917880baf1dd85d574fe5541a9aafb2/label_studio/data_manager/serializers.py#L444-L480","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nrequests.post('/api/dm/views', json={\"project\": pid, \"is_locked\": True})\n// after\nrequests.post('/api/dm/views', json={\"project\": pid, \"is_locked\": False})  # lock later as admin","handlingStrategy":"try-catch","validationCode":"def can_lock_view(user, project):\n    from data_manager.helpers import user_can_manage_view_lock, view_lock_project\n    return bool(user) and user_can_manage_view_lock(user, view_lock_project({'project': project}))","typeGuard":"def may_set_locked(user, view_payload: dict) -> bool:\n    return not view_payload.get('is_locked') or can_lock_view(user, view_payload.get('project'))","tryCatchPattern":"from rest_framework.exceptions import PermissionDenied\ntry:\n    view = create_view(payload)\nexcept PermissionDenied:\n    payload['is_locked'] = False\n    view = create_view(payload)  # or surface 'only managers can lock views'","preventionTips":["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"],"tags":["django","permissions","rest-framework","data-manager"],"backgroundTag":"permission-denied","analyzedSha":"0b49e9b53917880baf1dd85d574fe5541a9aafb2","analyzedAt":"2026-08-29T00:39:52.578Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}