Skip to content

[Feature]: Make RESTRICTED_DOCTYPES configurable, least-privilege MCP users cannot read Server Script and other admin DocTypes at all #249

Description

@Carlo-SR

Description

RESTRICTED_DOCTYPES in frappe_assistant_core/core/security_config.py is a hardcoded blocklist that cannot be configured, and only System Manager bypasses it. For anyone running a least-privilege integration user, this makes DocType access all-or-nothing: either the MCP user gets System Manager (full read, write, delete, plus run_database_query), or a large set of DocTypes is invisible to it no matter what the Frappe permission model says.

Please make this list configurable, or better, let Frappe's own DocType permissions decide read access and keep the hard block only for the write side of code-execution DocTypes.

What the code does today

  1. get_user_primary_role() only recognizes three roles: System Manager, Assistant Admin, Assistant User. Every other user resolves to the string "Default", so custom roles are invisible to this layer.
  2. is_doctype_accessible() returns True unconditionally for System Manager. For everything else it does:
    role_to_check = user_role if user_role in RESTRICTED_DOCTYPES else "Assistant User"
    RESTRICTED_DOCTYPES has exactly one key, "Assistant User", so both "Default" and "Assistant Admin" fall back to the Assistant User blocklist. Promoting the user to Assistant Admin changes nothing.
  3. validate_document_access() runs this check before any frappe.has_permission() call, so granting a role read permission on the DocType has no effect at all.

The blocklist covers, among others: Server Script, Client Script, Custom Script, DocType, DocField, Custom Field, Property Setter, Customize Form, Custom DocPerm, Role, User Permission, Workflow, plus all log DocTypes.

Verified on FAC 2.3.1 (Frappe 15.91.3, ERPNext 15.92.5) and re-checked against main at 7ddc433 (2026-08-20), where the logic is unchanged and still wired into get_document, list_documents, create_document, update_document, submit_document and chatgpt_fetch. Unlike ROLE_TOOL_ACCESS / BASIC_CORE_TOOLS, which you described as legacy in #216, this code path is live.

Concrete symptom with a low-privilege user:

list_documents(doctype="Server Script")   -> "Access to Server Script is restricted for your role"
fetch(id="Server Script/<name>")          -> "Permission denied: Access to Server Script is restricted for your role"
run_database_query("SELECT ... FROM `tabServer Script`")
                                          -> "Insufficient permissions. System Manager role required for query execution."

Use Case

We run FAC against a production ERPNext site and deliberately never connect it as a privileged user. Our setup is one dedicated integration user per environment with our own roles (MCP Read Only, MCP Write new Docs only, and so on), granting read on a wide set of DocTypes and write on very little. That is the whole point of the pattern: the assistant should be able to see a lot and change almost nothing.

The automations on that site are Server Scripts, Client Scripts, Custom Fields and Property Setters. Reviewing them, documenting them, checking which script touches a field, or diffing a production script against the test site is exactly the kind of read-only work we want the assistant to do. Today that is impossible without handing the same user System Manager, which also hands it delete rights and arbitrary SQL. So the safe option is to give up the feature, and the useful option is to give up the safety model.

To be explicit about what we are asking for: read access driven by our roles. We are not asking to write Server Scripts over MCP.

Proposed Solution

Options in order of preference:

  1. Let Frappe permissions decide read access. Drop the read-side blocklist and rely on frappe.has_permission(doctype, "read"), which is already called further down in validate_document_access(). If a site does not want its MCP user reading Server Script, it simply does not grant read on Server Script. This is the same reasoning you used in Feature: configurable role/user access for report tools + create read-only reports without System/Report/Script Manager #216 for the Report DocType: FAC should not second-guess the permission model it already defers to.
  2. Make the list configurable, in the same spirit as FAC Tool Configuration. For example a child table on Assistant Core Settings, or a FAC DocType Access DocType, with a mode per role: Use Frappe Permissions / Use Built-in Blocklist / Custom List, defaulting to the current behaviour so nothing changes for existing installs.
  3. Minimal version if the above is too much, one settings field Additional Allowed DocTypes that subtracts entries from RESTRICTED_DOCTYPES at runtime, so an admin can opt in per DocType without patching the app.

Two details worth keeping in any variant:

  • Split read from write. Hard-blocking create / update / delete on code-execution DocTypes (Server Script, Client Script, Property Setter) is defensible and we would not object. Blocking read is what hurts. The current single list does not make that distinction.
  • Make get_user_primary_role() aware of custom roles, or stop routing this decision through a single "primary role" string. Collapsing every non-assistant role to "Default" means site-specific roles can never express intent here.

Alternatives Considered

  • Grant System Manager to the MCP user. Works, defeats the purpose. It also unlocks run_database_query and every write tool on a production site.
  • Add Assistant Admin. Does not help, see point 2 above: it falls back to the same Assistant User blocklist.
  • Grant read DocPerms on Server Script. No effect, the blocklist is evaluated before the permission check.
  • Patch security_config.py on the site. Not viable for us, the instance is hosted and the change would be lost on the next app update.
  • Copy the script bodies out of the browser by hand. What we do now. It scales badly and the copy goes stale immediately.

Side observation: the blocklist is not a consistent boundary

While tracing this, the enforcement looked uneven, which may be worth a separate look:

  • validate_document_access() is only called by get_document, list_documents, create_document, update_document, submit_document and chatgpt_fetch.
  • delete_document.py never calls it. It relies purely on frappe.has_permission(doctype, "delete"), which is the model we are asking for on the read side.
  • search_doctype, search_documents, search_link, get_doctype_info and the report tools do not consult the list either.

Empirically, on our production site with the same low-privilege user that is refused by list_documents, search_doctype on Server Script happily returns document names, api_method, cron_format and our custom fields. Only the script field stays out of reach. So the blocklist does not really contain anything, it mainly blocks the well-behaved read path while leaving the search path open. That is another argument for letting the permission model handle it in one place.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions