semaphoreui/semaphore · error

owner can not left the project

Error message

owner can not left the project

What it means

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.

Solutions

  1. Assign another member the ProjectOwner role first (UpdateUser with role owner), then leave/remove yourself.
  2. Have an instance admin perform the removal — admins bypass this check.
  3. Create a new owner account, transfer ownership, then retry the leave operation.
  4. If the owner account is unavailable, use admin-level tooling to reassign project ownership in the database/store.

Example fix

// before
DELETE /api/project/{id}/users/{ownerId}   // 400 owner can not left the project
// after: transfer ownership first, then remove
PUT /api/project/{id}/users/{otherUserId}  {"role": "owner"}
DELETE /api/project/{id}/users/{ownerId}
Defensive patterns

Strategy: type-guard

Validate before calling

// before leave/remove self
const me = await getProjectUser(projectId, myId);
if (me.role === "owner" && !isAdmin) {
  await transferOwnership(projectId, newOwnerId); // must happen first
}

Type guard

function isSelfOwner(user, me) {
  return user.id === me.id && me.role === "owner" && !me.admin;
}

Try / catch

try {
  await deleteProjectUser(projectId, userId);
} catch (e) {
  if (/owner can not left the project/.test(e.message)) {
    show("Promote another owner before leaving the project.");
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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 semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/adf370f6a6d9b5e4. Report an issue: GitHub.

Appendix: source

Thrown at api/projects/users.go:126

	helpers.EventLog(r, helpers.EventLogCreate, helpers.EventLogItem{
		UserID:      helpers.UserFromContext(r).ID,
		ProjectID:   project.ID,
		ObjectType:  db.EventUser,
		ObjectID:    projectUser.UserID,
		Description: fmt.Sprintf("User ID %d added to team", projectUser.UserID),
	})

	w.WriteHeader(http.StatusNoContent)
}

// removeUser removes a user from a project team
func removeUser(targetUser db.User, w http.ResponseWriter, r *http.Request) {
	project := helpers.GetFromContext(r, "project").(db.Project)
	me := helpers.GetFromContext(r, "user").(*db.User) // logged in user
	myRole := helpers.GetFromContext(r, "projectUserRole").(db.ProjectUserRole)

	if !me.Admin && targetUser.ID == me.ID && myRole == db.ProjectOwner {
		helpers.WriteError(w, fmt.Errorf("owner can not left the project"))
		return
	}

	err := helpers.Store(r).DeleteProjectUser(project.ID, targetUser.ID)

	if err != nil {
		helpers.WriteError(w, err)
		return
	}

	helpers.EventLog(r, helpers.EventLogDelete, helpers.EventLogItem{
		UserID:      helpers.UserFromContext(r).ID,
		ProjectID:   project.ID,
		ObjectType:  db.EventUser,
		ObjectID:    targetUser.ID,
		Description: fmt.Sprintf("User ID %d removed from team", targetUser.ID),
	})

View on GitHub (pinned to 1774ccb71a)