在应用程序插件中实现 RBAC
在 Grafana 应用程序插件中,基于角色的访问控制 (RBAC) 对于创建安全且量身定制的用户体验至关重要。通过实现 RBAC,您可以确保只有拥有适当权限的用户才能访问敏感功能和数据,从而增强安全性和可用性。正确的配置至关重要,因为错误配置可能导致安全漏洞。
您可以在我们的 grafana-plugin-examples GitHub 仓库中找到使用 RBAC 的应用程序插件示例。
开始之前
确保您的开发环境满足以下先决条件
- Grafana 版本:使用 Grafana 11.2.0 或更高版本以访问最新的 RBAC 功能。
- 功能开关:激活
accessControlOnCall
功能开关以在 Grafana 中启用 RBAC 功能,这对于管理插件内的访问控制至关重要。
您可以通过将以下内容添加到您的 docker-compose.yaml
文件来确保启用正确的功能开关
environment:
- GF_FEATURE_TOGGLES_ENABLE=accessControlOnCall
定义角色
要为您的插件建立角色,请在您的 plugin.json
文件中插入一个 roles
部分。例如
"roles": [
{
"role": {
"name": "Patents Reader",
"description": "Read patents",
"permissions": [
{"action": "grafana-appwithrbac-app.patents:read"}
]
},
"grants": ["Admin"] // Automatically grants this role to users with the Admin role.
},
{
"role": {
"name": "Research Papers Reader",
"description": "Read research papers",
"permissions": [
{"action": "grafana-appwithrbac-app.papers:read"}
]
},
"grants": ["Viewer"] // Automatically grants this role to users with the Viewer role.
}
]
在 roles
数组中,每个角色对象都指定了 name
和 description
以增强清晰度和治理能力,而 permissions
则定义了该角色可以执行的具体操作,例如 read
或 write
。grants
数组决定了哪些默认用户角色(如 Admin
或 Viewer
)应该自动获得这些自定义角色。
例如,在上面的示例中,拥有 Viewer
角色的用户将自动被授予 Research Papers Reader
角色。
定义角色时,请确保每个角色通过唯一的权限进行明确区分,以避免冲突和意外访问。最好遵循最小权限原则,仅分配执行任务所需的最低权限。
保护前端包含项
要在前端页面上强制执行基于操作的访问控制,请将 action
参数合并到您的 plugin.json
文件中的包含项定义中。以下是应用方法
"includes": [
{
"type": "page",
"name": "Research documents",
"path": "/a/%PLUGIN_ID%/research-docs",
"action": "grafana-appwithrbac-app.papers:read",
"addToNav": true,
"defaultNav": false
// This page will only appear for users with the 'papers:read' permission
},
{
"type": "page",
"name": "Patents",
"path": "/a/%PLUGIN_ID%/patents",
"action": "grafana-appwithrbac-app.patents:read",
"addToNav": true,
"defaultNav": false
// This page will only appear for users with the 'patents:read' permission
}
]
保护代理路由
为了通过操作检查来保护您的代理路由,请在 plugin.json
文件中的路由定义中包含 reqAction
参数。以下是一个示例说明如何操作
"routes": [
{
"path": "api/external/patents",
"method": "*",
"reqAction": "grafana-appwithrbac-app.patents:read",
"url": "{{ .JsonData.backendUrl }}/api/external/patents",
"headers": [
{
"name": "Authorization",
"content": "{{ .SecureJsonData.backendApiToken }}"
}
]
}
]
保护后端资源
如果您的后端暴露资源,您可以通过基于操作的检查来保护它们。
要启用此保护,请激活以下功能
externalServiceAccounts
: 允许使用托管服务账号访问 Grafana 用户权限。idForwarding
: 需要提供一个 ID token 来识别请求者,无论是用户还是服务账号。
通过如下修改 docker-compose.yaml
文件,可以在您的 Grafana 实例中启用这些功能
environment:
- GF_FEATURE_TOGGLES_ENABLE=accessControlOnCall,idForwarding,externalServiceAccounts
后端服务账号和 ID 转发设置允许您的插件后端可靠地验证请求并确定用户的身份和权限。此设置对于维护对后端资源的安全和受控访问至关重要。
在您的 plugin.json
文件中,添加 iam
部分以获取具有所需权限的服务账号 token
"iam": {
"permissions": [
{"action": "users.permissions:read", "scope": "users:*"}
]
}
接下来,将 authlib/authz
库集成到您的插件后端代码中,以有效管理授权
import "github.com/grafana/authlib/authz"
要设置授权客户端,首先从传入请求的插件上下文中检索客户端密钥。由于客户端密钥保持不变,您只需初始化授权客户端一次。这种方法能有效地利用客户端缓存。
使用以下函数获取授权客户端
// GetAuthZClient returns an authz enforcement client configured thanks to the plugin context.
func (a *App) GetAuthZClient(req *http.Request) (authz.EnforcementClient, error) {
ctx := req.Context()
ctxLogger := log.DefaultLogger.FromContext(ctx)
cfg := backend.GrafanaConfigFromContext(ctx)
saToken, err := cfg.PluginAppClientSecret()
if err != nil || saToken == "" {
if err == nil {
err = errors.New("service account token not found")
}
ctxLogger.Error("Service account token not found", "error", err)
return nil, err
}
// Prevent two concurrent calls from updating the client
a.mx.Lock()
defer a.mx.Unlock()
if saToken == a.saToken {
ctxLogger.Debug("Token unchanged returning existing client")
return a.authzClient, nil
}
grafanaURL, err := cfg.AppURL()
if err != nil {
ctxLogger.Error("App URL not found", "error", err)
return nil, err
}
// Initialize the authorization client
client, err := authz.NewEnforcementClient(authz.Config{
APIURL: grafanaURL,
Token: saToken,
// Grafana is signing the JWTs on local setups
JWKsURL: strings.TrimRight(grafanaURL, "/") + "/api/signing-keys/keys",
},
// Fetch all user permissions prefixed with grafana-appwithrbac-app
authz.WithSearchByPrefix("grafana-appwithrbac-app"),
// Use a cache with a lower expiry time
authz.WithCache(cache.NewLocalCache(cache.Config{
Expiry: 10 * time.Second,
CleanupInterval: 5 * time.Second,
})),
)
if err != nil {
ctxLogger.Error("Initializing authz client", "error", err)
return nil, err
}
a.saToken = saToken
a.authzClient = client
return client, nil
}
WithSearchByPrefix
选项用于通过根据操作前缀进行过滤来最大限度地减少对授权服务器的频繁查询。
WithCache
选项允许自定义库的内部缓存,使您能够指定替代缓存设置。默认情况下,缓存会在 5 分钟后过期。
完成此设置后,您可以使用客户端实现访问控制。例如
func (a *App) HasAccess(req *http.Request, action string) (bool, error) {
// Retrieve the ID token
idToken := req.Header.Get("X-Grafana-Id")
if idToken == "" {
return false, errors.New("id token not found")
}
authzClient, err := a.GetAuthZClient(req)
if err != nil {
return false, err
}
// Check user access
hasAccess, err := authzClient.HasAccess(req.Context(), idToken, action)
if err != nil || !hasAccess {
return false, err
}
return true, nil
}
在 Resources
端点中使用函数进行访问控制检查,并验证用户是否拥有访问指定资源的必要权限。
if hasAccess, err := a.HasAccess(req, "grafana-appwithrbac-app.patents:read"); err != nil || !hasAccess {
if err != nil {
log.DefaultLogger.FromContext(req.Context()).Error("Error checking access", "error", err)
}
http.Error(w, "permission denied", http.StatusForbidden)
return
}
实现前端访问控制检查
实现前端访问检查以防止未经授权的用户导航到受限的 UI 部分,并确保与后端权限一致的一致且安全的用户体验。
为防止出现损坏的 UI,通过仅基于用户权限注册路由和显示链接来实施这些检查至关重要。这种积极主动的方法可确保用户界面反映后端定义的安全策略,从而提供无缝且安全的用户体验。
要执行访问控制检查,请从 Grafana 运行时包导入 hasPermission
函数。
import { hasPermission } from '@grafana/runtime';
然后可以按如下方式执行检查
if (hasPermission('grafana-appwithrbac-app.papers:read')) {
// Examples: register route, display link, and so on
}
分配角色
您可以通过导航到用户管理部分在 Grafana 中分配角色,您可以在其中根据用户的职责为用户分配自定义角色。详细步骤可在我们的综合角色管理指南中找到。
为特定用户分配角色需要 Grafana Cloud 或 Grafana Enterprise 许可证。
如果您拥有 Grafana Enterprise 许可证,则可以按如下方式编辑 docker-compose.yaml
文件
environment:
- GF_ENTERPRISE_LICENSE_TEXT=<your license>