This is an automated email from the ASF dual-hosted git repository.
FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new a7c2dcb docs: add Doris fine-grained access control guide (#161)
a7c2dcb is described below
commit a7c2dcbd6d4359e40aaf233ab376ecc623f53e12
Author: Yijia Su <[email protected]>
AuthorDate: Thu Jul 30 19:28:25 2026 +0800
docs: add Doris fine-grained access control guide (#161)
---
README.md | 5 +-
README.zh-CN.md | 27 ++-
docs/doris-fine-grained-access-control.md | 369 ++++++++++++++++++++++++++++++
test/test_product_identity.py | 16 ++
4 files changed, 415 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index b442b69..dad596a 100644
--- a/README.md
+++ b/README.md
@@ -1152,7 +1152,10 @@ default.
These settings govern MCP application permissions and security classification.
Database, table, column, and row access must still be enforced with Doris
users,
roles, grants, views, and row policies; OAuth mapping does not bypass Doris
-authorization.
+authorization. See the
+[Doris fine-grained access-control
guide](docs/doris-fine-grained-access-control.md)
+for an end-to-end column and row policy example, MCP identity-routing choices,
+and a verification checklist.
### SQL Security Validation
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 7cfea9e..68f097d 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -306,7 +306,32 @@ export
OAUTH_USERINFO_URL=https://issuer.example.com/userinfo
MCP 的角色、scope、安全级别、SQL 检查和数据脱敏属于应用层控制,不能替代
Doris 数据权限。生产环境必须使用 Doris 用户、角色、GRANT、视图和行级策略
落实最小权限。即使 OAuth 角色映射允许调用 `exec_query`,最终能读取的数据仍应
-由 Doris 拒绝或允许。
+由 Doris 拒绝或允许。完整 SQL、MCP 身份路由方式和验收清单见
+[Doris 细粒度访问控制指南](docs/doris-fine-grained-access-control.md)。
+
+最小配置示例:
+
+```sql
+CREATE ROLE mcp_orders_east_reader;
+CREATE USER 'mcp_orders_east'@'10.%'
+IDENTIFIED BY '<generated-high-entropy-password>';
+GRANT 'mcp_orders_east_reader' TO 'mcp_orders_east'@'10.%';
+
+GRANT SELECT_PRIV(order_id, region_id, order_total, created_at)
+ON internal.sales.orders
+TO ROLE 'mcp_orders_east_reader';
+
+CREATE ROW POLICY mcp_orders_east_only
+ON sales.orders
+AS RESTRICTIVE
+TO ROLE mcp_orders_east_reader
+USING (region_id = 'east');
+```
+
+列权限目前只支持 `SELECT_PRIV`。同一用户或角色不能再保留表级、库级或全局
+`SELECT_PRIV`,否则更宽的授权会使列限制失效。Row Policy、列权限和 Ranger
+脱敏都不能使用默认的 `root`、`admin` 用户验收;必须使用真实的受限业务用户,
+并通过 Token 绑定连接或 Doris OAuth 让 MCP 请求使用该用户的连接池。
不要在日志、URL、Issue 或 Pull Request 中暴露:
diff --git a/docs/doris-fine-grained-access-control.md
b/docs/doris-fine-grained-access-control.md
new file mode 100644
index 0000000..2218c3b
--- /dev/null
+++ b/docs/doris-fine-grained-access-control.md
@@ -0,0 +1,369 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Doris fine-grained access control for MCP
+
+This guide explains how to enforce table, column, and row access for Doris MCP
+Server. The enforcement point is Apache Doris, not the language model or an MCP
+prompt.
+
+The examples use an `orders` table and an east-region reader. Replace all
+names, hosts, and predicates with values reviewed for your deployment. Test
+the statements against the exact Doris version you run before production use.
+
+## Control-plane and data-plane boundaries
+
+Doris MCP Server has two distinct authorization layers:
+
+1. MCP authorization decides whether a principal may list or call a tool,
+ resource, or prompt.
+2. Doris authorization decides which catalogs, databases, tables, columns, and
+ rows the database identity may access.
+
+An OAuth scope such as `tool:call:exec_query` permits an MCP operation. It does
+not grant `SELECT_PRIV` in Doris and does not override a Doris row policy.
+Likewise, MCP security levels, SQL validation, and response masking are
+defense-in-depth controls rather than replacements for Doris RBAC.
+
+Fine-grained Doris policy works only when the MCP request reaches Doris as the
+intended restricted identity:
+
+| MCP database route | Doris identity used | Suitable for per-principal data
policy |
+|:-------------------|:--------------------|:---------------------------------------|
+| Global service configuration | One shared service user | No; every caller
receives that user's Doris policy |
+| Token-bound database configuration | Doris user bound to that static token |
Yes; create a separate least-privilege binding per policy set |
+| Doris-backed OAuth | Doris user who signed in | Yes; MySQL-channel tools use
that user's dedicated pool |
+| External OAuth with only MCP role/scope mapping | Usually the shared service
user | No; add a reviewed Doris identity-routing design or use another route
above |
+
+Never rely on a client-supplied tenant, region, or user filter as the
+authorization boundary. The client can omit or change it. Enforce the
+predicate in Doris.
+
+## Choose the Doris mechanism
+
+| Requirement | Doris mechanism | Behavior |
+|:------------|:----------------|:---------|
+| Allow only selected tables | Table-level `SELECT_PRIV` | Other tables fail
authorization |
+| Allow only selected columns | Column-level `SELECT_PRIV(column, ...)` |
Reading another column fails authorization |
+| Allow only selected rows | Row Policy | Doris automatically adds a filter
predicate |
+| Return masked values instead of denying a column | Apache Ranger Data
Masking | Doris returns a transformed value |
+
+Column permissions currently support only `SELECT_PRIV`. Apache Doris supports
+column data masking through Apache Ranger; MCP response masking is not an
+authoritative substitute because other Doris clients would bypass it.
+
+The default Doris `root` and `admin` users bypass the fine-grained mechanisms
+described here. Do not use them to verify column permissions, Row Policies, or
+Ranger masking.
+
+## End-to-end example
+
+Assume the protected table is:
+
+```sql
+CREATE DATABASE IF NOT EXISTS sales;
+
+CREATE TABLE IF NOT EXISTS sales.orders (
+ order_id BIGINT,
+ tenant_id VARCHAR(64),
+ region_id VARCHAR(32),
+ customer_name VARCHAR(128),
+ customer_phone VARCHAR(32),
+ order_total DECIMAL(18, 2),
+ created_at DATETIME
+)
+DUPLICATE KEY(order_id)
+DISTRIBUTED BY HASH(order_id) BUCKETS 1
+PROPERTIES ("replication_num" = "1");
+```
+
+The restricted identity must be a normal business user. Limit its host pattern
+to the MCP runtime network rather than using `%` when the deployment topology
+allows it:
+
+```sql
+CREATE ROLE mcp_orders_east_reader;
+
+CREATE USER 'mcp_orders_east'@'10.%'
+IDENTIFIED BY '<generated-high-entropy-password>';
+
+GRANT 'mcp_orders_east_reader' TO 'mcp_orders_east'@'10.%';
+```
+
+### Table-level read access
+
+If the user may read every column in the table, grant table-level read access:
+
+```sql
+GRANT SELECT_PRIV
+ON internal.sales.orders
+TO ROLE 'mcp_orders_east_reader';
+```
+
+Do not add this grant when the goal is column restriction. Doris privileges are
+additive; a table-, database-, catalog-, or global-level `SELECT_PRIV` would
+also allow the sensitive columns.
+
+### Column-level access
+
+To expose only non-sensitive fields, grant `SELECT_PRIV` on the explicit
+columns:
+
+```sql
+GRANT SELECT_PRIV(
+ order_id,
+ tenant_id,
+ region_id,
+ order_total,
+ created_at
+)
+ON internal.sales.orders
+TO ROLE 'mcp_orders_east_reader';
+```
+
+The role can select the listed columns. A query that includes
+`customer_name`, `customer_phone`, or `SELECT *` must fail authorization.
+
+Before depending on this restriction, inspect and revoke any broader grants
+held directly by the user or inherited from another role:
+
+```sql
+SHOW GRANTS FOR 'mcp_orders_east'@'10.%';
+SHOW ROLES;
+```
+
+For example, if an earlier table-level grant exists:
+
+```sql
+REVOKE SELECT_PRIV
+ON internal.sales.orders
+FROM ROLE 'mcp_orders_east_reader';
+```
+
+Then apply only the explicit column grant.
+
+### Row-level access
+
+Add a restrictive Row Policy for the same role:
+
+```sql
+CREATE ROW POLICY mcp_orders_east_region
+ON sales.orders
+AS RESTRICTIVE
+TO ROLE 'mcp_orders_east_reader'
+USING (region_id = 'east');
+```
+
+Doris appends the policy predicate to queries made by identities holding that
+role. Keep the predicate deterministic, review its null behavior, and use
+stable identifiers rather than display labels.
+
+Multiple `RESTRICTIVE` policies are combined with `AND`. Multiple `PERMISSIVE`
+policies are combined with `OR`, and the restrictive and permissive groups are
+then combined. Review the full effective policy set whenever a user inherits
+multiple roles.
+
+Inspect the configured policies:
+
+```sql
+SHOW ROW POLICY;
+SHOW ROW POLICY FOR ROLE mcp_orders_east_reader;
+```
+
+An administrator can also use `EXPLAIN` on a representative query to inspect
+the rewritten plan. The decisive test is still a query made as the restricted
+business user.
+
+## Route MCP requests through the restricted user
+
+### Token-bound database configuration
+
+Create or manage a static token whose `database_config` uses the restricted
+Doris user:
+
+```json
+{
+ "database_config": {
+ "host": "doris-fe.internal.example",
+ "port": 9030,
+ "user": "mcp_orders_east",
+ "password": "<secret-store-value>",
+ "database": "sales",
+ "charset": "UTF8",
+ "fe_http_port": 8030
+ }
+}
+```
+
+Token-bound database configuration validates and uses a dedicated connection
+pool for this route. It must not fall back to the global Doris account. Store
+only a token digest in `tokens.json`, keep the file at mode `0600`, and protect
+the Doris password as a secret.
+
+Do not bind a fine-grained token to `root`, `admin`, or a service account that
+has global/table-level privileges.
+
+### Doris-backed OAuth
+
+With `ENABLE_DORIS_OAUTH_AUTH=true`, the user signs in with Doris credentials.
+The issued `doa_` token is associated with that user's dedicated Doris pool.
+Enable only reviewed MySQL-channel capabilities:
+
+```bash
+ENABLE_DORIS_OAUTH_AUTH=true
+WORKERS=1
+
+DORIS_OAUTH_DB_TOOLS_ENABLED=true
+DORIS_OAUTH_QUERY_TOOLS_ENABLED=true
+DORIS_OAUTH_EXPLAIN_TOOLS_ENABLED=true
+```
+
+`exec_query`, reviewed metadata tools, and SQL explain then reach Doris as the
+signed-in user. Doris-backed OAuth must fail closed if its per-user pool is
+missing; it must never use the global service account as a fallback.
+
+The current Doris-backed OAuth implementation is single-process and requires
+`WORKERS=1`. See the main README for its complete operational limits.
+
+### External OAuth
+
+External OAuth roles and scopes regulate MCP operations. They do not
+automatically map the external subject to a Doris user. If external OAuth
+requests still use one global Doris account, all users share that account's
+column and row policy.
+
+Use token-bound Doris credentials, Doris-backed OAuth, or a separately reviewed
+identity broker before claiming per-user Doris isolation with external OAuth.
+
+## Verification checklist
+
+Run the following tests as `mcp_orders_east`, not as `root` or `admin`.
+
+Confirm the effective identity and grants:
+
+```sql
+SELECT CURRENT_USER();
+SHOW GRANTS;
+```
+
+Confirm allowed columns and rows:
+
+```sql
+SELECT order_id, tenant_id, region_id, order_total, created_at
+FROM sales.orders
+ORDER BY order_id
+LIMIT 20;
+
+SELECT COUNT(*) AS forbidden_region_rows
+FROM sales.orders
+WHERE region_id <> 'east';
+```
+
+Expected:
+
+- the first query succeeds;
+- every returned row has `region_id = 'east'`;
+- `forbidden_region_rows` is zero.
+
+Confirm sensitive columns are denied:
+
+```sql
+SELECT customer_phone
+FROM sales.orders
+LIMIT 1;
+
+SELECT *
+FROM sales.orders
+LIMIT 1;
+```
+
+Both queries must fail authorization.
+
+Then repeat the checks through the MCP route:
+
+1. Authenticate using the token binding or Doris OAuth user.
+2. Call `exec_query` with the allowed-column query.
+3. Call `exec_query` for `customer_phone` and confirm a bounded permission
+ error, without backend credentials or exception text.
+4. Query for another region and confirm no rows are returned.
+5. Run metadata tools and confirm they do not reveal inaccessible objects or
+ columns beyond what the Doris version exposes to that user.
+6. Repeat over every supported transport used by the deployment.
+
+An MCP test that uses the global service account does not prove fine-grained
+isolation, even if the MCP caller has a different role or token.
+
+## Safe policy changes and rollback
+
+Authorization changes should be reviewed, versioned outside the secret store,
+tested with a non-production user, and applied before issuing the matching MCP
+credential.
+
+To remove the example:
+
+```sql
+DROP ROW POLICY mcp_orders_east_region
+ON sales.orders
+FOR ROLE mcp_orders_east_reader;
+
+REVOKE SELECT_PRIV(
+ order_id,
+ tenant_id,
+ region_id,
+ order_total,
+ created_at
+)
+ON internal.sales.orders
+FROM ROLE 'mcp_orders_east_reader';
+
+REVOKE 'mcp_orders_east_reader'
+FROM 'mcp_orders_east'@'10.%';
+
+DROP USER 'mcp_orders_east'@'10.%';
+DROP ROLE mcp_orders_east_reader;
+```
+
+Revoke or disable the associated MCP token before dropping the Doris user.
+Hot-reloaded token configuration should be observed until all workers have
+stopped accepting the old binding.
+
+## Common mistakes
+
+- Testing with `root` or `admin`, which bypass fine-grained controls.
+- Granting column privileges and also retaining a broader table/database
+ `SELECT_PRIV`.
+- Filtering by tenant only in generated SQL or a prompt.
+- Mapping OAuth scopes but still querying Doris with one global account.
+- Treating MCP response masking as equivalent to Doris/Ranger enforcement.
+- Logging `tokens.json`, Doris passwords, bearer tokens, or failed SQL values.
+- Assuming a Row Policy applies to FE HTTP tools that use a service account.
+ Only operations routed through the intended Doris identity inherit its data
+ authorization.
+- Deploying before testing both allowed and denied cases through the real MCP
+ transport.
+
+## Apache Doris references
+
+- [Built-in
authorization](https://doris.apache.org/docs/4.x/admin-manual/auth/authorization/internal/)
+- [Data access
control](https://doris.apache.org/docs/4.x/admin-manual/auth/authorization/data/)
+-
[GRANT](https://doris.apache.org/docs/4.x/sql-manual/sql-statements/account-management/GRANT-TO/)
+-
[REVOKE](https://doris.apache.org/docs/4.x/sql-manual/sql-statements/account-management/REVOKE-FROM/)
+- [CREATE ROW
POLICY](https://doris.apache.org/docs/4.x/sql-manual/sql-statements/data-governance/CREATE-ROW-POLICY/)
+- [SHOW ROW
POLICY](https://doris.apache.org/docs/4.x/sql-manual/sql-statements/data-governance/SHOW-ROW-POLICY/)
+- [Apache Ranger
authorization](https://doris.apache.org/docs/4.x/admin-manual/auth/authorization/ranger/)
diff --git a/test/test_product_identity.py b/test/test_product_identity.py
index d98cc74..e234ca6 100644
--- a/test/test_product_identity.py
+++ b/test/test_product_identity.py
@@ -92,3 +92,19 @@ def test_readme_language_switcher_is_bidirectional():
assert "[English](README.md) | [简体中文](README.zh-CN.md)" in readme
assert "[English](README.md) | 简体中文" in chinese_readme
+
+
+def test_fine_grained_access_control_guide_is_linked_and_complete():
+ guide_path = PROJECT_ROOT / "docs" / "doris-fine-grained-access-control.md"
+ guide = guide_path.read_text(encoding="utf-8")
+ readme = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8")
+ chinese_readme = (PROJECT_ROOT /
"README.zh-CN.md").read_text(encoding="utf-8")
+ link = "docs/doris-fine-grained-access-control.md"
+
+ assert f"]({link})" in readme
+ assert f"]({link})" in chinese_readme
+ assert "GRANT SELECT_PRIV(" in guide
+ assert "CREATE ROW POLICY" in guide
+ assert "root` and `admin" in guide
+ assert "Doris-backed OAuth" in guide
+ assert "Token-bound database configuration" in guide
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]