{"record":{"id":"0fe7abefb06463e0","repo":"langflow-ai/langflow","slug":"setting-this-parent-would-create-a-role-hierarchy","errorCode":null,"errorMessage":"Setting this parent would create a role hierarchy cycle","messagePattern":"Setting this parent would create a role hierarchy cycle","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"src/backend/base/langflow/api/v1/authz_roles.py","lineNumber":197,"sourceCode":"    fields_set = payload.model_fields_set\n\n    if \"parent_role_id\" in fields_set:\n        if payload.parent_role_id is None:\n            role.parent_role_id = None\n        else:\n            if payload.parent_role_id == role.id:\n                raise HTTPException(\n                    status_code=status.HTTP_400_BAD_REQUEST,\n                    detail=\"A role cannot be its own parent\",\n                )\n            parent = await session.get(AuthzRole, payload.parent_role_id)\n            if parent is None:\n                raise HTTPException(\n                    status_code=status.HTTP_400_BAD_REQUEST,\n                    detail=\"parent_role_id does not reference an existing role\",\n                )\n            if await _detect_parent_cycle(session, role_id=role.id, proposed_parent_id=payload.parent_role_id):\n                raise HTTPException(\n                    status_code=status.HTTP_400_BAD_REQUEST,\n                    detail=\"Setting this parent would create a role hierarchy cycle\",\n                )\n            role.parent_role_id = payload.parent_role_id\n\n    if \"description\" in fields_set:\n        # description is nullable on the DB side — None is a legitimate clear.\n        role.description = payload.description\n\n    if \"name\" in fields_set:\n        # name is NOT NULL + unique on the DB side; reject an explicit null at\n        # the boundary so the caller gets a clear 400 instead of an opaque\n        # IntegrityError that the catch block below mislabels as \"Name conflict\".\n        if payload.name is None:\n            raise HTTPException(\n                status_code=status.HTTP_400_BAD_REQUEST,\n                detail=\"name cannot be null\",\n            )","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/v1/authz_roles.py#L179-L215","documentation":"Raised by PATCH /api/v1/authz/roles/{role_id} after _detect_parent_cycle walks the parent chain from the proposed parent and finds the role being edited already in it. Setting that parent would create a cycle in the role hierarchy, so the request is rejected with HTTP 400 before assignment.","triggerScenarios":"PATCH where role A's parent is set to B while B (or any of B's ancestors) already has A as an ancestor — e.g. A→B exists and you PATCH B with parent_role_id=A. Deep chains trigger it transitively, not just two-node loops.","commonSituations":"Reorganizing role hierarchies where admins move a parent beneath one of its descendants, or bulk import scripts that reorder hierarchy without topological ordering.","solutions":["Restructure the change: first set the intermediate role's parent to null (PATCH parent_role_id: null) to break the chain, then apply the intended parentage in topological order","Verify with GET /authz/roles and walk parent ids client-side before submitting the move","Apply hierarchy edits one hop at a time so any cycle is easy to localize"],"exampleFix":"// before\n// A is ancestor of B; trying to make A's parent = B\nawait api.patch(`/authz/roles/${A}`, { parent_role_id: B }); // 400 cycle\n\n// after\nawait api.patch(`/authz/roles/${A}`, { parent_role_id: null }); // detach\nawait api.patch(`/authz/roles/${B}`, { parent_role_id: A });  // desired order","handlingStrategy":"validation","validationCode":"// walk parent chain client-side; roles fetched from GET /authz/roles\nfunction createsCycle(roles: Role[], roleId: string, proposedParent: string): boolean {\n  const byId = new Map(roles.map(r => [r.id, r]));\n  let cur = byId.get(proposedParent);\n  while (cur) {\n    if (cur.id === roleId) return true;\n    cur = cur.parent_role_id ? byId.get(cur.parent_role_id) : undefined;\n  }\n  return false;\n}","typeGuard":"const isAcyclicMove = (roles: Role[], roleId: string, parentId: string): boolean =>\n  !createsCycle(roles, roleId, parentId);","tryCatchPattern":null,"preventionTips":["Simulate the parent change on the client hierarchy before PATCHing","Apply hierarchy edits in topological order, detaching first when reordering","Change one parent link per request so cycles are easy to localize"],"tags":["authz","rbac","http-400","roles","hierarchy","cycle-detection"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}