{"record":{"id":"adf370f6a6d9b5e4","repo":"semaphoreui/semaphore","slug":"owner-can-not-left-the-project","errorCode":null,"errorMessage":"owner can not left the project","messagePattern":"owner can not left the project","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"api/projects/users.go","lineNumber":126,"sourceCode":"\thelpers.EventLog(r, helpers.EventLogCreate, helpers.EventLogItem{\n\t\tUserID:      helpers.UserFromContext(r).ID,\n\t\tProjectID:   project.ID,\n\t\tObjectType:  db.EventUser,\n\t\tObjectID:    projectUser.UserID,\n\t\tDescription: fmt.Sprintf(\"User ID %d added to team\", projectUser.UserID),\n\t})\n\n\tw.WriteHeader(http.StatusNoContent)\n}\n\n// removeUser removes a user from a project team\nfunc removeUser(targetUser db.User, w http.ResponseWriter, r *http.Request) {\n\tproject := helpers.GetFromContext(r, \"project\").(db.Project)\n\tme := helpers.GetFromContext(r, \"user\").(*db.User) // logged in user\n\tmyRole := helpers.GetFromContext(r, \"projectUserRole\").(db.ProjectUserRole)\n\n\tif !me.Admin && targetUser.ID == me.ID && myRole == db.ProjectOwner {\n\t\thelpers.WriteError(w, fmt.Errorf(\"owner can not left the project\"))\n\t\treturn\n\t}\n\n\terr := helpers.Store(r).DeleteProjectUser(project.ID, targetUser.ID)\n\n\tif err != nil {\n\t\thelpers.WriteError(w, err)\n\t\treturn\n\t}\n\n\thelpers.EventLog(r, helpers.EventLogDelete, helpers.EventLogItem{\n\t\tUserID:      helpers.UserFromContext(r).ID,\n\t\tProjectID:   project.ID,\n\t\tObjectType:  db.EventUser,\n\t\tObjectID:    targetUser.ID,\n\t\tDescription: fmt.Sprintf(\"User ID %d removed from team\", targetUser.ID),\n\t})\n","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/api/projects/users.go#L108-L144","documentation":"removeUser (api/projects/users.go:126) blocks a project owner from removing themselves via LeftProject/RemoveUser unless the caller is an instance admin: if !me.Admin && targetUser.ID == me.ID && myRole == db.ProjectOwner it writes 'owner can not left the project'. This prevents projects from being left ownerless — ownership must be transferred first.","triggerScenarios":"DELETE project-user (RemoveUser) or the 'leave project' endpoint (LeftProject) where the authenticated user is the target, is not an admin, and their project role is ProjectOwner.","commonSituations":"Sole owner clicking 'Leave project' in the UI; owner trying to clean up their own membership after handing off but before the role change was saved; scripting bulk membership removals that include the owner.","solutions":["Assign another member the ProjectOwner role first (UpdateUser with role owner), then leave/remove yourself.","Have an instance admin perform the removal — admins bypass this check.","Create a new owner account, transfer ownership, then retry the leave operation.","If the owner account is unavailable, use admin-level tooling to reassign project ownership in the database/store."],"exampleFix":"// before\nDELETE /api/project/{id}/users/{ownerId}   // 400 owner can not left the project\n// after: transfer ownership first, then remove\nPUT /api/project/{id}/users/{otherUserId}  {\"role\": \"owner\"}\nDELETE /api/project/{id}/users/{ownerId}","handlingStrategy":"type-guard","validationCode":"// before leave/remove self\nconst me = await getProjectUser(projectId, myId);\nif (me.role === \"owner\" && !isAdmin) {\n  await transferOwnership(projectId, newOwnerId); // must happen first\n}","typeGuard":"function isSelfOwner(user, me) {\n  return user.id === me.id && me.role === \"owner\" && !me.admin;\n}","tryCatchPattern":"try {\n  await deleteProjectUser(projectId, userId);\n} catch (e) {\n  if (/owner can not left the project/.test(e.message)) {\n    show(\"Promote another owner before leaving the project.\");\n  }\n}","preventionTips":["Disable the leave button in the UI for sole owners","Always promote a successor owner before offboarding an owner","Use admin accounts for membership cleanups involving owners","Educate teams: ownership transfer is a separate step from leaving"],"tags":["projects","authorization","ownership"],"backgroundTag":"permission-denied","analyzedSha":"1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa","analyzedAt":"2026-09-07T11:00:33.293Z","contentChangedAt":"2026-09-07T11:00:33.293Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}