Implement RBAC in app plugins
Role-based access control (RBAC) in Grafana app plugins is essential for creating secure and tailored user experiences. By implementing RBAC, you ensure that sensitive functionalities and data are only accessible to users with appropriate permissions, enhancing both security and usability. Proper configuration is crucial as misconfigurations can lead to security vulnerabilities.
You can find an example app plugin that makes use of RBAC in our grafana-plugin-examples GitHub repository.
Before you beginβ
Ensure your development environment meets the following prerequisites:
- Grafana version: Use Grafana version 11.2.0 or later to access the most up-to-date RBAC features.
- Feature toggle: Activate the
accessControlOnCall
feature toggle to enable RBAC features in Grafana, which are essential for managing access controls within your plugin.
You can ensure the correct feature toggle is enabled by adding the following to your docker-compose.yaml
file:
environment:
- GF_FEATURE_TOGGLES_ENABLE=accessControlOnCall
Defining rolesβ
To establish roles for your plugin, insert a roles
section into your plugin.json
file. For example:
"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.
}
]
In the roles
array, each role object specifies name
and description
for clarity and governance, while permissions
define the exact actions the role can perform, such as read
or write
. The grants
array determines which default user roles, like Admin
or Viewer
, should automatically receive these custom roles.
For example, in the above example, users with the Viewer
role will automatically be granted the Research Papers Reader
role.
When defining roles, ensure each role is clearly differentiated with unique permissions to avoid conflicts and unintended access. It's best to follow the principle of least privilege, assigning the minimum permissions necessary for the tasks.
Secure frontend includesβ
To enforce action-based access control on your frontend pages, incorporate the action
parameter into the include definitions in your plugin.json
file. Here is how you can apply it:
"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
}
]
Secure proxied routesβ
To safeguard your proxied routes with action checks, include the reqAction
parameter in your route definitions within the plugin.json
file. Hereβs an example of how to do this:
"routes": [
{
"path": "api/external/patents",
"method": "*",
"reqAction": "grafana-appwithrbac-app.patents:read",
"url": "{{ .JsonData.backendUrl }}/api/external/patents",
"headers": [
{
"name": "Authorization",
"content": "{{ .SecureJsonData.backendApiToken }}"
}
]
}
]
Secure backend resourcesβ
If your backend exposes resources, you can secure them with action-based checks.
To enable this protection, activate the following features:
externalServiceAccounts
: Allows the use of managed service accounts to access Grafana user permissions.idForwarding
: Required to provide an ID token to identify the requester, whether it's a user or a service account.
These features can be enabled in your Grafana instance by modifying the docker-compose.yaml
file as follows:
environment:
- GF_FEATURE_TOGGLES_ENABLE=accessControlOnCall,idForwarding,externalServiceAccounts
The backend service account and ID forwarding setup allow your plugin's backend to authenticate requests and ascertain the user's identity and permissions reliably. This setup is essential for maintaining secure and controlled access to backend resources.
In your plugin.json
, add the iam
section to get a service account token with the needed permissions:
"iam": {
"permissions": [
{"action": "users.permissions:read", "scope": "users:*"}
]
}
Next, integrate the authlib/authz
library into your plugin's backend code to manage authorization effectively:
import "github.com/grafana/authlib/authz"
To set up the authorization client, start by retrieving the client secret from the plugin context of the incoming request. Since the client secret remains constant, you only need to initialize the authorization client once. This approach utilizes the client cache efficiently.
Use the following function to obtain the authorization client:
// 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
}
The WithSearchByPrefix
option is used to minimize frequent queries to the authorization server by filtering actions based on their prefix.
The WithCache
option enables customization of the library's internal cache, allowing you to specify alternative cache settings. By default, the cache expires after 5 minutes.
Following this setup, you can implement access control using the client. For example:
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
}
Use function within a Resources
endpoint to conduct an access control check and verify the user possesses the necessary permissions to access the specified resource.
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
}
Implement frontend access control checksβ
Implement frontend access checks to prevent unauthorized users from navigating to restricted UI sections, and ensure a consistent and secure user experience that aligns with backend permissions.
To prevent a broken UI, it is crucial to implement these checks by only registering routes and displaying links based on users' permissions. This proactive approach ensures that the user interface reflects the security policies defined by the backend, providing a seamless and secure user experience.
To perform access control checks, import the hasPermission
function from the Grafana runtime package.
import { hasPermission } from '@grafana/runtime';
Then checks can be performed as follow:
if (hasPermission('grafana-appwithrbac-app.papers:read')) {
// Examples: register route, display link, and so on
}
Assigning rolesβ
You can assign roles within Grafana by navigating to the user management section, where you can assign custom roles to users based on their responsibilities. Detailed steps can be found in our comprehensive role management guide.
Assigning roles to specific users requires either Grafana Cloud or a Grafana Enterprise license.
If you have a Grafana Enterprise license, then you can edit the docker-compose.yaml
file as follows:
environment:
- GF_ENTERPRISE_LICENSE_TEXT=<your license>