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
- Have another project owner (or an instance admin) change your role.
- First promote another member to owner, then have that owner demote you.
- Log in as an admin account to perform the role change — admins bypass the guard.
- 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
- Hide the role editor for a user's own owner row in the UI
- Maintain at least two owners per project to allow role changes
- Perform self-demotions through an admin
- Include this guard in API client tests before bulk role rewrites
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)