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 44e910a fix: make OAuth authorization mapping configurable (#157)
44e910a is described below
commit 44e910aa00ac486b3af80d41cf5b239ef6a154f8
Author: Yijia Su <[email protected]>
AuthorDate: Thu Jul 30 18:48:10 2026 +0800
fix: make OAuth authorization mapping configurable (#157)
---
.env.example | 11 ++
README.md | 52 +++--
doris_mcp_server/auth/oauth_provider.py | 92 +++++----
doris_mcp_server/utils/config.py | 281 ++++++++++++++++++++++++++++
test/auth/test_external_oauth_config.py | 63 +++++++
test/auth/test_external_oauth_validation.py | 61 ++++++
6 files changed, 502 insertions(+), 58 deletions(-)
diff --git a/.env.example b/.env.example
index 676e8b9..1b9ff10 100644
--- a/.env.example
+++ b/.env.example
@@ -208,6 +208,17 @@ OAUTH_EMAIL_CLAIM=email
OAUTH_ROLES_CLAIM=roles
OAUTH_GROUPS_CLAIM=groups
+# External OAuth authorization mapping. Role names are matched
+# case-insensitively. Trusted domains require email_verified=true.
+OAUTH_DEFAULT_ROLES=oauth_user
+OAUTH_DEFAULT_SECURITY_LEVEL=internal
+OAUTH_DEFAULT_PERMISSIONS=read_data
+OAUTH_TRUSTED_DOMAINS=
+OAUTH_TRUSTED_DOMAIN_SECURITY_LEVEL=confidential
+# JSON values replace the complete built-in mapping when configured.
+# OAUTH_ROLE_SECURITY_LEVELS_JSON={"analyst":"internal","executive":"secret"}
+#
OAUTH_ROLE_PERMISSIONS_JSON={"analyst":["read_data","query_database"],"executive":["read_data"]}
+
# OAuth session settings
OAUTH_SESSION_SECRET=your_oauth_session_secret_here
OAUTH_SESSION_EXPIRY=3600
diff --git a/README.md b/README.md
index 5bcd34a..6f735a7 100644
--- a/README.md
+++ b/README.md
@@ -1116,29 +1116,41 @@ The system supports four security levels with
hierarchical access control:
#### Role Configuration
-Configure user roles and permissions:
+External OAuth role mapping is configured through environment variables:
-```python
-# Example role configuration
-role_permissions = {
- "data_analyst": {
- "security_level": "internal",
- "permissions": ["read_data", "execute_query"],
- "allowed_tables": ["sales", "products", "orders"]
- },
- "data_admin": {
- "security_level": "confidential",
- "permissions": ["read_data", "execute_query", "admin"],
- "allowed_tables": ["*"]
- },
- "executive": {
- "security_level": "secret",
- "permissions": ["read_data", "execute_query", "admin"],
- "allowed_tables": ["*"]
- }
-}
+```env
+# Roles supplied when the provider returns no role claim
+OAUTH_DEFAULT_ROLES=oauth_user
+
+# Fallbacks for users whose roles do not occur in the JSON mappings
+OAUTH_DEFAULT_SECURITY_LEVEL=internal
+OAUTH_DEFAULT_PERMISSIONS=read_data
+
+# Exact domains only. Domain elevation is applied only when the provider
+# returns email_verified=true.
+OAUTH_TRUSTED_DOMAINS=example.com,internal.example.com
+OAUTH_TRUSTED_DOMAIN_SECURITY_LEVEL=confidential
+
+# Each JSON value replaces the complete built-in mapping.
+OAUTH_ROLE_SECURITY_LEVELS_JSON={"analyst":"internal","executive":"secret"}
+OAUTH_ROLE_PERMISSIONS_JSON={"analyst":["read_data","query_database"],"executive":["read_data","query_database","admin"]}
```
+Role names and trusted domains are matched case-insensitively. Supported
+security levels are `public`, `internal`, `confidential`, and `secret`. An
+explicit empty permission array denies application permissions for that role;
+an empty `OAUTH_DEFAULT_PERMISSIONS` value makes unknown roles fail closed.
+
+The built-in role defaults preserve previous behavior for `admin`,
+`administrator`, `data_admin`, `super_admin`, `data_analyst`, `developer`,
+`manager`, `viewer`, `user`, and `oauth_user`. No email domain is trusted by
+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.
+
### SQL Security Validation
The system automatically validates SQL queries for security risks:
diff --git a/doris_mcp_server/auth/oauth_provider.py
b/doris_mcp_server/auth/oauth_provider.py
index 24c649b..ae5e0a8 100644
--- a/doris_mcp_server/auth/oauth_provider.py
+++ b/doris_mcp_server/auth/oauth_provider.py
@@ -36,6 +36,13 @@ from .oauth_types import OAuthTokens, OAuthUserInfo
logger = get_logger(__name__)
+_SECURITY_LEVEL_RANK = {
+ SecurityLevel.PUBLIC: 0,
+ SecurityLevel.INTERNAL: 1,
+ SecurityLevel.CONFIDENTIAL: 2,
+ SecurityLevel.SECRET: 3,
+}
+
class OAuthAuthenticationProvider:
"""OAuth authentication provider for Doris MCP Server"""
@@ -275,26 +282,42 @@ class OAuthAuthenticationProvider:
Returns:
SecurityLevel for the user
"""
- # Check if user has admin roles
- admin_roles = {"admin", "administrator", "data_admin", "super_admin"}
- if any(role.lower() in admin_roles for role in user_info.roles):
- return SecurityLevel.SECRET
-
- # Check email domain for internal users
- if user_info.email:
- # You can configure trusted domains for internal access
- trusted_domains = ["yourcompany.com", "internal.org"] # Configure
as needed
- email_domain = user_info.email.split("@")[-1].lower()
- if email_domain in trusted_domains:
- return SecurityLevel.CONFIDENTIAL
-
- # Check for special roles
- elevated_roles = {"data_analyst", "developer", "manager"}
- if any(role.lower() in elevated_roles for role in user_info.roles):
- return SecurityLevel.CONFIDENTIAL
-
- # Default to internal level for OAuth users
- return SecurityLevel.INTERNAL
+ security_config = self.config.security
+ role_levels = {
+ str(role).strip().lower():
SecurityLevel(str(level).strip().lower())
+ for role, level in
security_config.oauth_role_security_levels.items()
+ }
+ matched_levels = [
+ role_levels[role]
+ for role in {
+ configured_role.strip().lower()
+ for configured_role in user_info.roles
+ }
+ if role in role_levels
+ ]
+
+ if user_info.email and user_info.email_verified is True:
+ local_part, separator, email_domain =
user_info.email.rpartition("@")
+ trusted_domains = {
+ domain.strip().lower().removeprefix("@")
+ for domain in security_config.oauth_trusted_domains
+ }
+ if (
+ separator
+ and local_part
+ and email_domain.lower() in trusted_domains
+ ):
+ matched_levels.append(
+ SecurityLevel(
+
security_config.oauth_trusted_domain_security_level.lower()
+ )
+ )
+
+ if matched_levels:
+ return max(matched_levels, key=_SECURITY_LEVEL_RANK.__getitem__)
+ return SecurityLevel(
+ security_config.oauth_default_security_level.strip().lower()
+ )
async def _map_permissions(self, roles: list[str]) -> list[str]:
"""Map OAuth roles to application permissions
@@ -305,32 +328,25 @@ class OAuthAuthenticationProvider:
Returns:
List of application permissions
"""
- permissions = set()
-
- # Role to permission mapping
+ permissions: set[str] = set()
+ matched_role = False
role_permissions = {
- "admin": ["admin", "read_data", "write_data", "manage_users"],
- "administrator": ["admin", "read_data", "write_data",
"manage_users"],
- "data_admin": ["admin", "read_data", "write_data"],
- "super_admin": ["admin", "read_data", "write_data",
"manage_users", "system_admin"],
- "data_analyst": ["read_data", "query_database"],
- "developer": ["read_data", "query_database", "debug"],
- "viewer": ["read_data"],
- "user": ["read_data"],
- "oauth_user": ["read_data"] # Default OAuth user permission
+ str(role).strip().lower(): configured_permissions
+ for role, configured_permissions in (
+ self.config.security.oauth_role_permissions.items()
+ )
}
- # Map roles to permissions
for role in roles:
- role_lower = role.lower()
+ role_lower = role.strip().lower()
if role_lower in role_permissions:
+ matched_role = True
permissions.update(role_permissions[role_lower])
- # Ensure OAuth users have at least basic permissions
- if not permissions:
- permissions.add("read_data")
+ if not matched_role:
+ permissions.update(self.config.security.oauth_default_permissions)
- return list(permissions)
+ return sorted(permissions)
def get_provider_info(self) -> dict[str, Any]:
"""Get OAuth provider information
diff --git a/doris_mcp_server/utils/config.py b/doris_mcp_server/utils/config.py
index dcbd7c5..6102c81 100644
--- a/doris_mcp_server/utils/config.py
+++ b/doris_mcp_server/utils/config.py
@@ -66,6 +66,36 @@ DORIS_OAUTH_METADATA_TOOL_ALLOWLIST_DEFAULT = list(
DORIS_OAUTH_METADATA_TOOL_NAMES
)
+EXTERNAL_OAUTH_SECURITY_LEVELS = frozenset(
+ {"public", "internal", "confidential", "secret"}
+)
+DEFAULT_EXTERNAL_OAUTH_ROLE_SECURITY_LEVELS = {
+ "admin": "secret",
+ "administrator": "secret",
+ "data_admin": "secret",
+ "super_admin": "secret",
+ "data_analyst": "confidential",
+ "developer": "confidential",
+ "manager": "confidential",
+}
+DEFAULT_EXTERNAL_OAUTH_ROLE_PERMISSIONS = {
+ "admin": ["admin", "read_data", "write_data", "manage_users"],
+ "administrator": ["admin", "read_data", "write_data", "manage_users"],
+ "data_admin": ["admin", "read_data", "write_data"],
+ "super_admin": [
+ "admin",
+ "read_data",
+ "write_data",
+ "manage_users",
+ "system_admin",
+ ],
+ "data_analyst": ["read_data", "query_database"],
+ "developer": ["read_data", "query_database", "debug"],
+ "viewer": ["read_data"],
+ "user": ["read_data"],
+ "oauth_user": ["read_data"],
+}
+
@dataclass(frozen=True)
class ConfigValue:
@@ -146,6 +176,52 @@ def _env_csv(name: str, default: list[str]) -> list[str]:
return [part.strip() for part in value.split(",") if part.strip()]
+def _env_json_string_map(
+ name: str,
+ default: dict[str, str],
+) -> dict[str, str]:
+ value = os.getenv(name)
+ if value is None:
+ return default
+ try:
+ parsed = json.loads(value)
+ except json.JSONDecodeError as exc:
+ raise AuthConfigError(f"{name} must be a valid JSON object") from exc
+ if not isinstance(parsed, dict) or any(
+ not isinstance(key, str) or not isinstance(item, str)
+ for key, item in parsed.items()
+ ):
+ raise AuthConfigError(f"{name} must map strings to strings")
+ return {
+ key.strip(): item.strip()
+ for key, item in parsed.items()
+ }
+
+
+def _env_json_string_list_map(
+ name: str,
+ default: dict[str, list[str]],
+) -> dict[str, list[str]]:
+ value = os.getenv(name)
+ if value is None:
+ return default
+ try:
+ parsed = json.loads(value)
+ except json.JSONDecodeError as exc:
+ raise AuthConfigError(f"{name} must be a valid JSON object") from exc
+ if not isinstance(parsed, dict) or any(
+ not isinstance(key, str)
+ or not isinstance(items, list)
+ or any(not isinstance(item, str) for item in items)
+ for key, items in parsed.items()
+ ):
+ raise AuthConfigError(f"{name} must map strings to arrays of strings")
+ return {
+ key.strip(): [item.strip() for item in items]
+ for key, items in parsed.items()
+ }
+
+
def _coerce_csv_config(value: Any) -> list[str]:
if value is None:
return []
@@ -482,6 +558,21 @@ class SecurityConfig:
oauth_name_claim: str = "name"
oauth_roles_claim: str = "roles" # Custom claim for roles
oauth_default_roles: list[str] = field(default_factory=lambda:
["oauth_user"])
+ oauth_default_security_level: str = "internal"
+ oauth_trusted_domains: list[str] = field(default_factory=list)
+ oauth_trusted_domain_security_level: str = "confidential"
+ oauth_role_security_levels: dict[str, str] = field(
+ default_factory=lambda:
dict(DEFAULT_EXTERNAL_OAUTH_ROLE_SECURITY_LEVELS)
+ )
+ oauth_role_permissions: dict[str, list[str]] = field(
+ default_factory=lambda: {
+ role: list(permissions)
+ for role, permissions in
DEFAULT_EXTERNAL_OAUTH_ROLE_PERMISSIONS.items()
+ }
+ )
+ oauth_default_permissions: list[str] = field(
+ default_factory=lambda: ["read_data"]
+ )
def __post_init__(self) -> None:
"""Initialize default OAuth scopes based on provider"""
@@ -496,6 +587,142 @@ class SecurityConfig:
self.oauth_scopes = ["openid", "email", "profile"]
+def _normalize_external_oauth_authorization_config(
+ security: SecurityConfig,
+) -> None:
+ """Normalize and validate external OAuth authorization mappings."""
+
+ def normalize_security_level(value: Any, setting: str) -> str:
+ normalized = str(value or "").strip().lower()
+ if normalized not in EXTERNAL_OAUTH_SECURITY_LEVELS:
+ allowed = ", ".join(sorted(EXTERNAL_OAUTH_SECURITY_LEVELS))
+ raise AuthConfigError(f"{setting} must be one of {allowed}")
+ return normalized
+
+ security.oauth_default_security_level = normalize_security_level(
+ security.oauth_default_security_level,
+ "OAUTH_DEFAULT_SECURITY_LEVEL",
+ )
+ security.oauth_trusted_domain_security_level = normalize_security_level(
+ security.oauth_trusted_domain_security_level,
+ "OAUTH_TRUSTED_DOMAIN_SECURITY_LEVEL",
+ )
+
+ if not isinstance(security.oauth_trusted_domains, list) or any(
+ not isinstance(domain, str)
+ for domain in security.oauth_trusted_domains
+ ):
+ raise AuthConfigError(
+ "OAUTH_TRUSTED_DOMAINS must be a comma-separated list of domains"
+ )
+ normalized_domains = []
+ for configured_domain in security.oauth_trusted_domains:
+ domain = str(configured_domain).strip().lower().removeprefix("@")
+ if (
+ not domain
+ or "@" in domain
+ or any(character.isspace() for character in domain)
+ ):
+ raise AuthConfigError(
+ "OAUTH_TRUSTED_DOMAINS must contain exact email domains"
+ )
+ if domain not in normalized_domains:
+ normalized_domains.append(domain)
+ security.oauth_trusted_domains = normalized_domains
+
+ normalized_role_levels: dict[str, str] = {}
+ if not isinstance(security.oauth_role_security_levels, dict):
+ raise AuthConfigError(
+ "OAUTH_ROLE_SECURITY_LEVELS_JSON must be a JSON object"
+ )
+ for configured_role, configured_level in (
+ security.oauth_role_security_levels.items()
+ ):
+ if not isinstance(configured_role, str) or not isinstance(
+ configured_level,
+ str,
+ ):
+ raise AuthConfigError(
+ "OAUTH_ROLE_SECURITY_LEVELS_JSON must map strings to strings"
+ )
+ role = configured_role.strip().lower()
+ if not role:
+ raise AuthConfigError(
+ "OAUTH_ROLE_SECURITY_LEVELS_JSON contains an empty role"
+ )
+ normalized_role_levels[role] = normalize_security_level(
+ configured_level,
+ f"OAUTH_ROLE_SECURITY_LEVELS_JSON[{role}]",
+ )
+ security.oauth_role_security_levels = normalized_role_levels
+
+ normalized_role_permissions: dict[str, list[str]] = {}
+ if not isinstance(security.oauth_role_permissions, dict):
+ raise AuthConfigError(
+ "OAUTH_ROLE_PERMISSIONS_JSON must be a JSON object"
+ )
+ for configured_role, configured_permissions in (
+ security.oauth_role_permissions.items()
+ ):
+ if not isinstance(configured_role, str):
+ raise AuthConfigError(
+ "OAUTH_ROLE_PERMISSIONS_JSON role names must be strings"
+ )
+ role = configured_role.strip().lower()
+ if not role:
+ raise AuthConfigError(
+ "OAUTH_ROLE_PERMISSIONS_JSON contains an empty role"
+ )
+ if not isinstance(configured_permissions, list) or any(
+ not isinstance(permission, str)
+ for permission in configured_permissions
+ ):
+ raise AuthConfigError(
+ f"OAUTH_ROLE_PERMISSIONS_JSON[{role}] must be an array of
strings"
+ )
+ permissions = []
+ for configured_permission in configured_permissions:
+ permission = configured_permission.strip()
+ if not permission:
+ raise AuthConfigError(
+ f"OAUTH_ROLE_PERMISSIONS_JSON[{role}] contains an "
+ "empty permission"
+ )
+ if permission not in permissions:
+ permissions.append(permission)
+ normalized_role_permissions[role] = permissions
+ security.oauth_role_permissions = normalized_role_permissions
+
+ if not isinstance(security.oauth_default_roles, list) or any(
+ not isinstance(role, str)
+ for role in security.oauth_default_roles
+ ):
+ raise AuthConfigError(
+ "OAUTH_DEFAULT_ROLES must be a comma-separated list"
+ )
+ if not isinstance(security.oauth_default_permissions, list) or any(
+ not isinstance(permission, str)
+ for permission in security.oauth_default_permissions
+ ):
+ raise AuthConfigError(
+ "OAUTH_DEFAULT_PERMISSIONS must be a comma-separated list"
+ )
+ security.oauth_default_roles = list(
+ dict.fromkeys(
+ role.strip()
+ for role in security.oauth_default_roles
+ if role.strip()
+ )
+ )
+ security.oauth_default_permissions = list(
+ dict.fromkeys(
+ permission.strip()
+ for permission in security.oauth_default_permissions
+ if permission.strip()
+ )
+ )
+
+
@dataclass
class PerformanceConfig:
"""Performance configuration"""
@@ -831,6 +1058,52 @@ class DorisConfig:
os.getenv("OAUTH_REQUIRED_SCOPE")
)
_mark_source(config, "oauth_required_scopes", "env")
+ if "OAUTH_DEFAULT_ROLES" in os.environ:
+ config.security.oauth_default_roles = _env_csv(
+ "OAUTH_DEFAULT_ROLES",
+ config.security.oauth_default_roles,
+ )
+ _mark_source(config, "oauth_default_roles", "env")
+ if "OAUTH_DEFAULT_SECURITY_LEVEL" in os.environ:
+ config.security.oauth_default_security_level = os.getenv(
+ "OAUTH_DEFAULT_SECURITY_LEVEL",
+ config.security.oauth_default_security_level,
+ ).strip()
+ _mark_source(config, "oauth_default_security_level", "env")
+ if "OAUTH_TRUSTED_DOMAINS" in os.environ:
+ config.security.oauth_trusted_domains = _env_csv(
+ "OAUTH_TRUSTED_DOMAINS",
+ config.security.oauth_trusted_domains,
+ )
+ _mark_source(config, "oauth_trusted_domains", "env")
+ if "OAUTH_TRUSTED_DOMAIN_SECURITY_LEVEL" in os.environ:
+ config.security.oauth_trusted_domain_security_level = os.getenv(
+ "OAUTH_TRUSTED_DOMAIN_SECURITY_LEVEL",
+ config.security.oauth_trusted_domain_security_level,
+ ).strip()
+ _mark_source(
+ config,
+ "oauth_trusted_domain_security_level",
+ "env",
+ )
+ if "OAUTH_ROLE_SECURITY_LEVELS_JSON" in os.environ:
+ config.security.oauth_role_security_levels = _env_json_string_map(
+ "OAUTH_ROLE_SECURITY_LEVELS_JSON",
+ config.security.oauth_role_security_levels,
+ )
+ _mark_source(config, "oauth_role_security_levels", "env")
+ if "OAUTH_ROLE_PERMISSIONS_JSON" in os.environ:
+ config.security.oauth_role_permissions = _env_json_string_list_map(
+ "OAUTH_ROLE_PERMISSIONS_JSON",
+ config.security.oauth_role_permissions,
+ )
+ _mark_source(config, "oauth_role_permissions", "env")
+ if "OAUTH_DEFAULT_PERMISSIONS" in os.environ:
+ config.security.oauth_default_permissions = _env_csv(
+ "OAUTH_DEFAULT_PERMISSIONS",
+ config.security.oauth_default_permissions,
+ )
+ _mark_source(config, "oauth_default_permissions", "env")
if "ENABLE_DORIS_OAUTH_AUTH" in os.environ:
config.security.enable_doris_oauth_auth =
_str_to_bool(os.getenv("ENABLE_DORIS_OAUTH_AUTH"))
_mark_source(config, "enable_doris_oauth_auth", "env")
@@ -1325,6 +1598,13 @@ class DorisConfig:
"oauth_scopes": self.security.oauth_scopes,
"oauth_required_scopes": self.security.oauth_required_scopes,
"oauth_introspection_endpoint":
self.security.oauth_introspection_endpoint,
+ "oauth_default_roles": self.security.oauth_default_roles,
+ "oauth_default_security_level":
self.security.oauth_default_security_level,
+ "oauth_trusted_domains": self.security.oauth_trusted_domains,
+ "oauth_trusted_domain_security_level":
self.security.oauth_trusted_domain_security_level,
+ "oauth_role_security_levels":
self.security.oauth_role_security_levels,
+ "oauth_role_permissions": self.security.oauth_role_permissions,
+ "oauth_default_permissions":
self.security.oauth_default_permissions,
"enable_doris_oauth_auth":
self.security.enable_doris_oauth_auth,
"allow_unauthenticated_non_loopback":
self.security.allow_unauthenticated_non_loopback,
"doris_oauth_base_url": self.security.doris_oauth_base_url,
@@ -1854,6 +2134,7 @@ def normalize_effective_auth_config(
raise AuthConfigError(str(exc)) from exc
if enable_external_oauth_auth:
+ _normalize_external_oauth_authorization_config(config.security)
config.security.oauth_issuer = _validate_external_oauth_url(
config.security.oauth_issuer,
setting="OAUTH_ISSUER",
diff --git a/test/auth/test_external_oauth_config.py
b/test/auth/test_external_oauth_config.py
index 1bf31da..75ca2e9 100644
--- a/test/auth/test_external_oauth_config.py
+++ b/test/auth/test_external_oauth_config.py
@@ -135,6 +135,17 @@ def
test_external_oauth_environment_maps_token_context_settings(monkeypatch):
"OAUTH_USERINFO_URL": f"{ISSUER}/userinfo",
"OAUTH_SCOPE": "tool:list, resource:list resource:read",
"OAUTH_REQUIRED_SCOPE": "tool:list resource:read",
+ "OAUTH_DEFAULT_ROLES": "member, auditor",
+ "OAUTH_DEFAULT_SECURITY_LEVEL": "public",
+ "OAUTH_TRUSTED_DOMAINS": "Example.COM, internal.example.test",
+ "OAUTH_TRUSTED_DOMAIN_SECURITY_LEVEL": "confidential",
+ "OAUTH_ROLE_SECURITY_LEVELS_JSON": (
+ '{"member":"internal","executive":"secret"}'
+ ),
+ "OAUTH_ROLE_PERMISSIONS_JSON": (
+ '{"member":["read_data"],"auditor":["read_data","audit"]}'
+ ),
+ "OAUTH_DEFAULT_PERMISSIONS": "",
}
for name, value in values.items():
monkeypatch.setenv(name, value)
@@ -159,3 +170,55 @@ def
test_external_oauth_environment_maps_token_context_settings(monkeypatch):
]
assert config.security.oauth_introspection_client_id ==
("introspection-client")
assert config.security.oauth_introspection_client_secret ==
("introspection-secret")
+ assert config.security.oauth_default_roles == ["member", "auditor"]
+ assert config.security.oauth_default_security_level == "public"
+ assert config.security.oauth_trusted_domains == [
+ "example.com",
+ "internal.example.test",
+ ]
+ assert config.security.oauth_trusted_domain_security_level ==
"confidential"
+ assert config.security.oauth_role_security_levels == {
+ "member": "internal",
+ "executive": "secret",
+ }
+ assert config.security.oauth_role_permissions == {
+ "member": ["read_data"],
+ "auditor": ["read_data", "audit"],
+ }
+ assert config.security.oauth_default_permissions == []
+
+
[email protected](
+ ("name", "value", "message"),
+ [
+ (
+ "OAUTH_ROLE_SECURITY_LEVELS_JSON",
+ '["admin"]',
+ "must map strings to strings",
+ ),
+ (
+ "OAUTH_ROLE_PERMISSIONS_JSON",
+ '{"admin":"read_data"}',
+ "must map strings to arrays of strings",
+ ),
+ ],
+)
+def test_external_oauth_environment_rejects_invalid_authorization_json(
+ monkeypatch,
+ tmp_path,
+ name,
+ value,
+ message,
+):
+ monkeypatch.setenv(name, value)
+
+ with pytest.raises(AuthConfigError, match=message):
+ DorisConfig.from_env(str(tmp_path / "missing.env"))
+
+
+def test_external_oauth_rejects_invalid_authorization_security_level():
+ config = _external_oauth_config()
+ config.security.oauth_default_security_level = "top-secret"
+
+ with pytest.raises(AuthConfigError, match="OAUTH_DEFAULT_SECURITY_LEVEL"):
+ normalize_effective_auth_config(config)
diff --git a/test/auth/test_external_oauth_validation.py
b/test/auth/test_external_oauth_validation.py
index 7af778e..e719a95 100644
--- a/test/auth/test_external_oauth_validation.py
+++ b/test/auth/test_external_oauth_validation.py
@@ -23,6 +23,8 @@ from doris_mcp_server.auth.oauth_token_validation import (
OAuthAccessTokenValidationError,
)
from doris_mcp_server.auth.oauth_types import OAuthState, OAuthTokens,
OAuthUserInfo
+from doris_mcp_server.utils.config import DorisConfig
+from doris_mcp_server.utils.security import SecurityLevel
ISSUER = "https://issuer.example.test"
RESOURCE = "https://mcp.example.test/mcp"
@@ -76,6 +78,7 @@ class _FlowOAuthClient:
def _provider(client) -> OAuthAuthenticationProvider:
provider = object.__new__(OAuthAuthenticationProvider)
+ provider.config = DorisConfig()
provider.enabled = True
provider.oauth_client = client
return provider
@@ -160,3 +163,61 @@ async def
test_userinfo_subject_must_match_introspected_subject():
await provider.authenticate_with_token("access-1")
assert exc_info.value.error == "invalid_token"
+
+
[email protected]
+async def test_external_oauth_authorization_uses_configured_role_mappings():
+ provider = _provider(_FlowOAuthClient())
+ provider.config.security.oauth_role_security_levels = {
+ "analyst": "confidential",
+ "executive": "secret",
+ }
+ provider.config.security.oauth_role_permissions = {
+ "analyst": ["query_database", "read_data"],
+ "suspended": [],
+ }
+ provider.config.security.oauth_default_permissions = []
+
+ user_info = OAuthUserInfo(
+ sub="user-1",
+ roles=["Analyst", "Executive"],
+ )
+
+ assert (
+ await provider._determine_security_level(user_info)
+ is SecurityLevel.SECRET
+ )
+ assert await provider._map_permissions(user_info.roles) == [
+ "query_database",
+ "read_data",
+ ]
+ assert await provider._map_permissions(["unknown"]) == []
+ assert await provider._map_permissions(["suspended"]) == []
+
+
[email protected]
+async def test_external_oauth_trusted_domain_requires_verified_email():
+ provider = _provider(_FlowOAuthClient())
+ provider.config.security.oauth_trusted_domains = ["example.test"]
+ provider.config.security.oauth_trusted_domain_security_level =
"confidential"
+ provider.config.security.oauth_default_security_level = "public"
+
+ verified_user = OAuthUserInfo(
+ sub="verified",
+ email="[email protected]",
+ email_verified=True,
+ )
+ unverified_user = OAuthUserInfo(
+ sub="unverified",
+ email="[email protected]",
+ email_verified=False,
+ )
+
+ assert (
+ await provider._determine_security_level(verified_user)
+ is SecurityLevel.CONFIDENTIAL
+ )
+ assert (
+ await provider._determine_security_level(unverified_user)
+ is SecurityLevel.PUBLIC
+ )
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]