Skip to content

feat: add editable project user roles for owner and admin - #4993

Open
Promist-Moon wants to merge 2 commits into
OpenFn:mainfrom
Promist-Moon:3603-editing-collaborator-roles
Open

feat: add editable project user roles for owner and admin#4993
Promist-Moon wants to merge 2 commits into
OpenFn:mainfrom
Promist-Moon:3603-editing-collaborator-roles

Conversation

@Promist-Moon

@Promist-Moon Promist-Moon commented Jul 19, 2026

Copy link
Copy Markdown

Description

This PR adds project role editing for collaborators in Project Settings (#collaboration). Users with permissions owner and admin can edit the collaborator permissions to be admin, editor, or viewer. Tests for the relevant permissions were also added.

Closes #3603

Screenshot 2026-07-20 at 12 14 55 AM

Validation steps

  1. Start the app locally and log in as a user with owner role on a project.
  2. Go to Project Settings -> Collaboration.
  3. Confirm role dropdown is shown for eligible collaborator rows.
  4. Change a collaborator role viewer -> admin and verify that the success flash appears. The role should persist after refresh
  5. Confirm owner row is not editable.
  6. Confirm current user row is not editable.
  7. Log in as editor (or viewer) and verify the dropdown lists are not available.

Additional notes for the reviewer

  1. Scope is limited to Project Settings collaborator role editing; no owner transfer was added.

AI Usage

Please disclose whether you've used AI anywhere in this PR (it's cool, we just want to know!):

  • I have used Claude Code
  • I have used another model
  • I have not used AI

You can read more details in our Responsible AI Policy

Pre-submission checklist

  • I have performed an AI review of my code (we recommend using /review
    with Claude Code)
  • I have implemented and tested all related authorization policies.
    (e.g., :owner, :admin, :editor, :viewer)
  • I have updated the changelog.
  • I have ticked a box in "AI usage" in this PR

@github-project-automation github-project-automation Bot moved this to New Issues in Core Jul 19, 2026
@elias-ba
elias-ba self-requested a review July 21, 2026 14:40
@elias-ba

elias-ba commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Hey @Promist-Moon , thanks for this! Looks like a solid contribution. I'll give it a proper review later this week.

@elias-ba elias-ba self-assigned this Jul 21, 2026
@Promist-Moon

Copy link
Copy Markdown
Author

Thanks Elias! Let me know when there's something you'd like me to fix!

@elias-ba elias-ba left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Promist-Moon,

Thanks for picking this one up 👏🏽 The approach is right, and I really like that you re-checked that project_user_id belongs to the mounted project. I think we should try to keep that standard for other places in this page.

Three things in the set_role handler need fixing before this merges, written up inline. The role allowlist rejects nothing, so "owner" reaches the database write. A sandbox owner can demote a parent-project admin they are not allowed to remove. And a crafted project_user_id crashes the settings LiveView for any project member, viewers included. The rest of the comments are small.

Thanks again for the work 🙏

Comment on lines +662 to +667
changeset =
{%{role: project_user.role |> to_string()}, %{role: :string}}
|> Ecto.Changeset.cast(%{role: role}, [:role])
|> Ecto.Changeset.validate_inclusion(:role, ~w(viewer editor admin))

case Ecto.Changeset.get_change(changeset, :role) do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the one I'd most like fixed. validate_inclusion/4 only appends to changeset.errors, it never touches changeset.changes, and get_change/2 reads straight out of changes. So the case matches on any cast-able string including "owner", and it reaches Projects.update_project_user/2 unfiltered.

:owner is a legal RolesEnum value, so the only thing stopping it today is the project_owner_unique_index partial index, which is a data-integrity constraint rather than an authorisation control. On a project with no owner row the write succeeds and mints a new owner, and those are reachable: Accounts.purge_user/1 does a raw Repo.delete_all over the user's project_users with no ownership guard.

Could we gate on changeset.valid?, or just pattern-match the three allowed strings ?

Comment on lines +904 to +911
defp role_editable?(
project_user,
current_user,
can_edit_project_user_role
) do
can_edit_project_user_role and project_user.role != :owner and
project_user.user_id != current_user.id
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is user_removable?/5 minus its sandbox clause, not (sandbox? and parent_admin?(project, project_user)), which delete_project_user!/1 also backs up by raising.

It matters because build_sandbox_project_attributes/5 copies the parent owner into a sandbox as :admin, and provisioning only needs :editor on the parent. I confirmed locally that a sandbox owner can't remove the parent owner's row but can demote it to viewer.

Could we thread project and sandbox? through and mirror that clause ? You'd want include: :user on the fetch so parent_admin?/2 can run.

%{"project_user_id" => project_user_id, "role" => role},
%{assigns: assigns} = socket
) do
project_user = Projects.get_project_user!(project_user_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs before any of the authorisation branches, on a client-controlled hidden input. An unknown UUID raises Ecto.NoResultsError and a non-UUID raises Ecto.Query.CastError, either of which crashes the settings LiveView. Any member can fire it, viewers included.

The non-bang get_project_user/1, with a nil clause falling through to the unauthorised flash, would cover it.

Comment on lines +479 to +482
editable? =
assigns.can_edit_project_user_role and
assigns.project_user.role != :owner and
assigns.project_user.user_id != assigns.current_user.id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: these three conditions are also spelled out in role_editable?/3, and the sandbox drift I flagged there is what the duplication costs. Could we export one predicate and call it from both ?

"role" => "owner"
})

assert html =~ "Error when updating the project user"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This passes because the unique index collided, not because the allowlist rejected anything, which is what masked the changeset issue above. Asserting on an authorisation refusal would make it meaningful.

The crafted-event tests also all use a viewer as the actor. An admin firing set_role at the owner row or at their own row would be worth covering.

Comment thread CHANGELOG.md
Comment on lines +38 to +41
- Project settings now allow `owner` and `admin` users to edit collaborator
roles inline (viewer/editor/admin) without remove-and-readd. Owner rows and
self-role edits remain non-editable.
[#4993](https://github.com/OpenFn/lightning/pull/4993)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small one: new feature so I'd put this under Added rather than Changed, and link #3603 rather than the PR.

@github-project-automation github-project-automation Bot moved this from New Issues to In review in Core Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Allow editing collaborator roles & permissions after they are added

2 participants