Skip to main content
Glama
Arize-ai

@arizeai/phoenix-mcp

Official
by Arize-ai
grafana-comparison.md20.1 kB
# Grafana Source Code Research **Complete Grafana Source Code Analysis** - Detailed findings from reviewing Grafana's LDAP implementation to ensure Phoenix compatibility. ## Files Reviewed 1. [settings.go](https://raw.githubusercontent.com/grafana/grafana/84a07be6e4e1acd8f064c3b390c30188d5703afc/pkg/services/ldap/settings.go) - Struct definitions, defaults 2. [ldap.toml](https://raw.githubusercontent.com/grafana/grafana/84a07be6e4e1acd8f064c3b390c30188d5703afc/conf/ldap.toml) - Example config 3. [service/ldap.go](https://raw.githubusercontent.com/grafana/grafana/84a07be6e4e1acd8f064c3b390c30188d5703afc/pkg/services/ldap/service/ldap.go) - Validation, defaults 4. [ldap.go](https://raw.githubusercontent.com/grafana/grafana/84a07be6e4e1acd8f064c3b390c30188d5703afc/pkg/services/ldap/ldap.go) - Group matching logic 5. [helpers.go](https://raw.githubusercontent.com/grafana/grafana/84a07be6e4e1acd8f064c3b390c30188d5703afc/pkg/services/ldap/helpers.go) - `IsMemberOf()` function 6. [multildap.go](https://raw.githubusercontent.com/grafana/grafana/84a07be6e4e1acd8f064c3b390c30188d5703afc/pkg/services/ldap/multildap/multildap.go) - Multi-server failover ## Grafana's GroupToOrgRole Struct ```go type GroupToOrgRole struct { GroupDN string `json:"group_dn"` OrgId int64 `json:"org_id"` // Defaults to 1 IsGrafanaAdmin *bool `json:"grafana_admin"` // Optional, for server admin OrgRole RoleType `json:"org_role"` // "Admin", "Editor", "Viewer" } ``` ## Grafana's Group Matching Logic ```go func IsMemberOf(memberOf []string, group string) bool { if group == "*" { return true // Wildcard matches ALL users } for _, member := range memberOf { if strings.EqualFold(member, group) { // Case-insensitive! return true } } return false } ``` ## Phoenix Adaptation Phoenix adapts Grafana's configuration format while accounting for Phoenix-specific differences: **Similarities**: - ✅ Wildcard `"*"` support - MATCHES GRAFANA (checked first, matches all users) - ✅ **Case-insensitive DN matching** - MATCHES GRAFANA (`strings.EqualFold`) - ✅ First-match-wins priority - MATCHES GRAFANA - ✅ Multi-server failover (tries in order) - MATCHES GRAFANA **Phoenix-Specific Differences**: - ✅ Field name: `role` (not `org_role`) - Phoenix has no organizations - ✅ Values: `"ADMIN"`, `"MEMBER"`, `"VIEWER"` (uppercase) - Matches Phoenix role names - ⚠️ Omit `org_id` - Phoenix doesn't have multi-org support - ⚠️ Omit `grafana_admin` - Phoenix doesn't have server admin concept - ⚠️ Omit team sync (`TeamOrgGroupDTO`) - Phoenix doesn't have teams, only roles ## Configuration Field Comparison Complete field-by-field comparison between Grafana's `ServerConfig` struct and Phoenix's environment variables. ### Server Connection | Grafana (`settings.go`) | Phoenix | Status | |------------------------|---------|--------| | `Host string` | `PHOENIX_LDAP_HOST` | ✅ Match | | `Port int` (default 389) | `PHOENIX_LDAP_PORT` (default 389/636 based on mode) | ✅ Match | ### TLS Configuration | Grafana | Phoenix | Status | |---------|---------|--------| | `UseSSL bool` | Combined: `PHOENIX_LDAP_TLS_MODE=ldaps` | ✅ Equivalent | | `StartTLS bool` | Combined: `PHOENIX_LDAP_TLS_MODE=starttls` | ✅ Equivalent | | `SkipVerifySSL bool` | `PHOENIX_LDAP_TLS_VERIFY` (inverted logic) | ✅ Equivalent | | `MinTLSVersion string` | ❌ Not implemented | ⚠️ Low priority | | `TLSCiphers []string` | ❌ Not implemented | ⚠️ Low priority | | `RootCACert string` | `PHOENIX_LDAP_TLS_CA_CERT_FILE` | ✅ Match | | `ClientCert string` | `PHOENIX_LDAP_TLS_CLIENT_CERT_FILE` | ✅ Match | | `ClientKey string` | `PHOENIX_LDAP_TLS_CLIENT_KEY_FILE` | ✅ Match | ### Bind Configuration | Grafana | Phoenix | Status | |---------|---------|--------| | `BindDN string` | `PHOENIX_LDAP_BIND_DN` | ✅ Match | | `BindPassword string` | `PHOENIX_LDAP_BIND_PASSWORD` | ✅ Match | | `Timeout int` (default 10s) | ❌ Not implemented | ⚠️ Low priority | ### User Search | Grafana | Phoenix | Status | |---------|---------|--------| | `SearchBaseDNs []string` | `PHOENIX_LDAP_USER_SEARCH_BASE_DNS` (JSON array) | ✅ **Match** | | `SearchFilter string` | `PHOENIX_LDAP_USER_SEARCH_FILTER` | ✅ Match | ### Attribute Mapping | Grafana (`AttributeMap`) | Phoenix | Status | |--------------------------|---------|--------| | `Email string` | `PHOENIX_LDAP_ATTR_EMAIL` | ✅ Match | | `Name string` | `PHOENIX_LDAP_ATTR_DISPLAY_NAME` | ✅ Match | | `MemberOf string` | `PHOENIX_LDAP_ATTR_MEMBER_OF` | ✅ Match | | `Username string` | ❌ Not needed (from login form) | ✅ N/A | | `Surname string` | ❌ Not implemented | ✅ Not needed | | ❌ Not in Grafana | `PHOENIX_LDAP_ATTR_UNIQUE_ID` | ✅ Phoenix extra | ### Group Search (POSIX) | Grafana | Phoenix | Status | |---------|---------|--------| | `GroupSearchBaseDNs []string` | `PHOENIX_LDAP_GROUP_SEARCH_BASE_DNS` (JSON array) | ✅ **Match** | | `GroupSearchFilter string` | `PHOENIX_LDAP_GROUP_SEARCH_FILTER` | ✅ Match | | `GroupSearchFilterUserAttribute string` | `PHOENIX_LDAP_GROUP_SEARCH_FILTER_USER_ATTR` | ✅ **Match** | #### Group Search Filter User Attribute Behavior Both Grafana and Phoenix support `group_search_filter_user_attribute` (Grafana) / `GROUP_SEARCH_FILTER_USER_ATTR` (Phoenix) with identical behavior: **Grafana** (`ldap.go` line 598-605): ```go if config.GroupSearchFilterUserAttribute == "" { filterReplace = getAttribute(config.Attr.Username, entry) } else { filterReplace = getAttribute( config.GroupSearchFilterUserAttribute, entry, ) } ``` **Phoenix** (`ldap.py`): ```python if self.config.group_search_filter_user_attr: filter_value = _get_attribute(user_entry, self.config.group_search_filter_user_attr) else: filter_value = username # Uses login username ``` **Common values:** - `"uid"`: For POSIX groups using `memberUid` (contains uid attribute value) - `"dn"` or `"distinguishedName"`: For groups using `member` with full DNs (Active Directory) - Not set (default): Uses the login username (works for most POSIX setups) **Grafana documentation reference:** - https://grafana.com/docs/grafana/latest/setup-grafana/configure-access/configure-authentication/ldap/ ### Group Role Mappings | Grafana (`GroupToOrgRole`) | Phoenix | Status | |---------------------------|---------|--------| | `GroupDN string` | `group_dn` | ✅ Match | | `OrgRole RoleType` | `role` (not `org_role`) | ✅ Intentional | | `OrgId int64` | ❌ Not applicable | ✅ N/A (no orgs) | | `IsGrafanaAdmin *bool` | ❌ Not applicable | ✅ N/A | ### Sign-up Control | Grafana | Phoenix | Status | |---------|---------|--------| | `AllowSignUp bool` | `PHOENIX_LDAP_ALLOW_SIGN_UP` | ✅ Match | --- ### Missing Features (Low Priority) These Grafana features are not implemented in Phoenix. They can be added later if requested: | Feature | Grafana | Use Case | |---------|---------|----------| | `MinTLSVersion` | Force TLS 1.2+ | Enterprise security compliance | | `TLSCiphers` | Restrict cipher suites | Enterprise security compliance | | `Timeout` | Connection timeout (seconds) | Network reliability | ### Phoenix Extras (Not in Grafana) | Feature | Phoenix | Use Case | |---------|---------|----------| | `PHOENIX_LDAP_ATTR_UNIQUE_ID` | Immutable user identifier | Email change resilience (objectGUID, entryUUID) | --- ## Implementation Logic Discrepancies Detailed comparison of runtime behavior between Grafana and Phoenix LDAP implementations. ### 1. Multi-Server Failover Behavior Both Grafana and Phoenix support multiple LDAP servers for high availability, but they handle different error conditions differently. #### Failover Trigger Comparison | Error Condition | Grafana | Phoenix | Match? | |----------------|---------|---------|--------| | **Connection failed** (Dial error) | ✅ Try next server | ✅ Try next server | ✅ Match | | **User not found** | ✅ Try next server | ❌ Return immediately | ⚠️ **Different** | | **Invalid password** | ✅ Try next server | ❌ Return immediately | ⚠️ **Different** | | **Other LDAP errors** | ❌ Return error | ✅ Try next server | ⚠️ **Different** | #### Grafana's Approach (`multildap.go`) Grafana defines "silent errors" that trigger failover: ```go // isSilentError - these errors cause failover to next server func isSilentError(err error) bool { continueErrs := []error{ErrInvalidCredentials, ErrCouldNotFindUser} for _, cerr := range continueErrs { if errors.Is(err, cerr) { return true } } return false } // Login - tries all servers func (multiples *MultiLDAP) Login(query *login.LoginUserQuery) { ldapSilentErrors := []error{} for index, config := range multiples.configs { // Dial failure → try next server if err := server.Dial(); err != nil { if index == len(multiples.configs)-1 { return nil, err // Last server, return error } continue // Try next } user, err := server.Login(query) if err != nil { if isSilentError(err) { // User not found OR invalid password ldapSilentErrors = append(ldapSilentErrors, err) continue // TRY NEXT SERVER } return nil, err // Other errors stop immediately } if user != nil { return user, nil // Success! } } // After all servers tried: // - Return ErrInvalidCredentials if ANY server returned it // - Otherwise return ErrCouldNotFindUser for _, ldapErr := range ldapSilentErrors { if errors.Is(ldapErr, ErrInvalidCredentials) { return nil, ErrInvalidCredentials } } return nil, ErrCouldNotFindUser } ``` **Grafana's use case**: Supports **heterogeneous LDAP servers** where: - Different servers may have different user populations - User "alice" might exist only on Server B, not Server A - Common in organizations with multiple AD domains or forests #### Phoenix's Approach (`ldap.py`) Phoenix only fails over on connection errors: ```python for server in self.servers: try: with self._establish_connection(server) as conn: user_entry = self._search_user(conn, escaped_username) if not user_entry: # User not found → STOP immediately (no failover) self._dummy_bind_for_timing(server, password) return None if not self._verify_user_password(server, user_dn, password): # Wrong password → STOP immediately (no failover) return None # Success! return LDAPUserInfo(...) except LDAPException as e: # Connection/protocol error → TRY NEXT SERVER logger.warning(f"LDAP server {server.host} failed: {type(e).__name__}") continue # All servers failed with LDAPException return None ``` **Phoenix's use case**: Assumes **replica servers** where: - All servers are replicas of the same directory - User sets are identical across all servers - "User not found" is a definitive answer, not a reason to try elsewhere #### Design Rationale **Why Phoenix chose NOT to failover on "user not found":** 1. **Semantic correctness**: In replica-based HA, servers have identical data. If user doesn't exist on Server A, they won't exist on Server B. 2. **Avoid masking issues**: Failing over on "user not found" could hide: - Replication lag issues - Misconfigured search bases - AD Global Catalog vs Domain Controller inconsistencies 3. **Security**: Trying all servers on "invalid password" could: - Increase attack surface (more servers to probe) - Leak information about which server has the user 4. **Simplicity**: One server answers definitively; no need to aggregate errors. #### When This Matters | Scenario | Grafana | Phoenix | |----------|---------|---------| | **Replica HA** (identical user sets) | Works (redundant checks) | Works (optimal) | | **Multi-domain/forest** (different user sets) | Works | ❌ Won't find users on non-primary servers | | **AD GC vs DC** (partial vs full attributes) | Works | May fail if GC is primary | #### Known Limitation **Phoenix does not support heterogeneous LDAP servers** (different user populations on different servers). | Architecture | Supported? | |--------------|------------| | Replica HA (identical servers) | ✅ Fully supported | | Single AD domain with multiple DCs | ✅ Fully supported | | Multi-domain AD forest | ❌ Not supported | | Multiple separate LDAP directories | ❌ Not supported | **Why we chose NOT to match Grafana:** 1. **Security by default**: Probing all servers on "user not found" or "wrong password" increases attack surface and enables username enumeration. 2. **Most deployments are replicas**: The common enterprise pattern is HA with identical server sets, not heterogeneous directories. 3. **Don't mask infrastructure issues**: Failing over on "user not found" can hide replication lag, misconfigured search bases, or AD GC/DC inconsistencies that should be addressed at the infrastructure layer. 4. **Simpler, faster**: One server answers definitively; no need to aggregate errors or try all servers on failure. **Future enhancement**: If customer demand requires multi-domain/heterogeneous LDAP support, this could be added as an opt-in configuration flag (e.g., `PHOENIX_LDAP_FAILOVER_MODE=heterogeneous`). --- ### 2. DN Comparison for Group Matching | Aspect | Grafana | Phoenix | Impact | |--------|---------|---------|--------| | Method | `strings.EqualFold()` | RFC 4514 canonicalization | ✅ Phoenix MORE robust | | Handles whitespace | ❌ No | ✅ Yes | Phoenix better | | Handles multi-valued RDN | ❌ No | ✅ Yes | Phoenix better | **Grafana** (`helpers.go`): ```go func IsMemberOf(memberOf []string, group string) bool { if group == "*" { return true } for _, member := range memberOf { if strings.EqualFold(member, group) { // Simple case-insensitive return true } } return false } ``` **Phoenix** (`ldap.py`): ```python def _is_member_of(canonical_user_groups: set[str], target_group: str) -> bool: if target_group == "*": return True target_canonical = canonicalize_dn(target_group) # Full RFC 4514 return target_canonical in canonical_user_groups ``` **Result**: Phoenix correctly matches DNs that differ only in whitespace or attribute ordering. Grafana may fail to match equivalent DNs. --- ### 3. No Group Match Behavior | Aspect | Grafana | Phoenix | Impact | |--------|---------|---------|--------| | Behavior when no groups match | Deny + disable user | Deny access | ✅ Match | | `SkipOrgRoleSync` option | ✅ Yes | ❌ No | ⚠️ Phoenix stricter | **Grafana** (`ldap.go`): ```go // validateGrafanaUser - denies access if groups configured but none match if !server.cfg.SkipOrgRoleSync && len(server.Config.Groups) > 0 && (len(user.OrgRoles) == 0 && ...) { return ErrInvalidCredentials } // SkipOrgRoleSync option allows bypassing group checks entirely if server.cfg.SkipOrgRoleSync { server.log.Debug("Skipping organization role mapping.") return extUser, nil // Allow login without role assignment } ``` **Phoenix** (`ldap.py`): ```python role = self.map_groups_to_role(groups) if not role: return None # Always deny if no matching groups ``` **Rationale**: Phoenix intentionally omits `SkipOrgRoleSync` for security - every LDAP user MUST have an explicit role assignment. --- ### 4. Role Assignment Strategy | Aspect | Grafana | Phoenix | Impact | |--------|---------|---------|--------| | Multi-org support | ✅ Yes (per-org roles) | ❌ No (single role) | ✅ N/A | | First-match-wins | Per org | Global | ✅ Equivalent | | Wildcard `"*"` | ✅ Yes | ✅ Yes | ✅ Match | **Grafana** (`ldap.go`): ```go for _, group := range server.Config.Groups { // Only use first match FOR EACH ORG if extUser.OrgRoles[group.OrgId] != "" { continue } if IsMemberOf(memberOf, group.GroupDN) { extUser.OrgRoles[group.OrgId] = group.OrgRole } } ``` **Phoenix** (`ldap.py`): ```python for mapping in self.config.group_role_mappings: if _is_member_of(canonical_user_groups, group_dn): return _validate_phoenix_role(role) # Return immediately ``` **Result**: Effectively equivalent for single-org use case. Phoenix has no multi-org concept. --- ### Summary Table | Feature | Match | Notes | |---------|-------|-------| | Wildcard `"*"` support | ✅ Match | Both check wildcard first | | Case-insensitive DN | ✅ Match | Phoenix more robust (RFC 4514) | | First-match-wins priority | ✅ Match | Both iterate in config order | | Multi-server failover (connection error) | ✅ Match | Both continue to next server | | Multi-server failover (user not found) | ⚠️ Different | Grafana continues, Phoenix stops | | Multi-server failover (wrong password) | ⚠️ Different | Grafana continues, Phoenix stops | | No group match = deny | ✅ Match | Both deny access | | SkipOrgRoleSync option | ⚠️ Missing | Intentionally omitted in Phoenix | | DN canonicalization | ✅ Phoenix better | RFC 4514 vs simple string compare | ### Multi-Server Architecture Support | Architecture | Grafana | Phoenix | |--------------|---------|---------| | **Replica HA** (identical servers) | ✅ Works | ✅ Works (optimal) | | **Multi-domain** (different user sets) | ✅ Works | ⚠️ Only finds users on first responding server | | **AD Forest** (multiple domains) | ✅ Works | ⚠️ Requires all users in primary domain | --- ## Key Compatibility Findings ### Configuration Format - **Grafana**: TOML-based with `[[servers]]` blocks - **Phoenix**: Environment variables with JSON for role mappings - **Compatibility**: Same logical structure, different serialization format ### User Identification - **Grafana**: Uses DN as primary identifier (stored in `user_auth.auth_id`) - **Phoenix**: Uses email (simple mode) or immutable unique ID like objectGUID (enterprise mode) - **Key difference**: Phoenix doesn't use DN for identity matching (DNs change too frequently) - **Both**: Support email synchronization on login - See [User Identification Strategy](./user-identification-strategy.md) for details ### TLS Security - **Both**: Support STARTTLS and LDAPS - **Critical**: Both require explicit TLS upgrade sequencing (see [Protocol Compliance](./protocol-compliance.md#7-starttls-implementation--security)) - **Finding**: Grafana v11.4 has STARTTLS vulnerability (tested via adversarial MITM proxy) ### DN Handling - **Grafana**: Case-insensitive via `strings.EqualFold()`, used for user identification - **Phoenix**: RFC 4514-compliant canonicalization for group DN matching only - **Key difference**: Phoenix doesn't use DN for user identification (uses email or unique_id instead) ## Additional Grafana Implementation Patterns For detailed analysis of Grafana-specific patterns that informed Phoenix's implementation, see: - [Configuration Reference](./configuration.md) - Phoenix vs. Grafana environment variable mapping - [Security Deep-Dive](./security.md) - LDAP injection prevention (based on Grafana patterns) - [Protocol Compliance](./protocol-compliance.md) - TLS security testing (includes Grafana v11.4 vulnerability findings) - [User Identification Strategy](./user-identification-strategy.md) - Email/Unique ID identification (not DN) **Note**: The main specification document contains extensive Grafana compatibility analysis throughout, including: - Phase 0: Grafana Compatibility Verification (lines 874-908) - Grafana Implementation Details subsections in Appendix A (lines 1531-2416 of original document) - Decision Reversibility Analysis comparing Grafana vs Phoenix decisions (lines 3933-4383)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Arize-ai/phoenix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server