-
Notifications
You must be signed in to change notification settings - Fork 32
feat: gate access behind a Flow User role #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
KushX
wants to merge
2
commits into
frappe:develop
Choose a base branch
from
Cecypo-Tech:flow-user-access-control-pr
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import frappe | ||
|
|
||
| from flow.permissions import FLOW_USER_ROLE, ensure_flow_role | ||
|
|
||
| """Grandfather existing users onto the new Flow User role. | ||
|
|
||
| Before this patch every logged-in user could drive Flow; the role did not exist | ||
| and the doctypes granted access to `All`. Introducing the gate would silently | ||
| lock out everyone, including the admins who need to hand access out, so this | ||
| preserves the status quo once: every enabled System Manager keeps access, and | ||
| the site owner revokes individually afterwards. | ||
|
|
||
| Only System Managers are granted — not literally everyone who could reach the | ||
| API before — because the pre-gate audience included Website Users, and | ||
| reinstating that would defeat the point of the change. | ||
|
|
||
| Granting a role saves the User doc, which re-runs *every* installed app's User | ||
| validation. On a real site those reject for reasons that have nothing to do with | ||
| Flow — a user whose existing role mix another app forbids, say. A backfill must | ||
| never abort `bench migrate` over that, so each grant is isolated in its own | ||
| savepoint: a rejection rolls back that one user and the sweep continues. Skipped | ||
| users are listed at the end and can be granted by hand. | ||
|
|
||
| Fresh installs never run this: `install_app` marks all patches complete without | ||
| executing them (frappe/installer.py), so a new site starts with nobody | ||
| grandfathered in. | ||
| """ | ||
|
|
||
|
|
||
| def execute(): | ||
| ensure_flow_role() | ||
|
|
||
| system_managers = frappe.get_all( | ||
| "Has Role", | ||
| filters={"role": "System Manager", "parenttype": "User"}, | ||
| pluck="parent", | ||
| distinct=True, | ||
| ) | ||
| if not system_managers: | ||
| return | ||
|
|
||
| enabled = set( | ||
| frappe.get_all( | ||
| "User", | ||
| filters={"name": ["in", system_managers], "enabled": 1, "user_type": "System User"}, | ||
| pluck="name", | ||
| ) | ||
| ) | ||
| enabled.discard("Administrator") # bypasses the gate anyway | ||
| if not enabled: | ||
| return | ||
|
|
||
| already = set( | ||
| frappe.get_all( | ||
| "Has Role", | ||
| filters={"role": FLOW_USER_ROLE, "parenttype": "User", "parent": ["in", list(enabled)]}, | ||
| pluck="parent", | ||
| ) | ||
| ) | ||
|
|
||
| granted: list[str] = [] | ||
| skipped: list[tuple[str, str]] = [] | ||
| for index, user in enumerate(sorted(enabled - already)): | ||
| savepoint = f"flow_grant_role_{index}" | ||
| frappe.db.savepoint(savepoint) | ||
| try: | ||
| # add_roles goes through the User doc so role-change side effects still fire. | ||
| frappe.get_doc("User", user).add_roles(FLOW_USER_ROLE) | ||
| except Exception as e: | ||
| frappe.db.rollback(save_point=savepoint) | ||
| # The rejecting app queues a dialog; drop it so it cannot surface later. | ||
| frappe.clear_messages() | ||
| skipped.append((user, str(e)[:200])) | ||
| else: | ||
| frappe.db.release_savepoint(savepoint) | ||
| granted.append(user) | ||
|
|
||
| if skipped: | ||
| listed = "\n".join(f"- {user}: {reason}" for user, reason in skipped) | ||
| print( | ||
| f"Flow: granted '{FLOW_USER_ROLE}' to {len(granted)} user(s). " | ||
| f"{len(skipped)} could not be granted and will not have Flow access:\n{listed}\n" | ||
| f"Grant the role manually (User > Roles) if any of them should keep it." | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Copyright (c) 2026, Frappe Technologies and contributors | ||
| # License: MIT. See LICENSE | ||
|
|
||
| """Access control for Flow. | ||
|
|
||
| Flow is gated by a single role, `Flow User`. A user either may use the | ||
| assistant or may not — there are no sub-feature toggles, because an agent | ||
| already acts strictly within the running user's own permissions (see | ||
| `flow.utils.safe_exec`, which strips `ignore_permissions` and omits raw SQL | ||
| entirely). So the only question worth asking is *who*, and a role answers it | ||
| the way any other Frappe feature would. | ||
|
|
||
| The gate is deliberately the role alone: a System Manager without `Flow User` | ||
| is refused, so an admin can hand out access without handing out their own. | ||
| `Administrator` bypasses `frappe.only_for` upstream and cannot be excluded. | ||
|
|
||
| Enforcement belongs on the whitelisted API (`flow.api`) — that is the | ||
| untrusted boundary. Server-side Python (`flow.lib.session`, triggers) is | ||
| trusted and stays ungated, or a trigger firing as a low-privilege `run_as` | ||
| user would break. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import frappe | ||
|
|
||
| FLOW_USER_ROLE = "Flow User" | ||
|
|
||
|
|
||
| def assert_flow_access() -> None: | ||
| """Raise unless the current user may use Flow. Call first in every whitelisted endpoint.""" | ||
| frappe.only_for(FLOW_USER_ROLE, message=True) | ||
|
|
||
|
|
||
| def has_flow_access(user: str | None = None) -> bool: | ||
| """Whether `user` (default: session user) may use Flow. Used for UI hints, never as the gate.""" | ||
| if (user or frappe.session.user) == "Administrator": | ||
| return True | ||
| return FLOW_USER_ROLE in frappe.get_roles(user) | ||
|
|
||
|
|
||
| def ensure_flow_role() -> None: | ||
| """Create the Flow User role if missing. Idempotent — safe on install and every migrate. | ||
|
|
||
| The existence check is an optimisation, not the guarantee: two lifecycle hooks racing | ||
| could both pass it. `ignore_if_duplicate` makes the insert itself the safe operation. | ||
| """ | ||
| if frappe.db.exists("Role", FLOW_USER_ROLE): | ||
| return | ||
| frappe.get_doc( | ||
| { | ||
| "doctype": "Role", | ||
| "role_name": FLOW_USER_ROLE, | ||
| # Flow lives in the desk, so the role must carry desk access to be useful. | ||
| "desk_access": 1, | ||
| } | ||
| ).insert(ignore_permissions=True, ignore_if_duplicate=True) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.