semaphoreui/semaphore · error

owner can not change his role in the project

Error message

owner can not change his role in the project

What it means

UpdateUser (api/projects/users.go:167) prevents a non-admin project owner from changing their own role: if !me.Admin && targetUser.ID == me.ID && targetUserRole == db.ProjectOwner it returns 'owner can not change his role in the project'. This stops the last owner from demoting themselves and orphaning the project.

Solutions

  1. Have another project owner (or an instance admin) change your role.
  2. First promote another member to owner, then have that owner demote you.
  3. Log in as an admin account to perform the role change — admins bypass the guard.
  4. If no second owner exists, promote one via that owner's session or admin API before self-demotion.

Example fix

// before
PUT /api/project/42/users/7 {"role": "member"}   // as owner #7 -> rejected
// after (as admin, or another owner performs it)
PUT /api/project/42/users/7 {"role": "member"}
Defensive patterns

Strategy: type-guard

Validate before calling

// before PUT role change
if (target.id === me.id && me.role === "owner" && !me.admin) {
  throw new Error("Owners cannot change their own role; ask another owner or an admin");
}

Type guard

function isSelfRoleChangeBlocked(me, targetUser, targetUserRole) {
  return !me.admin && targetUser.id === me.id && targetUserRole === "owner";
}

Try / catch

try {
  await updateProjectUserRole(projectId, userId, role);
} catch (e) {
  if (/owner can not change his role/.test(e.message)) {
    show("Ask another project owner or an admin to change your role.");
  }
}

Prevention

When it happens

Trigger: PUT /api/project/{project_id}/users/{user_id} (role change) where the authenticated user targets themselves, is not an admin, and currently holds db.ProjectOwner.

Common situations: Owner trying to demote themselves to member/maintainer directly; UI role editor saving the owner's own row; automation that iterates members and rewrites roles including 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/a8c1b492dcbd1c17. Report an issue: GitHub.

Appendix: source

Thrown at api/projects/users.go:167

func LeftProject(w http.ResponseWriter, r *http.Request) {
	me := helpers.GetFromContext(r, "user").(*db.User) // logged in user
	removeUser(*me, w, r)
}

// RemoveUser removes a user from a project team
func RemoveUser(w http.ResponseWriter, r *http.Request) {
	targetUser := helpers.GetFromContext(r, "projectUser").(db.User) // target user
	removeUser(targetUser, w, r)
}

func UpdateUser(w http.ResponseWriter, r *http.Request) {
	project := helpers.GetFromContext(r, "project").(db.Project)
	me := helpers.GetFromContext(r, "user").(*db.User) // logged in user
	targetUser := helpers.GetFromContext(r, "projectUser").(db.User)
	targetUserRole := helpers.GetFromContext(r, "projectUserRole").(db.ProjectUserRole)

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

	var projectUser struct {
		Role db.ProjectUserRole `json:"role"`
	}

	if !helpers.Bind(w, r, &projectUser) {
		return
	}

	if !projectUser.Role.IsValid() {
		_, err := helpers.Store(r).GetProjectOrGlobalRoleBySlug(project.ID, string(projectUser.Role))
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
	}

View on GitHub (pinned to 1774ccb71a)