跳到主要内容

在应用程序插件中实现 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 数组中,每个角色对象都指定了 namedescription 以供清晰性和治理,而 permissions 定义了该角色可以执行的确切操作,例如 readwritegrants 数组确定哪些默认用户角色(如 AdminViewer)应该自动接收这些自定义角色。

例如,在上面的示例中,具有 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 令牌来识别请求者,无论是用户还是服务帐户。
注意

可以通过修改 docker-compose.yaml 文件,在您的 Grafana 实例中启用这些功能,如下所示

environment:
- GF_FEATURE_TOGGLES_ENABLE=accessControlOnCall,idForwarding,externalServiceAccounts

后端服务帐户和 ID 转发设置允许您的插件的后端对请求进行身份验证,并可靠地确定用户的身份和权限。此设置对于维护对后端资源的安全和受控访问至关重要。

在您的 plugin.json 中,添加 iam 部分以获取具有所需权限的服务帐户令牌

"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 企业版许可证

如果您拥有 Grafana 企业版许可证,则可以按如下方式编辑 docker-compose.yaml 文件

environment:
- GF_ENTERPRISE_LICENSE_TEXT=<your license>