Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Mariell Hoversholm committed 2024-11-15 14:54:28 +01:00
commit 72db1be1d4
261 files changed
+5776 -4627

No files matched your search

+3 -25
View File
@@ -5536,35 +5536,16 @@ exports[`better eslint`] = {
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/panel/xychart/ManualEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/xychart/XYChartPanel.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/xychart/scatter.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"]
],
"public/app/plugins/panel/xychart/v2/SeriesEditor.tsx:5381": [
"public/app/plugins/panel/xychart/SeriesEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"]
],
"public/app/plugins/panel/xychart/v2/migrations.ts:5381": [
"public/app/plugins/panel/xychart/migrations.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/xychart/v2/scatter.ts:5381": [
"public/app/plugins/panel/xychart/scatter.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
@@ -5720,9 +5701,6 @@ exports[`no undocumented stories`] = {
"packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.story.tsx:5381": [
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
],
"packages/grafana-ui/src/components/Combobox/Combobox.story.tsx:5381": [
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
],
"packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.story.tsx:5381": [
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
],
+10 -9
View File
@@ -40,7 +40,7 @@
/docs/sources/alerting/ @brendamuir
/docs/sources/dashboards/ @imatwawana
/docs/sources/explore/ @grafana/explore-squad @lwandz13
/docs/sources/explore/ @grafana/observability-traces-and-profiling @lwandz13
/docs/sources/panels-visualizations/ @imatwawana
/docs/sources/release-notes/ @irenerl24 @GrafanaWriter
/docs/sources/upgrade-guide/ @imatwawana
@@ -123,7 +123,7 @@
/pkg/services/apikey/ @grafana/identity-squad
/pkg/services/cleanup/ @grafana/grafana-backend-group
/pkg/services/contexthandler/ @grafana/grafana-backend-group @grafana/grafana-app-platform-squad
/pkg/services/correlations/ @grafana/explore-squad
/pkg/services/correlations/ @grafana/dataviz-squad
/pkg/services/dashboardimport/ @grafana/grafana-backend-group
/pkg/services/dashboards/ @grafana/grafana-app-platform-squad
/pkg/services/dashboardversion/ @grafana/grafana-backend-group
@@ -141,7 +141,7 @@
/pkg/services/provisioning/ @grafana/grafana-search-and-storage
/pkg/services/provisioning/alerting/ @grafana/alerting-backend
/pkg/services/query/ @grafana/grafana-app-platform-squad
/pkg/services/queryhistory/ @grafana/explore-squad
/pkg/services/queryhistory/ @grafana/observability-traces-and-profiling
/pkg/services/quota/ @grafana/grafana-search-and-storage
/pkg/services/screenshot/ @grafana/grafana-backend-group
/pkg/services/search/ @grafana/grafana-search-and-storage
@@ -163,7 +163,7 @@
/pkg/tests/apis/ @grafana/grafana-app-platform-squad
/pkg/tests/apis/query @grafana/grafana-datasources-core-services
/pkg/tests/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend
/pkg/tests/api/correlations/ @grafana/explore-squad
/pkg/tests/api/correlations/ @grafana/dataviz-squad
/pkg/tsdb/grafanads/ @grafana/grafana-backend-group
/pkg/tsdb/opentsdb/ @grafana/partner-datasources
/pkg/util/ @grafana/grafana-backend-group
@@ -418,7 +418,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/core/components/OptionsUI/ @grafana/dashboards-squad @grafana/dataviz-squad
/public/app/core/history/ @grafana/explore-squad
/public/app/core/history/ @grafana/observability-traces-and-profiling
/public/app/features/admin/ @grafana/identity-access-team
# Temp owners until Enterprise team takes over
@@ -433,7 +433,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/visualization/data-hover/ @grafana/dataviz-squad
/public/app/features/commandPalette/ @grafana/grafana-frontend-platform
/public/app/features/connections/ @grafana/plugins-platform-frontend
/public/app/features/correlations/ @grafana/explore-squad
/public/app/features/correlations/ @grafana/dataviz-squad
/public/app/features/dashboard/ @grafana/dashboards-squad
/public/app/features/dashboard/components/TransformationsEditor/ @grafana/dataviz-squad
/public/app/features/dashboard-scene/ @grafana/dashboards-squad
@@ -441,7 +441,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/datasources/ @grafana/plugins-platform-frontend
/public/app/features/dimensions/ @grafana/dataviz-squad
/public/app/features/dataframe-import/ @grafana/dataviz-squad
/public/app/features/explore/ @grafana/explore-squad
/public/app/features/explore/ @grafana/observability-traces-and-profiling
/public/app/features/expressions/ @grafana/observability-metrics
/public/app/features/folders/ @grafana/grafana-frontend-platform
/public/app/features/inspector/ @grafana/dashboards-squad
@@ -457,7 +457,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/playlist/ @grafana/dashboards-squad
/public/app/features/plugins/ @grafana/plugins-platform-frontend
/public/app/features/profile/ @grafana/grafana-frontend-platform
/public/app/features/query-library/ @grafana/explore-squad
/public/app/features/query-library/ @grafana/grafana-frontend-platform
/public/app/features/runtime/ @ryantxu
/public/app/features/query/ @grafana/dashboards-squad
/public/app/features/sandbox/ @grafana/grafana-frontend-platform
@@ -473,6 +473,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/users/ @grafana/access-squad
/public/app/features/variables/ @grafana/dashboards-squad
/public/app/features/preferences/ @grafana/grafana-frontend-platform
/public/app/features/bookmarks/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/alertlist/ @grafana/alerting-frontend
/public/app/plugins/panel/annolist/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/barchart/ @grafana/dataviz-squad
@@ -630,7 +631,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/pkg/services/rendering/ @grafana/sharing-squad
# SSE - Server Side Expressions
/pkg/expr/ @grafana/observability-metrics
/pkg/expr/ @grafana/grafana-datasources-core-services
# Cloud middleware
/grafana-mixin/ @grafana/grafana-backend-services-squad
+7 -1
View File
@@ -626,6 +626,11 @@ id_response_header_namespaces = user api-key service-account
# This feature currently **only supports single-organization deployments**
managed_service_accounts_enabled = false
#################################### Passwordless Auth ###########################
[auth.passwordless]
enabled = false
code_expiration = 20m
#################################### SSO Settings ###########################
[sso_settings]
# interval for reloading the SSO Settings from the database
@@ -1756,7 +1761,8 @@ hide_angular_deprecation =
# Comma separated list of plugin ids for which environment variables should be forwarded. Used only when feature flag pluginsSkipHostEnvVars is enabled.
forward_host_env_vars =
# Comma separated list of plugin ids to install as part of the startup process.
preinstall = grafana-lokiexplore-app
# By default, the following plugins will be preinstalled: "grafana-lokiexplore-app"
preinstall =
# Controls whether preinstall plugins asynchronously (in the background) or synchronously (blocking). Useful when preinstalled plugins are used with provisioning.
preinstall_async = true
# Disables preinstall feature. It has the same effect as setting preinstall to an empty list.
@@ -103,53 +103,45 @@ To browse for available plugins:
### Install a plugin
To install a plugin:
The most common way to install a plugin is through the Grafana UI, but alternative methods are also available.
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view all plugins.
1. Browse and find a plugin.
1. Click the plugin's logo.
1. Click **Install**.
When the update is complete, you'll see a confirmation message that the installation was successful.
There are also additional ways to install plugins depending on your setup.
### Update a plugin
#### Install a plugin using Grafana CLI
To update a plugin:
Grafana CLI allows you to install, upgrade, and manage your Grafana plugins using a command line. For more information about Grafana CLI plugin commands, refer to [Plugin commands]({{< relref "../../cli/#plugins-commands" >}}).
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view all plugins.
1. Click the **Installed** filter to show only installed plugins.
1. Click the plugin's logo.
1. Click **Update**.
#### Install a plugin from a ZIP file
When the update is complete, you'll see a confirmation message that the update was successful.
This method is typically used for plugins not available in the Plugin Catalog or in environments without internet access.
### Uninstall a plugin
Download the archive containing the plugin assets, and install it by extracting the archive into the plugin directory. For example:
To uninstall a plugin:
```bash
unzip my-plugin-0.2.0.zip -d YOUR_PLUGIN_DIR/my-plugin
```
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view all plugins.
1. Click the plugin's logo.
1. Click the **Installed** filter to show only installed plugins.
1. Click **Uninstall**.
The path to the plugin directory is defined in the configuration file. For more information, refer to [Configuration]({{< relref "../../setup-grafana/configure-grafana/#plugins" >}}).
When the update is complete, you'll see a confirmation message that the uninstall was successful.
#### Install a plugin in air-gapped environment
## Install Grafana plugins
Plugin installation usually requires an internet connection. You can check which endpoints are used during the installation on your instance and add them to your instance’s allowlist.
Grafana supports data source, panel, and app plugins.
If this is not possible you can go via installing a plugin using [Grafana CLI](#install-a-plugin-using-grafana-cli) or as a [ZIP file](#install-a-plugin-from-a-zip-file).
1. In a web browser, navigate to the [Grafana plugin catalog](https://grafana.com/plugins) and find a plugin that you want to install.
1. Click the plugin, and then click the **Installation** tab.
You can fetch any plugin from Grafana.com API following the download link referenced in the API.
Here is an example based on `grafana-lokiexplore-app` plugins.
### Install plugin on Grafana Cloud
1. Open `https://grafana.com/api/plugins/grafana-lokiexplore-app` and look for `links` section
1. Find a `download` url which looks something like `https://grafana.com/api/plugins/grafana-lokiexplore-app/versions/1.0.2/download`
1. Use this URL to download the plugin ZIP file, which you can then install as described above.
On the **Installation tab**, in the **For** field, click the name of the Grafana instance on which you want to install the plugin.
Grafana Cloud handles the plugin installation automatically.
If you're logged in to Grafana Cloud when you add a plugin, log out and then log back in again to use the new plugin.
### Install plugins using the Grafana Helm chart
#### Install plugins using the Grafana Helm chart
With the Grafana Helm chart, add the plugins you want to install as a list using the `plugins` field in the your values file. For more information about the configuration, refer to [the Helm chart configuration reference](https://github.com/grafana/helm-charts/tree/main/charts/grafana#configuration).
@@ -162,21 +154,29 @@ plugins:
- redis-datasource
```
### Install plugin on local Grafana
When the update is complete, a confirmation message will indicate the installation was successful.
Follow the instructions on the **Install** tab. You can either install the plugin with a Grafana CLI command or by downloading and uncompressing a zip file into the Grafana plugins directory. We recommend using Grafana CLI in most instances. The zip option is available if your Grafana server doesn't have access to the internet.
### Update a plugin
For more information about Grafana CLI plugin commands, refer to [Plugin commands]({{< relref "../../cli/#plugins-commands" >}}).
To update a plugin:
#### Install a packaged plugin
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view all plugins.
1. Click the **Installed** filter to show only installed plugins.
1. Click the plugin's logo.
1. Click **Update**.
After the user has downloaded the archive containing the plugin assets, they can install it by extracting the archive into their plugin directory. For example:
When the update is complete, a confirmation message will indicate the installation was successful.
```bash
unzip my-plugin-0.2.0.zip -d YOUR_PLUGIN_DIR/my-plugin
```
### Uninstall a plugin
The path to the plugin directory is defined in the configuration file. For more information, refer to [Configuration]({{< relref "../../setup-grafana/configure-grafana/#plugins" >}}).
To uninstall a plugin:
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view all plugins.
1. Click the plugin's logo.
1. Click the **Installed** filter to show only installed plugins.
1. Click **Uninstall**.
When the update is complete, a confirmation message will indicate the installation was successful.
## Plugin signatures
@@ -253,17 +253,17 @@ All of these settings are optional.
The _HTTP\*_ tag denotes data sources that communicate using the HTTP protocol, including all core data source plugins except MySQL, PostgreSQL, and MS SQL.
{{< /admonition >}}
| Name | Type | Data source | Description |
| ----------------- | ------ | ---------------------------------- | -------------------------------------------------------- |
| tlsCACert | string | _HTTP\*_, MySQL, PostgreSQL | CA cert for out going requests |
| tlsClientCert | string | _HTTP\*_, MySQL, PostgreSQL | TLS Client cert for outgoing requests |
| tlsClientKey | string | _HTTP\*_, MySQL, PostgreSQL | TLS Client key for outgoing requests |
| password | string | _HTTP\*_, MySQL, PostgreSQL, MSSQL | password |
| basicAuthPassword | string | _HTTP\*_ | password for basic authentication |
| accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch |
| secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch |
| sigV4AccessKey | string | Elasticsearch and Prometheus | SigV4 access key. Required when using keys auth provider |
| sigV4SecretKey | string | Elasticsearch and Prometheus | SigV4 secret key. Required when using keys auth provider |
| Name | Type | Data source | Description |
| ----------------- | ------ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| tlsCACert | string | _HTTP\*_, MySQL, PostgreSQL | CA cert for out going requests. You can point directly to your stored cert by using an environment variable following the `$__file{path/to/ca}` format. |
| tlsClientCert | string | _HTTP\*_, MySQL, PostgreSQL | TLS Client cert for outgoing requests. You can point directly to your stored cert by using an environment variable following the `$__file{path/to/cert}` format. |
| tlsClientKey | string | _HTTP\*_, MySQL, PostgreSQL | TLS Client key for outgoing requests. You can point directly to your stored key by using an environment variable following the `$__file{path/to/key}` format. |
| password | string | _HTTP\*_, MySQL, PostgreSQL, MSSQL | password |
| basicAuthPassword | string | _HTTP\*_ | password for basic authentication |
| accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch |
| secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch |
| sigV4AccessKey | string | Elasticsearch and Prometheus | SigV4 access key. Required when using keys auth provider |
| sigV4SecretKey | string | Elasticsearch and Prometheus | SigV4 secret key. Required when using keys auth provider |
#### Custom HTTP headers for data sources
@@ -100,6 +100,8 @@ The following list contains role-based access control actions.
| `folders:delete` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Delete one or more folders and their subfolders. |
| `folders:read` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Read one or more folders and their subfolders. |
| `folders:write` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> | Update one or more folders and their subfolders. |
| `groupsync.mappings:read` | None | List group attribute sync mappings. To use this permission, enable the `groupAttributeSync` feature toggle. |
| `groupsync.mappings:write` | None | List, create, update, and delete group attribute sync mappings. To use this permission, enable the `groupAttributeSync` feature toggle. |
| `ldap.config:reload` | None | Reload the LDAP configuration. |
| `ldap.status:read` | None | Verify the availability of the LDAP server or servers. |
| `ldap.user:read` | None | Read users via LDAP. |
@@ -54,13 +54,13 @@ The following tables list permissions associated with basic and fixed roles.
## Basic role assignments
| Basic role | UID | Associated fixed roles | Description |
| ------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Grafana Admin | `basic_grafana_admin` | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:plugins:maintainer`<br>`fixed:authentication.config:writer`<br>`fixed:library.panels:creator`<br>`fixed:library.panels:reader`<br>`fixed:library.panels:general.reader`<br>`fixed:library.panels:writer`<br>`fixed:library.panels:general.writer` | Default [Grafana server administrator](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators) assignments. |
| Admin | `basic_admin` | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:dashboards.public:writer`<br>`fixed:folders:reader`<br>`fixed:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:writer`<br>`fixed:apikeys:reader`<br>`fixed:apikeys:writer`<br>`fixed:alerting.provisioning.secrets:reader`<br>`fixed:alerting.provisioning:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:plugins:writer`<br>`fixed:library.panels:creator`<br>`fixed:library.panels:reader`<br>`fixed:library.panels:general.reader`<br>`fixed:library.panels:writer`<br>`fixed:library.panels:general.writer`<br>`fixed:alerting.provisioning.status:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. |
| Editor | `basic_editor` | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled<br>`fixed:alerting:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:library.panels:creator`<br>`fixed:library.panels:general.reader`<br>`fixed:library.panels:general.writer`<br>`fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. |
| Viewer | `basic_viewer` | `fixed:datasources.id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader`<br>`fixed:plugins.app:reader`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:library.panels:general.reader`<br>`fixed:datasources:explorer` if the `viewers_can_edit` configuration flag is enabled | Default [Viewer](ref:rbac-basic-roles) assignments. |
| No Basic Role | n/a | | Default [No Basic Role](ref:rbac-basic-roles) |
| Basic role | UID | Associated fixed roles | Description |
| ------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Grafana Admin | `basic_grafana_admin` | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:plugins:maintainer`<br>`fixed:authentication.config:writer`<br>`fixed:library.panels:creator`<br>`fixed:library.panels:reader`<br>`fixed:library.panels:general.reader`<br>`fixed:library.panels:writer`<br>`fixed:library.panels:general.writer`<br>`fixed:groupsync:writer` | Default [Grafana server administrator](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators) assignments. |
| Admin | `basic_admin` | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:dashboards.public:writer`<br>`fixed:folders:reader`<br>`fixed:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:writer`<br>`fixed:apikeys:reader`<br>`fixed:apikeys:writer`<br>`fixed:alerting.provisioning.secrets:reader`<br>`fixed:alerting.provisioning:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:plugins:writer`<br>`fixed:library.panels:creator`<br>`fixed:library.panels:reader`<br>`fixed:library.panels:general.reader`<br>`fixed:library.panels:writer`<br>`fixed:library.panels:general.writer`<br>`fixed:alerting.provisioning.status:writer`<br>`fixed:groupsync:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. |
| Editor | `basic_editor` | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled<br>`fixed:alerting:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:library.panels:creator`<br>`fixed:library.panels:general.reader`<br>`fixed:library.panels:general.writer`<br>`fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. |
| Viewer | `basic_viewer` | `fixed:datasources.id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader`<br>`fixed:plugins.app:reader`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:library.panels:general.reader`<br>`fixed:datasources:explorer` if the `viewers_can_edit` configuration flag is enabled | Default [Viewer](ref:rbac-basic-roles) assignments. |
| No Basic Role | n/a | | Default [No Basic Role](ref:rbac-basic-roles) |
## Fixed role definitions
@@ -115,6 +115,8 @@ To learn how to use the roles API to determine the role UUIDs, refer to [Manage
| `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. |
| `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and <br>`folders.permissions:write` | Read and update all folder permissions. |
| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`<br>`ldap.status:read` | Read the LDAP configuration and LDAP status information. |
| `fixed:groupsync:reader` | `fixed_tLIbDrE6kw93sKqooF8GVS9BF4E` | `groupsync.mappings:read` | List all group attribute sync mappings. To use this role, enable the `groupAttributeSync` feature toggle. |
| `fixed:groupsync:writer` | `fixed_q7XUYx_efzxxsVmWhQgpiYClwBs` | `groupsync.mappings:read`<br>`groupsync.mappings:write` | Create, read, update, and delete all group attribute sync mappings. To use this role, enable the `groupAttributeSync` feature toggle. |
| `fixed:ldap:writer` | `fixed_p6AvnU4GCQyIh7-hbwI-bk3GYnU` | All permissions from `fixed:ldap:reader` and <br>`ldap.user:sync`<br>`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. |
| `fixed:library.panels:creator` | `fixed_6eX6ItfegCIY5zLmPqTDW8ZV7KY` | `library.panels:create`<br>`folders:read` | Create library panel at the root level. |
| `fixed:library.panels:general.reader` | `fixed_ct0DghiBWR_2BiQm3EvNPDVmpio` | `library.panels:read` | Read all library panels at the root level. |
@@ -85,6 +85,9 @@ Multiple alert instances can be created as a result of one alert rule (also know
{{% admonition type="note" %}}
For Grafana Cloud Free Forever, you can create up to 100 free Grafana-managed alert rules with each alert rule having a maximum of 1000 alert instances.
For all paid tiers (Cloud Pro and Advanced), there is a soft limit of 2000 alert rules and unlimited alert instances. To increase the limit, open a support ticket from the [Cloud portal](https://grafana.com/docs/grafana-cloud/account-management/support/).
{{% /admonition %}}
Grafana-managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules.
@@ -24,7 +24,7 @@ A _playlist_ is a list of dashboards that are displayed in a sequence. You might
Grafana automatically scales dashboards to any resolution, which makes them perfect for big screens.
You can access the Playlist feature from Grafana's side menu, in the Dashboards submenu.
You can access the **Playlist** feature from Grafana's side menu, in the Dashboards submenu.
{{< admonition type="note" >}}
You must have at least Editor role permissions to create and manage playlists.
@@ -41,12 +41,12 @@ Use the information in this section to access playlists. Start and control the d
### Start a playlist
You can start a playlist in six different view modes. View modes determine how the menus and navigation bar appear on the dashboards as well as how panels are sized.
You can start a playlist in four different view modes. View modes determine how the menus and navigation bar appear on the dashboards as well as how panels are sized.
1. Click **Dashboards** in the main menu.
1. Click **Playlists**.
1. Find the desired playlist and click **Start playlist**.
1. In the dialog box that opens, select one of the [six playlist modes](#playlist-modes) available.
1. In the dialog box that opens, select one of the [four playlist modes](#playlist-modes) available.
1. Disable any dashboard controls that you don't want displayed while the list plays; these controls are enabled and visible by default. Select from:
- **Time and refresh**
@@ -55,31 +55,28 @@ You can start a playlist in six different view modes. View modes determine how t
1. Click **Start \<playlist name\>**.
The playlist displays each dashboard for the time specified in the **Interval** field, set when creating or editing a playlist. After a playlist starts, you can [control](#control-a-playlist) it using the navbar at the top of your screen.
The playlist displays each dashboard for the time specified in the **Interval** field, set when creating or editing a playlist. After a playlist starts, you can [start or stop it](#control-a-playlist) it using the controls at the top of your screen.
### Playlist modes
| Mode | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Normal mode | <ul><li>The side menu remains visible.</li></ul><ul><li>The navbar and dashboard controls appear at the top of the screen.</li></ul> |
| Normal mode (with auto fit panels) | <ul><li>The side menu remains visible.</li></ul><ul><li>The navbar and dashboard controls appear at the top of the screen.</li></ul><ul><li>Dashboard panels automatically adjust to optimize space on screen.</li></ul> |
| TV mode | <ul><li>The side menu is hidden or removed.</li></ul><ul><li>The navbar and dashboard controls appear at the top of the screen.</li></ul><ul><li>Enabled automatically after one minute of user inactivity.</li></ul><ul><li>Enable it manually using the `d v` sequence shortcut, or by appending the parameter `?inactive` to the dashboard URL.</li></ul><ul><li>Disable it with any mouse movement or keyboard action.</li></ul> |
| TV mode (with auto fit panels) | <ul><li>The side menu is hidden or removed.</li></ul><ul><li>The navbar and dashboard controls appear at the top of the screen.</li></ul><ul><li>Dashboard panels automatically adjust to optimize space on screen.</li></ul><ul> |
| Kiosk mode | <ul><li>The side menu is hidden or removed.</li></ul><ul><li>The navbar and dashboard controls appear at the top of the screen.</li></ul><ul><li>You can disable or enable it manually using the `d v` sequence shortcut after the playlist has started.</li></ul> |
| Kiosk mode (with auto fit panels) | <ul><li>The side menu is hidden or removed.</li></ul><ul><li>The navbar and dashboard controls appear at the top of the screen.</li></ul><ul><li>You can disable or enable it manually using the `d v` sequence shortcut after the playlist has started.</li></ul><ul><li>Dashboard panels automatically adjust to optimize space on screen.</li></ul> |
<!-- prettier-ignore-start -->
### Control a playlist
| Mode | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------ |
| Normal mode | <ul><li>The main menu and navbar remain visible.</li><li>Dashboard controls are hidden.</li><li>Playlist controls are displayed at the top of the screen.<li><ul> |
| Normal mode (with auto fit panels) | <ul><li>The main menu and navbar remain visible.</li><li>Dashboard controls are hidden.</li><li>Playlist controls are displayed at the top of the screen.</li><li>Dashboard panels automatically adjust to optimize space on screen.</li></ul> |
| Kiosk mode | <ul><li>The main menu, navbar, and dashboard controls are hidden.</li><li>You can disable the playlist manually by pressing the `Esc` key after the playlist has started. Doing so causes the playlist controls to be displayed at the top of the screen briefly.</li></ul> |
| Kiosk mode (with auto fit panels) | <ul><<li>The main menu, navbar, and dashboard controls are hidden.</li><li>You can disable the playlist manually by pressing the `Esc` key after the playlist has started. Doing so causes the playlist controls to be displayed at the top of the screen briefly.</li><li>Dashboard panels automatically adjust to optimize space on screen.</li></ul> |
You can control a playlist in **Normal** or **TV** mode after it's started, using the navigation bar at the top of your screen. Press the Esc key in your keyboard to stop the playlist.
<!-- prettier-ignore-end -->
| Button | Result |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Next (double-right arrow) | Advances to the next dashboard. |
| Back (left arrow) | Returns to the previous dashboard. |
| Stop (square) | Ends the playlist, and exits to the current dashboard. |
| Cycle view mode (monitor icon) | Rotates the display of the dashboards in different view modes. |
| Time range | Displays data within a time range. It can be set to display the last 5 minutes up to 5 years ago, or a custom time range, using the down arrow. |
| Refresh (circle arrow) | Reloads the dashboard, to display the current data. It can be set to reload automatically every 5 seconds to 1 day, using the drop-down arrow. |
### Playlist controls
You can control a playlist in **Normal** mode after it's started, using the buttons at the top of your screen. Press the `Esc` key to stop the playlist.
- **Next (double-right arrow)** - Advances to the next dashboard.
- **Back (doublt-left arrow)** - Returns to the previous dashboard.
- **Stop playlist** - Ends the playlist, and exits to the current dashboard.
## Create a playlist
@@ -328,6 +328,8 @@ font_regular = DejaVuSansCondensed.ttf
font_bold = DejaVuSansCondensed-Bold.ttf
# Name of the TrueType font file with italic style
font_italic = DejaVuSansCondensed-Oblique.ttf
# Allowed domains to receive reports. Use * to allow all domains. Use a comma-separated list to allow multiple domains. Example: allowed_domains = grafana.com, example.org
allowed_domains = *
```
## Report settings
@@ -338,11 +340,19 @@ You can configure organization-wide report settings in the **Settings** under **
You can customize the branding options.
Report branding:
### Attachment settings
#### PDF
- **Company logo:** Company logo displayed in the report PDF. It can be configured by specifying a URL, or by uploading a file. The maximum file size is 16 MB. Defaults to the Grafana logo.
Email branding:
- **Theme:** Theme of the PDF attached to the report. Defaults to **Light**. The selected theme is also applied to the PDFs generated when you click **Preview PDF** during report creation or select the **Export as PDF** option on a dashboard. If **Current** is selected, the PDF in the report will be in the Admin's instance theme, but the preview and exported PDFs will be in the user's instance theme.
#### Embedded Image
- **Theme:** Theme of the dashboard image embedded in the email. Defaults to **Dark**.
### Email branding
- **Company logo:** Company logo displayed in the report email. It can be configured by specifying a URL, or by uploading a file. The maximum file size is 16 MB. Defaults to the Grafana logo.
- **Email footer:** Toggle to enable the report email footer. Select **Sent by** or **None**.
@@ -77,6 +77,8 @@ This feature was previously called **email sharing**.
Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud).
{{< /admonition >}}
{{< docs/private-preview product="Sharing externally with specific people" >}}
To share a dashboard with specific external users, you can send them a link by email. Use this option when you only want to share your dashboard with specific people. When you share dashboards by email, recipients receive a one-time use link that's valid for **one hour**. Once the link is used, the viewer has access to the shared dashboard for **30 days**.
When you share a dashboard with an email link, your organization is billed per user, regardless of how many dashboards are shared. Grafana bills monthly per user until access is revoked.
@@ -76,6 +76,11 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/#add-ad-hoc-filters
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/add-template-variables/#add-ad-hoc-filters
shared-dashboards:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/share-dashboards-panels/shared-dashboards/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/share-dashboards-panels/shared-dashboards/
---
# Use dashboards
@@ -90,19 +95,18 @@ The dashboard user interface provides a number of features that you can use to c
The following image and descriptions highlight all dashboard features.
![An annotated image of a dashboard](/media/docs/grafana/dashboards/screenshot-dashboard-annotated-v11.3.png)
![An annotated image of a dashboard](/media/docs/grafana/dashboards/screenshot-dashboard-annotated-v11.3-2.png)
1. **Grafana home** - Click **Home** in the breadcrumb to go to the home page configured in the Grafana instance.
1. **Dashboard folder** - When you click the dashboard folder name, you can search for other dashboards contained in the folder and perform other [folder management tasks](ref:dashboard-folders).
1. **Dashboard title** - You can create your own dashboard titles or have Grafana create them for you using [generative AI features](ref:generative-ai-features).
1. **Kiosk mode** - Click to display the dashboard on a large screen such as a TV or a kiosk. Kiosk mode hides the main menu, navbar, and dashboard controls. Learn more about kiosk mode in our [How to Create Kiosks to Display Dashboards on a TV blog post](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/). Press `Esc` to leave kiosk mode.
1. **Mark as favorite** - Mark the dashboard as one of your favorites so it's included in your list of **Starred** dashboards in the main menu.
1. **Public label** - When you share a dashboard externally, it's marked with the **Public** label.
1. **Public label** - When you [share a dashboard externally](ref:shared-dashboards), it's marked with the **Public** label.
1. **Dashboard insights** - Click to view analytics about your dashboard including information about users, activity, query counts. Learn more about [dashboard analytics](ref:dashboard-analytics).
1. **Edit** - Click to leave view-only mode and enter edit mode, where you can make changes directly to the dashboard and access dashboard settings, as well as several panel editing functions.
1. **Export** - Access [dashboard exporting](ref:export-dashboards) options.
1. **Share dashboard** - Access several [dashboard sharing](ref:sharing) options.
1. **Kiosk mode** - Click to display the dashboard on a large screen such as a TV or a kiosk. Kiosk mode hides elements such as navigation menus. Learn more about kiosk mode in our [How to Create Kiosks to Display Dashboards on a TV blog post](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/). Press `Enter` to leave kiosk mode.
1. **Variables** - Use [variables](ref:variables) to create more interactive and dynamic dashboards.
1. **Variables** - Use [variables](ref:variables), including ad hoc filters, to create more interactive and dynamic dashboards.
1. **Dashboard links** - Link to other dashboards, panels, and external websites. Learn more about [dashboard links](ref:dashboard-links).
1. **Current dashboard time range and time picker** - Click to select [relative time range](#relative-time-range) options and set custom [absolute time ranges](#absolute-time-range).
- You can change the **Timezone** and **Fiscal year** settings from the time range controls by clicking the **Change time settings** button.
@@ -60,7 +60,7 @@ We encourage you to locate the repository of the corresponding plugin and create
### Links
- [Migrate Angular to React](https://grafana.com/developers/plugin-tools/migration-guides/migrate-angularjs-to-react)
- [Migrate Angular to React](https://grafana.com/developers/plugin-tools/migration-guides/angular-react/)
- [Build a panel plugin](https://grafana.com/tutorials/build-a-panel-plugin/)
- [Build a data source plugin](https://grafana.com/tutorials/build-a-data-source-plugin/)
- [List of current Angular plugins]({{< relref "./angular-plugins/" >}})
@@ -445,10 +445,13 @@ Content-Type: application/json
Authorization: Basic YWRtaW46YWRtaW4=
{
"name": "grafana"
"name": "grafana",
"secondsToLive": 604800
}
```
Default value for the `secondsToLive` is 0, which means that the service account token will never expire.
**Example Response**:
```http
@@ -91,7 +91,23 @@ The text element lets you easily add text to the canvas. The element also suppor
### Icon
The icon element lets you add a supported icon to the canvas. Icons can have their color set based on thresholds / value mappings.
The icon element lets you add a supported icon to the canvas. Icons can have their color set based on thresholds or value mappings.
#### Add a custom icon
You can add a custom icon by referencing an SVG file. To add a custom icon, follow these steps:
1. Under **Icon > SVG Path**, if it's not already selected, select **Fixed** as your file source.
1. Click **Select a value** in the field below.
1. In the dialog box that opens, click the **URL** tab.
1. Enter the URL in the field below the **URL** tab.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-canvas-custom-image-v11.3.png" max-width="300px" alt="Add a custom image URL" >}}
1. Click **Select**.
1. (Optional) Add a background image to your icon with the **Background (icon)** option by following the steps to [add a custom image](#add-custom-images-to-elements).
If you don't have an SVG file, you can use a rectangle element instead of an icon and set its background image to an image file type. To add a custom image for another element type, follow the steps to [add a custom image](#add-custom-images-to-elements).
### Server
@@ -111,6 +127,25 @@ A button click will only trigger an API call when [inline editing](#inline-editi
{{< docs/play title="Canvas Visualization: Buttons" url="https://play.grafana.org/d/c9ea65f5-ed5a-45cf-8fb7-f82af7c3afdf/" >}}
## Add custom images to elements
You can add custom background images to all elements except **Button** by referencing an image URL.
The image must be hosted at a URL that allows requests from your Grafana instance.
To upload a custom image, follow these steps:
1. Under **Background (\<ELEMENT TYPE\>)**, if it's not already selected, select **Fixed** as your image source.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-canvas-custom-image-src-v11.3.png" max-width="300px" alt="Custom image source selection" >}}
1. Click **Select a value** in the field below.
1. In the dialog box that opens, click the **URL** tab.
1. Enter the URL in the field below the **URL** tab.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-canvas-custom-image-v11.3.png" max-width="300px" alt="Add a custom image URL" >}}
1. Click **Select**.
## Connections
When building a canvas, you can connect elements together to create more complex visualizations. Connections are created by dragging from the connection anchor of one element to the connection anchor of another element. You can also create connections to the background of the canvas. Connection anchors are displayed when you hover over an element and inline editing is turned on.
@@ -83,6 +83,12 @@ If a node lacks edge connections, it’s displayed on its own outside of the net
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
## Node graph options
Use the following options to refine your node graph visualization.
- **Zoom mode** - Choose how the node graph should handle zoom and scroll events.
## Nodes options
The **Nodes** options section provides configurations for node behaviors.
@@ -28,7 +28,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `publicDashboardsScene` | Enables public dashboard rendering using scenes | Yes |
| `featureHighlights` | Highlight Grafana Enterprise features | |
| `correlations` | Correlations page | Yes |
| `autoMigrateXYChartPanel` | Migrate old XYChart panel to new XYChart2 model | Yes |
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `accessControlOnCall` | Access control primitives for OnCall | Yes |
| `nestedFolders` | Enable folder nesting | Yes |
@@ -80,6 +79,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes |
| `cloudwatchMetricInsightsCrossAccount` | Enables cross account observability for Cloudwatch Metric Insights query builder | Yes |
| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | |
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes |
## Public preview feature toggles
@@ -114,7 +114,9 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `ssoSettingsSAML` | Use the new SSO Settings API to configure the SAML connector |
| `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars |
| `ssoSettingsLDAP` | Use the new SSO Settings API to configure LDAP |
| `singleTopNav` | Unifies the top search bar and breadcrumb bar into one |
| `useSessionStorageForRedirection` | Use session storage for handling the redirection after login |
| `reportingUseRawTimeRange` | Uses the original report or dashboard time range instead of making an absolute transformation |
## Experimental feature toggles
@@ -203,7 +205,6 @@ Experimental features might be changed or removed without prior notice.
| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture |
| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying |
| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources |
| `singleTopNav` | Unifies the top search bar and breadcrumb bar into one |
| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards |
| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics |
| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range |
@@ -216,7 +217,6 @@ Experimental features might be changed or removed without prior notice.
| `timeRangeProvider` | Enables time pickers sync |
| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor |
| `userStorageAPI` | Enables the user storage API |
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins |
| `dashboardSchemaV2` | Enables the new dashboard schema version 2, implementing changes necessary for dynamic dashboards and dashboards as code. |
| `playlistsWatcher` | Enables experimental watcher for playlists |
| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode |
@@ -159,10 +159,19 @@ Defaults to `false`.
auto_login = true
```
### Avoid automatic OAuth login
### Avoid automatic login
To sign in with a username and password and avoid automatic OAuth login, add the `disableAutoLogin` parameter to your login URL.
For example: `grafana.example.com/login?disableAutoLogin` or `grafana.example.com/login?disableAutoLogin=true`
The `disableAutoLogin=true` URL parameter allows users to bypass the automatic login feature in scenarios where incorrect configuration changes prevent normal login functionality.
This feature is especially helpful when you need to access the login screen to troubleshoot and fix misconfigurations.
#### How to use
1. Add `disableAutoLogin=true` as a query parameter to your Grafana URL.
- Example: `grafana.example.net/login?disableAutoLogin=true` or `grafana.example.net/login?disableAutoLogin`
1. This will redirect you to the standard login screen, bypassing the automatic login mechanism.
1. Fix any configuration issues and test your login setup.
This feature is available for both for OAuth and SAML. Ensure that after fixing the issue, you remove the parameter or revert the configuration to re-enable the automatic login feature, if desired.
### Hide sign-out menu
@@ -12,12 +12,12 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Azure AD OAuth2
title: Configure Azure AD OAuth2 authentication
menuTitle: Azure AD/Entra ID OAuth
title: Configure Azure AD/Entra ID OAuth authentication
weight: 800
---
# Configure Azure AD OAuth2 authentication
# Configure Azure AD/Entra ID OAuth authentication
The Azure AD authentication allows you to use a Microsoft Entra ID (formerly known as Azure Active Directory) tenant as an identity provider for Grafana. You can use Entra ID application roles to assign users and groups to Grafana roles from the Azure Portal.
@@ -27,7 +27,7 @@ If Users use the same email address in Microsoft Entra ID that they use with oth
## Create the Microsoft Entra ID application
To enable the Azure AD OAuth2, register your application with Entra ID.
To enable the Azure AD/Entra ID OAuth, register your application with Entra ID.
1. Log in to [Azure Portal](https://portal.azure.com), then click **Microsoft Entra ID** in the side menu.
@@ -181,7 +181,7 @@ Ensure that you have followed the steps in [Create the Microsoft Entra ID applic
Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle.
{{% /admonition %}}
As a Grafana Admin, you can configure your Azure AD OAuth2 client from within Grafana using the Grafana UI. To do this, navigate to the **Administration > Authentication > Azure AD** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values.
As a Grafana Admin, you can configure your Azure AD/Entra ID OAuth client from within Grafana using the Grafana UI. To do this, navigate to the **Administration > Authentication > Azure AD** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values.
After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations.
@@ -1,7 +1,7 @@
---
aliases:
- ../../../auth/generic-oauth/
description: Configure generic OAuth2 authentication
description: Configure Generic OAuth authentication
keywords:
- grafana
- configuration
@@ -12,12 +12,12 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Generic OAuth2
title: Configure generic OAuth2 authentication
menuTitle: Generic OAuth
title: Configure Generic OAuth authentication
weight: 700
---
# Configure generic OAuth2 authentication
# Configure Generic OAuth authentication
{{< docs/shared lookup="auth/intro.md" source="grafana" version="<GRAFANA VERSION>" >}}
@@ -31,9 +31,9 @@ Grafana provides OAuth2 integrations for the following auth providers:
- [Keycloak OAuth]({{< relref "../keycloak" >}})
- [Okta OAuth]({{< relref "../okta" >}})
If your OAuth2 provider is not listed, you can use generic OAuth2 authentication.
If your OAuth2 provider is not listed, you can use Generic OAuth authentication.
This topic describes how to configure generic OAuth2 authentication using different methods and includes [examples of setting up generic OAuth2]({{< relref "#examples-of-setting-up-generic-oauth2" >}}) with specific OAuth2 providers.
This topic describes how to configure Generic OAuth authentication using different methods and includes [examples of setting up Generic OAuth]({{< relref "#examples-of-setting-up-generic-oauth2" >}}) with specific OAuth2 providers.
## Before you begin
@@ -53,7 +53,7 @@ If Users use the same email address in Azure AD that they use with other authent
Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle.
{{% /admonition %}}
As a Grafana Admin, you can configure Generic OAuth2 client from within Grafana using the Generic OAuth UI. To do this, navigate to **Administration > Authentication > Generic OAuth** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values.
As a Grafana Admin, you can configure Generic OAuth client from within Grafana using the Generic OAuth UI. To do this, navigate to **Administration > Authentication > Generic OAuth** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values.
After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations.
@@ -98,7 +98,7 @@ Ensure that you have access to the [Grafana configuration file]({{< relref "../.
### Steps
To integrate your OAuth2 provider with Grafana using our generic OAuth2 authentication, follow these steps:
To integrate your OAuth2 provider with Grafana using our Generic OAuth authentication, follow these steps:
1. Create an OAuth2 application in your chosen OAuth2 provider.
1. Set the callback URL for your OAuth2 app to `http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/generic_oauth`.
@@ -114,9 +114,9 @@ To integrate your OAuth2 provider with Grafana using our generic OAuth2 authenti
| `client_id`, `client_secret` | These values must match the client ID and client secret from your OAuth2 app. |
| `auth_url` | The authorization endpoint of your OAuth2 provider. |
| `api_url` | The user information endpoint of your OAuth2 provider. Information returned by this endpoint must be compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). |
| `enabled` | Enables generic OAuth2 authentication. Set this value to `true`. |
| `enabled` | Enables Generic OAuth authentication. Set this value to `true`. |
Review the list of other generic OAuth2 [configuration options]({{< relref "#configuration-options" >}}) and complete them, as necessary.
Review the list of other Generic OAuth [configuration options]({{< relref "#configuration-options" >}}) and complete them, as necessary.
1. Optional: [Configure a refresh token]({{< relref "#configure-a-refresh-token" >}}):
@@ -130,7 +130,7 @@ To integrate your OAuth2 provider with Grafana using our generic OAuth2 authenti
1. Optional: [Configure team synchronization]({{< relref "#configure-team-synchronization" >}}).
1. Restart Grafana.
You should now see a generic OAuth2 login button on the login page and be able to log in or sign up with your OAuth2 provider.
You should now see a Generic OAuth login button on the login page and be able to log in or sign up with your OAuth2 provider.
### Configure login
@@ -185,7 +185,7 @@ When a user logs in using an OAuth2 provider, Grafana verifies that the access t
Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired.
To configure generic OAuth2 to use a refresh token, set `use_refresh_token` configuration option to `true` and perform one or both of the following steps, if required:
To configure Generic OAuth to use a refresh token, set `use_refresh_token` configuration option to `true` and perform one or both of the following steps, if required:
1. Extend the `scopes` field of `[auth.generic_oauth]` section in Grafana configuration file with additional scopes.
1. Enable the refresh token on the provider.
@@ -335,7 +335,7 @@ org_mapping = org_foo:org_foo:Viewer org_bar:org_bar:Editor *:org_baz:Editor
By using Team Sync, you can link your OAuth2 groups to teams within Grafana. This will automatically assign users to the appropriate teams.
Teams for each user are synchronized when the user logs in.
Generic OAuth2 groups can be referenced by group ID, such as `8bab1c86-8fba-33e5-2089-1d1c80ec267d` or `myteam`.
Generic OAuth groups can be referenced by group ID, such as `8bab1c86-8fba-33e5-2089-1d1c80ec267d` or `myteam`.
For information on configuring OAuth2 groups with Grafana using the `groups_attribute_path` configuration option, refer to [configuration options]({{< relref "#configuration-options" >}}).
To learn more about Team Sync, refer to [Configure team sync]({{< relref "../../configure-team-sync" >}}).
@@ -367,55 +367,60 @@ Payload:
## Configuration options
The following table outlines the various generic OAuth2 configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana.
The following table outlines the various Generic OAuth configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana. For more information, refer to [Override configuration with environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}).
| Setting | Required | Description | Default |
| ---------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `enabled` | No | Enables generic OAuth2 authentication. | `false` |
| `name` | No | Name that refers to the generic OAuth2 authentication from the Grafana user interface. | `OAuth` |
| `icon` | No | Icon used for the generic OAuth2 authentication in the Grafana user interface. | `signin` |
| `client_id` | Yes | Client ID provided by your OAuth2 app. | |
| `client_secret` | Yes | Client secret provided by your OAuth2 app. | |
| `auth_url` | Yes | Authorization endpoint of your OAuth2 provider. | |
| `token_url` | Yes | Endpoint used to obtain the OAuth2 access token. | |
| `api_url` | Yes | Endpoint used to obtain user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | |
| `auth_style` | No | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` |
| `scopes` | No | List of comma- or space-separated OAuth2 scopes. | `user:email` |
| `empty_scopes` | No | Set to `true` to use an empty scope during authentication. | `false` |
| `allow_sign_up` | No | Controls Grafana user creation through the generic OAuth2 login. Only existing Grafana users can log in with Generic OAuth if set to `false`. | `true` |
| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `id_token_attribute_name` | No | The name of the key used to extract the ID token from the returned OAuth2 token. | `id_token` |
| `login_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user login lookup from the user ID token. For more information on how user login is retrieved, refer to [Configure login]({{< relref "#configure-login" >}}). | |
| `name_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user name lookup from the user ID token. This name will be used as the user's display name. For more information on how user display name is retrieved, refer to [Configure display name]({{< relref "#configure-display-name" >}}). | |
| `email_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user email lookup from the user information. For more information on how user email is retrieved, refer to [Configure email address]({{< relref "#configure-email-address" >}}). | |
| `email_attribute_name` | No | Name of the key to use for user email lookup within the `attributes` map of OAuth2 ID token. For more information on how user email is retrieved, refer to [Configure email address]({{< relref "#configure-email-address" >}}). | `email:primary` |
| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `org_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `org_mapping` | No | List of comma- or space-separated `<ExternalOrgName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `allow_assign_grafana_admin` | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` |
| `groups_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a string array of groups. | |
| `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | |
| `allowed_organizations` | No | List of comma- or space-separated organizations. The user should be a member of at least one organization to log in. | |
| `allowed_domains` | No | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | |
| `team_ids` | No | String list of team IDs. If set, the user must be a member of one of the given teams to log in. If you configure `team_ids`, you must also configure `teams_url` and `team_ids_attribute_path`. | |
| `team_ids_attribute_path` | No | The [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana team ID lookup within the results returned by the `teams_url` endpoint. | |
| `teams_url` | No | The URL used to query for team IDs. If not set, the default value is `/teams`. If you configure `teams_url`, you must also configure `team_ids_attribute_path`. | |
| `tls_skip_verify_insecure` | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | The path to the certificate. | |
| `tls_client_key` | No | The path to the key. | |
| `tls_client_ca` | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `false` |
| `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. | `false` |
{{< admonition type="note" >}}
If the configuration option requires a JMESPath expression that includes a colon, enclose the entire expression in quotes to prevent parsing errors. For example `role_attribute_path: "role:view"`
{{< /admonition >}}
## Examples of setting up generic OAuth2
| Setting | Required | Supported on Cloud | Description | Default |
| ---------------------------- | -------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `enabled` | No | Yes | Enables Generic OAuth authentication. | `false` |
| `name` | No | Yes | Name that refers to the Generic OAuth authentication from the Grafana user interface. | `OAuth` |
| `icon` | No | Yes | Icon used for the Generic OAuth authentication in the Grafana user interface. | `signin` |
| `client_id` | Yes | Yes | Client ID provided by your OAuth2 app. | |
| `client_secret` | Yes | Yes | Client secret provided by your OAuth2 app. | |
| `auth_url` | Yes | Yes | Authorization endpoint of your OAuth2 provider. | |
| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | |
| `api_url` | Yes | Yes | Endpoint used to obtain user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | |
| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` |
| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `user:email` |
| `empty_scopes` | No | Yes | Set to `true` to use an empty scope during authentication. | `false` |
| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Generic OAuth login. Only existing Grafana users can log in with Generic OAuth if set to `false`. | `true` |
| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `id_token_attribute_name` | No | Yes | The name of the key used to extract the ID token from the returned OAuth2 token. | `id_token` |
| `login_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user login lookup from the user ID token. For more information on how user login is retrieved, refer to [Configure login]({{< relref "#configure-login" >}}). | |
| `name_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user name lookup from the user ID token. This name will be used as the user's display name. For more information on how user display name is retrieved, refer to [Configure display name]({{< relref "#configure-display-name" >}}). | |
| `email_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user email lookup from the user information. For more information on how user email is retrieved, refer to [Configure email address]({{< relref "#configure-email-address" >}}). | |
| `email_attribute_name` | No | Yes | Name of the key to use for user email lookup within the `attributes` map of OAuth2 ID token. For more information on how user email is retrieved, refer to [Configure email address]({{< relref "#configure-email-address" >}}). | `email:primary` |
| `role_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` |
| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `org_mapping` | No | No | List of comma- or space-separated `<ExternalOrgName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `allow_assign_grafana_admin` | No | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `groups_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a string array of groups. | |
| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | |
| `allowed_organizations` | No | Yes | List of comma- or space-separated organizations. The user should be a member of at least one organization to log in. | |
| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | |
| `team_ids` | No | Yes | String list of team IDs. If set, the user must be a member of one of the given teams to log in. If you configure `team_ids`, you must also configure `teams_url` and `team_ids_attribute_path`. | |
| `team_ids_attribute_path` | No | Yes | The [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana team ID lookup within the results returned by the `teams_url` endpoint. | |
| `teams_url` | No | Yes | The URL used to query for team IDs. If not set, the default value is `/teams`. If you configure `teams_url`, you must also configure `team_ids_attribute_path`. | |
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | No | The path to the certificate. | |
| `tls_client_key` | No | No | The path to the key. | |
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `false` |
| `use_refresh_token` | No | Yes | Set to `true` to use refresh token and check access token expiration. | `false` |
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
This section includes examples of setting up generic OAuth2 integration.
## Examples of setting up Generic OAuth
This section includes examples of setting up Generic OAuth integration.
### Set up OAuth2 with Descope
To set up generic OAuth2 authentication with Descope, follow these steps:
To set up Generic OAuth authentication with Descope, follow these steps:
1. Create a Descope Project [here](https://app.descope.com/gettingStarted), and go through the Getting Started Wizard to configure your authentication. You can skip step if you already have Descope project set up.
@@ -449,7 +454,7 @@ To set up generic OAuth2 authentication with Descope, follow these steps:
### Set up OAuth2 with Auth0
To set up generic OAuth2 authentication with Auth0, follow these steps:
To set up Generic OAuth authentication with Auth0, follow these steps:
1. Create an Auth0 application using the following parameters:
@@ -482,7 +487,7 @@ To set up generic OAuth2 authentication with Auth0, follow these steps:
### Set up OAuth2 with Bitbucket
To set up generic OAuth2 authentication with Bitbucket, follow these steps:
To set up Generic OAuth authentication with Bitbucket, follow these steps:
1. Navigate to **Settings > Workspace setting > OAuth consumers** in BitBucket.
@@ -517,7 +522,7 @@ By default, a refresh token is included in the response for the **Authorization
### Set up OAuth2 with OneLogin
To set up generic OAuth2 authentication with OneLogin, follow these steps:
To set up Generic OAuth authentication with OneLogin, follow these steps:
1. Create a new Custom Connector in OneLogin with the following settings:
@@ -553,7 +558,7 @@ To set up generic OAuth2 authentication with OneLogin, follow these steps:
### Set up OAuth2 with Dex
To set up generic OAuth2 authentication with [Dex IdP](https://dexidp.io/), follow these
To set up Generic OAuth authentication with [Dex IdP](https://dexidp.io/), follow these
steps:
1. Add Grafana as a client in the Dex config YAML file:
@@ -1,7 +1,7 @@
---
aliases:
- ../../../auth/github/
description: Configure GitHub OAuth2 authentication
description: Configure GitHub OAuth authentication
keywords:
- grafana
- configuration
@@ -12,16 +12,16 @@ labels:
- cloud
- enterprise
- oss
menuTitle: GitHub OAuth2
title: Configure GitHub OAuth2 authentication
menuTitle: GitHub OAuth
title: Configure GitHub OAuth authentication
weight: 900
---
# Configure GitHub OAuth2 authentication
# Configure GitHub OAuth authentication
{{< docs/shared lookup="auth/intro.md" source="grafana" version="<GRAFANA VERSION>" >}}
This topic describes how to configure GitHub OAuth2 authentication.
This topic describes how to configure GitHub OAuth authentication.
{{% admonition type="note" %}}
If Users use the same email address in GitHub that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information.
@@ -31,13 +31,23 @@ If Users use the same email address in GitHub that they use with other authentic
Ensure you know how to create a GitHub OAuth app. Consult GitHub's documentation on [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) for more information.
### Create a GitHub OAuth App
1. Log in to your GitHub account.
In **Profile > Settings > Developer settings**, select **OAuth Apps**.
1. Click **New OAuth App**.
1. Fill out the fields, using your Grafana homepage URL when appropriate.
In the **Authorization callback URL** field, enter the following: `https://<YOUR-GRAFANA-URL>/login/github` .
1. Note your client ID.
1. Generate, then note, your client secret.
## Configure GitHub authentication client using the Grafana UI
{{% admonition type="note" %}}
Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle.
{{% /admonition %}}
As a Grafana Admin, you can configure GitHub OAuth2 client from within Grafana using the GitHub UI. To do this, navigate to **Administration > Authentication > GitHub** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values.
As a Grafana Admin, you can configure GitHub OAuth client from within Grafana using the GitHub UI. To do this, navigate to **Administration > Authentication > GitHub** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values.
After you have filled in the form, click **Save** . If the save was successful, Grafana will apply the new configurations.
@@ -156,7 +166,18 @@ All other users are granted the `Viewer` role.
role_attribute_path = contains(groups[*], '@my-github-organization/my-github-team') && 'Editor' || 'Viewer'
```
#### Map server administrator role
##### Map roles using multiple GitHub teams
In this example, the users from GitHub teams `admins` and `devops` have been granted the `Admin` role,
the users from GitHub teams `engineers` and `managers` have been granted the `Editor` role,
the users from GitHub team `qa` have been granted the `Viewer` role and
all other users are granted the `None` role.
```bash
role_attribute_path = contains(groups[*], '@my-github-organization/admins') && 'Admin' || contains(groups[*], '@my-github-organization/devops') && 'Admin' || contains(groups[*], '@my-github-organization/engineers') && 'Editor' || contains(groups[*], '@my-github-organization/managers') && 'Editor' || contains(groups[*], '@my-github-organization/qa') && 'Viewer' || 'None'
```
##### Map server administrator role
In this example, the user with login `octocat` has been granted the `Admin` organization role as well as the Grafana server admin role.
All other users are granted the `Viewer` role.
@@ -215,30 +236,35 @@ To learn more about Team Sync, refer to [Configure team sync]({{< relref "../../
## Configuration options
The table below describes all GitHub OAuth configuration options. Like any other Grafana configuration, you can apply these options as environment variables.
The table below describes all GitHub OAuth configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana. For more information, refer to [Override configuration with environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}).
| Setting | Required | Description | Default |
| ---------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `enabled` | No | Whether GitHub OAuth authentication is allowed. | `false` |
| `name` | No | Name used to refer to the GitHub authentication in the Grafana user interface. | `GitHub` |
| `icon` | No | Icon used for GitHub authentication in the Grafana user interface. | `github` |
| `client_id` | Yes | Client ID provided by your GitHub OAuth app. | |
| `client_secret` | Yes | Client secret provided by your GitHub OAuth app. | |
| `auth_url` | Yes | Authorization endpoint of your GitHub OAuth provider. | `https://github.com/login/oauth/authorize` |
| `token_url` | Yes | Endpoint used to obtain GitHub OAuth access token. | `https://github.com/login/oauth/access_token` |
| `api_url` | Yes | Endpoint used to obtain GitHub user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://api.github.com/user` |
| `scopes` | No | List of comma- or space-separated GitHub OAuth scopes. | `user:email,read:org` |
| `allow_sign_up` | No | Whether to allow new Grafana user creation through GitHub login. If set to `false`, then only existing Grafana users can log in with GitHub OAuth. | `true` |
| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the user information obtained from the UserInfo endpoint. If no role is found, Grafana creates a JSON data with `groups` key that maps to GitHub teams obtained from GitHub's [`/api/user/teams`](https://docs.github.com/en/rest/teams/teams#list-teams-for-the-authenticated-user) endpoint, and evaluates the expression using this data. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping](#org-roles-mapping-example). | |
| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping](#org-roles-mapping-example). | `false` |
| `org_mapping` | No | List of comma- or space-separated `<ExternalGitHubTeamName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `allow_assign_grafana_admin` | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. | `false` |
| `allowed_organizations` | No | List of comma- or space-separated organizations. User must be a member of at least one organization to log in. | |
| `allowed_domains` | No | List of comma- or space-separated domains. User must belong to at least one domain to log in. | |
| `team_ids` | No | Integer list of team IDs. If set, user has to be a member of one of the given teams to log in. | |
| `tls_skip_verify_insecure` | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | The path to the certificate. | |
| `tls_client_key` | No | The path to the key. | |
| `tls_client_ca` | No | The path to the trusted certificate authority list. | |
{{< admonition type="note" >}}
If the configuration option requires a JMESPath expression that includes a colon, enclose the entire expression in quotes to prevent parsing errors. For example `role_attribute_path: "role:view"`
{{< /admonition >}}
| Setting | Required | Supported on Cloud | Description | Default |
| ---------------------------- | -------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `enabled` | No | Yes | Whether GitHub OAuth authentication is allowed. | `false` |
| `name` | No | Yes | Name used to refer to the GitHub authentication in the Grafana user interface. | `GitHub` |
| `icon` | No | Yes | Icon used for GitHub authentication in the Grafana user interface. | `github` |
| `client_id` | Yes | Yes | Client ID provided by your GitHub OAuth app. | |
| `client_secret` | Yes | Yes | Client secret provided by your GitHub OAuth app. | |
| `auth_url` | Yes | Yes | Authorization endpoint of your GitHub OAuth provider. | `https://github.com/login/oauth/authorize` |
| `token_url` | Yes | Yes | Endpoint used to obtain GitHub OAuth access token. | `https://github.com/login/oauth/access_token` |
| `api_url` | Yes | Yes | Endpoint used to obtain GitHub user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://api.github.com/user` |
| `scopes` | No | Yes | List of comma- or space-separated GitHub OAuth scopes. | `user:email,read:org` |
| `allow_sign_up` | No | Yes | Whether to allow new Grafana user creation through GitHub login. If set to `false`, then only existing Grafana users can log in with GitHub OAuth. | `true` |
| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `role_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the user information obtained from the UserInfo endpoint. If no role is found, Grafana creates a JSON data with `groups` key that maps to GitHub teams obtained from GitHub's [`/api/user/teams`](https://docs.github.com/en/rest/teams/teams#list-teams-for-the-authenticated-user) endpoint, and evaluates the expression using this data. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping](#org-roles-mapping-example). | |
| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping](#org-roles-mapping-example). | `false` |
| `org_mapping` | No | No | List of comma- or space-separated `<ExternalGitHubTeamName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. | `false` |
| `allow_assign_grafana_admin` | No | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `allowed_organizations` | No | Yes | List of comma- or space-separated organizations. User must be a member of at least one organization to log in. | |
| `allowed_domains` | No | Yes | List of comma- or space-separated domains. User must belong to at least one domain to log in. | |
| `team_ids` | No | Yes | Integer list of team IDs. If set, user has to be a member of one of the given teams to log in. | |
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | No | The path to the certificate. | |
| `tls_client_key` | No | No | The path to the key. | |
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
@@ -1,7 +1,7 @@
---
aliases:
- ../../../auth/gitlab/
description: Grafana OAuthentication Guide
description: Grafana GitLab OAuth Guide
keywords:
- grafana
- configuration
@@ -12,16 +12,16 @@ labels:
- cloud
- enterprise
- oss
menuTitle: GitLab OAuth2
title: Configure GitLab OAuth2 authentication
menuTitle: GitLab OAuth
title: Configure GitLab OAuth authentication
weight: 1000
---
# Configure GitLab OAuth2 authentication
# Configure GitLab OAuth authentication
{{< docs/shared lookup="auth/intro.md" source="grafana" version="<GRAFANA VERSION>" >}}
This topic describes how to configure GitLab OAuth2 authentication.
This topic describes how to configure GitLab OAuth authentication.
{{% admonition type="note" %}}
If Users use the same email address in GitLab that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information.
@@ -31,13 +31,23 @@ If Users use the same email address in GitLab that they use with other authentic
Ensure you know how to create a GitLab OAuth application. Consult GitLab's documentation on [creating a GitLab OAuth application](https://docs.gitlab.com/ee/integration/oauth_provider.html) for more information.
### Create a GitLab OAuth Application
1. Log in to your GitLab account and go to **Profile > Preferences > Applications**.
1. Click **Add new application**.
1. Fill out the fields.
- In the **Redirect URI** field, enter the following: `https://<YOUR-GRAFANA-URL>/login/gitlab` and check `openid`, `email`, `profile` in the **Scopes** list.
- Leave the **Confidential** checkbox checked.
1. Click **Save application**.
1. Note your **Application ID** (this is the `Client Id`) and **Secret** (this is the `Client Secret`).
## Configure GitLab authentication client using the Grafana UI
{{% admonition type="note" %}}
Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle.
{{% /admonition %}}
As a Grafana Admin, you can configure GitLab OAuth2 client from within Grafana using the GitLab UI. To do this, navigate to **Administration > Authentication > GitLab** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values.
As a Grafana Admin, you can configure GitLab OAuth client from within Grafana using the GitLab UI. To do this, navigate to **Administration > Authentication > GitLab** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values.
After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations.
@@ -97,10 +107,10 @@ To configure GitLab authentication with Grafana, follow these steps:
1. Refer to the following table to update field values located in the `[auth.gitlab]` section of the Grafana configuration file:
| Field | Description |
| ---------------------------- | -------------------------------------------------------------------------------------------- |
| `client_id`, `client_secret` | These values must match the client ID and client secret from your GitLab OAuth2 application. |
| `enabled` | Enables GitLab authentication. Set this value to `true`. |
| Field | Description |
| ---------------------------- | --------------------------------------------------------------------------------------------- |
| `client_id`, `client_secret` | These values must match the `Application ID` and `Secret` from your GitLab OAuth application. |
| `enabled` | Enables GitLab authentication. Set this value to `true`. |
Review the list of other GitLab [configuration options]({{< relref "#configuration-options" >}}) and complete them, as necessary.
@@ -234,7 +244,9 @@ use_refresh_token = true
## Configure team synchronization
> **Note:** Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud](/docs/grafana-cloud/).
{{% admonition type="note" %}}
Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud](/docs/grafana-cloud/).
{{% /admonition %}}
By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams.
Teams for each user are synchronized when the user logs in.
@@ -247,31 +259,36 @@ To learn more about Team Sync, refer to [Configure team sync]({{< relref "../../
## Configuration options
The table below describes all GitLab OAuth configuration options. Like any other Grafana configuration, you can apply these options as environment variables.
The table below describes all GitLab OAuth configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana. For more information, refer to [Override configuration with environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}).
| Setting | Required | Description | Default |
| ---------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `enabled` | Yes | Whether GitLab OAuth authentication is allowed. | `false` |
| `client_id` | Yes | Client ID provided by your GitLab OAuth app. | |
| `client_secret` | Yes | Client secret provided by your GitLab OAuth app. | |
| `auth_url` | Yes | Authorization endpoint of your GitLab OAuth provider. If you use your own instance of GitLab instead of gitlab.com, adjust `auth_url` by replacing the `gitlab.com` hostname with your own. | `https://gitlab.com/oauth/authorize` |
| `token_url` | Yes | Endpoint used to obtain GitLab OAuth access token. If you use your own instance of GitLab instead of gitlab.com, adjust `token_url` by replacing the `gitlab.com` hostname with your own. | `https://gitlab.com/oauth/token` |
| `api_url` | No | Grafana uses `<api_url>/user` endpoint to obtain GitLab user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://gitlab.com/api/v4` |
| `name` | No | Name used to refer to the GitLab authentication in the Grafana user interface. | `GitLab` |
| `icon` | No | Icon used for GitLab authentication in the Grafana user interface. | `gitlab` |
| `scopes` | No | List of comma or space-separated GitLab OAuth scopes. | `openid email profile` |
| `allow_sign_up` | No | Whether to allow new Grafana user creation through GitLab login. If set to `false`, then only existing Grafana users can log in with GitLab OAuth. | `true` |
| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the GitLab OAuth token. If no role is found, Grafana creates a JSON data with `groups` key that maps to groups obtained from GitLab's `/oauth/userinfo` endpoint, and evaluates the expression using this data. Finally, if a valid role is still not found, the expression is evaluated against the user information retrieved from `api_url/users` endpoint and groups retrieved from `api_url/groups` endpoint. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana role cannot be extracted using `role_attribute_path`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `org_mapping` | No | List of comma- or space-separated `<ExternalGitlabGroupName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `allow_assign_grafana_admin` | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. | `false` |
| `allowed_domains` | No | List of comma or space-separated domains. User must belong to at least one domain to log in. | |
| `allowed_groups` | No | List of comma or space-separated groups. The user should be a member of at least one group to log in. | |
| `tls_skip_verify_insecure` | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | The path to the certificate. | |
| `tls_client_key` | No | The path to the key. | |
| `tls_client_ca` | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` |
| `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. The `accessTokenExpirationCheck` feature toggle should also be enabled to use refresh token. | `true` |
{{< admonition type="note" >}}
If the configuration option requires a JMESPath expression that includes a colon, enclose the entire expression in quotes to prevent parsing errors. For example `role_attribute_path: "role:view"`
{{< /admonition >}}
| Setting | Required | Supported on Cloud | Description | Default |
| ---------------------------- | -------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `enabled` | Yes | Yes | Whether GitLab OAuth authentication is allowed. | `false` |
| `client_id` | Yes | Yes | Client ID provided by your GitLab OAuth app. | |
| `client_secret` | Yes | Yes | Client secret provided by your GitLab OAuth app. | |
| `auth_url` | Yes | Yes | Authorization endpoint of your GitLab OAuth provider. If you use your own instance of GitLab instead of gitlab.com, adjust `auth_url` by replacing the `gitlab.com` hostname with your own. | `https://gitlab.com/oauth/authorize` |
| `token_url` | Yes | Yes | Endpoint used to obtain GitLab OAuth access token. If you use your own instance of GitLab instead of gitlab.com, adjust `token_url` by replacing the `gitlab.com` hostname with your own. | `https://gitlab.com/oauth/token` |
| `api_url` | No | Yes | Grafana uses `<api_url>/user` endpoint to obtain GitLab user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://gitlab.com/api/v4` |
| `name` | No | Yes | Name used to refer to the GitLab authentication in the Grafana user interface. | `GitLab` |
| `icon` | No | Yes | Icon used for GitLab authentication in the Grafana user interface. | `gitlab` |
| `scopes` | No | Yes | List of comma or space-separated GitLab OAuth scopes. | `openid email profile` |
| `allow_sign_up` | No | Yes | Whether to allow new Grafana user creation through GitLab login. If set to `false`, then only existing Grafana users can log in with GitLab OAuth. | `true` |
| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `role_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the GitLab OAuth token. If no role is found, Grafana creates a JSON data with `groups` key that maps to groups obtained from GitLab's `/oauth/userinfo` endpoint, and evaluates the expression using this data. Finally, if a valid role is still not found, the expression is evaluated against the user information retrieved from `api_url/users` endpoint and groups retrieved from `api_url/groups` endpoint. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana role cannot be extracted using `role_attribute_path`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `org_mapping` | No | No | List of comma- or space-separated `<ExternalGitlabGroupName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. | `false` |
| `allow_assign_grafana_admin` | No | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `allowed_domains` | No | Yes | List of comma or space-separated domains. User must belong to at least one domain to log in. | |
| `allowed_groups` | No | Yes | List of comma or space-separated groups. The user should be a member of at least one group to log in. | |
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | No | The path to the certificate. | |
| `tls_client_key` | No | No | The path to the key. | |
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` |
| `use_refresh_token` | No | Yes | Set to `true` to use refresh token and check access token expiration. The `accessTokenExpirationCheck` feature toggle should also be enabled to use refresh token. | `true` |
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
@@ -1,7 +1,7 @@
---
aliases:
- ../../../auth/google/
description: Grafana OAuthentication Guide
description: Grafana Google OAuth Guide
labels:
products:
- cloud
@@ -1,7 +1,7 @@
---
aliases:
- ../../../auth/keycloak/
description: Keycloak Grafana OAuthentication Guide
description: Grafana Keycloak Guide
keywords:
- grafana
- keycloak
@@ -260,27 +260,32 @@ To learn more about Team Sync, refer to [Configure Team Sync]({{< relref "../../
## Configuration options
The following table outlines the various Okta OIDC configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana.
The following table outlines the various Okta OIDC configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana. For more information, refer to [Override configuration with environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}).
| Setting | Required | Description | Default |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- |
| `enabled` | No | Enables Okta OIDC authentication. | `false` |
| `name` | No | Name that refers to the Okta OIDC authentication from the Grafana user interface. | `Okta` |
| `icon` | No | Icon used for the Okta OIDC authentication in the Grafana user interface. | `okta` |
| `client_id` | Yes | Client ID provided by your Okta OIDC app. | |
| `client_secret` | Yes | Client secret provided by your Okta OIDC app. | |
| `auth_url` | Yes | Authorization endpoint of your Okta OIDC provider. | |
| `token_url` | Yes | Endpoint used to obtain the Okta OIDC access token. | |
| `api_url` | Yes | Endpoint used to obtain user information. | |
| `scopes` | No | List of comma- or space-separated Okta OIDC scopes. | `openid profile email groups` |
| `allow_sign_up` | No | Controls Grafana user creation through the Okta OIDC login. Only existing Grafana users can log in with Okta OIDC if set to `false`. | `true` |
| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the Okta OIDC ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `org_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `org_mapping` | No | List of comma- or space-separated `<ExternalOrgName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` |
| `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. | |
| `allowed_domains` | No | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | |
| `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` |
| `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. | `false` |
{{< admonition type="note" >}}
If the configuration option requires a JMESPath expression that includes a colon, enclose the entire expression in quotes to prevent parsing errors. For example `role_attribute_path: "role:view"`
{{< /admonition >}}
| Setting | Required | Supported on Cloud | Description | Default |
| ----------------------- | -------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- |
| `enabled` | No | Yes | Enables Okta OIDC authentication. | `false` |
| `name` | No | Yes | Name that refers to the Okta OIDC authentication from the Grafana user interface. | `Okta` |
| `icon` | No | Yes | Icon used for the Okta OIDC authentication in the Grafana user interface. | `okta` |
| `client_id` | Yes | Yes | Client ID provided by your Okta OIDC app. | |
| `client_secret` | Yes | Yes | Client secret provided by your Okta OIDC app. | |
| `auth_url` | Yes | Yes | Authorization endpoint of your Okta OIDC provider. | |
| `token_url` | Yes | Yes | Endpoint used to obtain the Okta OIDC access token. | |
| `api_url` | Yes | Yes | Endpoint used to obtain user information. | |
| `scopes` | No | Yes | List of comma- or space-separated Okta OIDC scopes. | `openid profile email groups` |
| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Okta OIDC login. Only existing Grafana users can log in with Okta OIDC if set to `false`. | `true` |
| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `role_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the Okta OIDC ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `org_mapping` | No | No | List of comma- or space-separated `<ExternalOrgName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` |
| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. | |
| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | |
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` |
| `use_refresh_token` | No | Yes | Set to `true` to use refresh token and check access token expiration. | `false` |
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
@@ -0,0 +1,51 @@
<mjml>
<!-- global variables -->
<mj-include path="./partials/_globals.mjml" />
<!-- css styling -->
<mj-include path="./partials/layout/theme.css" type="css" css-inline="inline" />
<mj-head>
<!-- ⬇ Don't forget to specify an email subject below! ⬇ -->
<mj-title> {{ Subject .Subject .TemplateData "Verify your email" }} </mj-title>
<mj-include path="./partials/layout/head.mjml" />
</mj-head>
<mj-body>
<mj-section>
<mj-include path="./partials/layout/header.mjml" />
</mj-section>
<mj-wrapper css-class="background" padding="0">
<mj-section padding="0">
<mj-column>
<mj-text>
<h2>Please verify your email</h2>
</mj-text>
<mj-text>
Copy and paste the confirmation code into the login form to verify your email address. This confirmation code
will expire in {{ .Expire }} minutes.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="10px 25px">
<mj-column css-class="well">
<mj-text font-size="22px" font-weight="bold" align="center"> {{ .ConfirmationCode }} </mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-text> Alternatively, you can use the button below to verify your email address. </mj-text>
<mj-button href="{{ .AppUrl }}login/?code={{ .Code }}&amp;confirmationCode={{ .ConfirmationCode }}">
Verify your email
</mj-button>
<mj-text> You can also copy and paste this link into your browser directly: </mj-text>
<mj-text>
<a rel="noopener" href="{{ .AppUrl }}login?code={{ .Code }}&amp;confirmationCode={{ .ConfirmationCode }}"
>{{ .AppUrl }}login?code={{ .Code }}&amp;confirmationCode={{ .ConfirmationCode }}</a
>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<mj-section>
<mj-include path="./partials/layout/footer.mjml" />
</mj-section>
</mj-body>
</mjml>
@@ -0,0 +1,10 @@
[[HiddenSubject .Subject "Verify your email"]]
Hi,
Copy and paste the email verification code:
[[.ConfirmationCode]]
into the login form to verify your email address. This confirmation code will expire in {{ .Expire }} minutes.
Alternatively, you can use the button below to verify your email address.
[[.AppUrl]]login/?code=[[.Code]]&confirmationCode=[[.ConfirmationCode]]
@@ -0,0 +1,53 @@
<mjml>
<!-- global variables -->
<mj-include path="./partials/_globals.mjml" />
<!-- css styling -->
<mj-include path="./partials/layout/theme.css" type="css" css-inline="inline" />
<mj-head>
<!-- ⬇ Don't forget to specify an email subject below! ⬇ -->
<mj-title> {{ Subject .Subject .TemplateData "Welcome to Grafana, please complete your sign up!" }} </mj-title>
<mj-include path="./partials/layout/head.mjml" />
</mj-head>
<mj-body>
<mj-section>
<mj-include path="./partials/layout/header.mjml" />
</mj-section>
<mj-wrapper css-class="background" padding="0">
<mj-section padding="0">
<mj-column>
<mj-text>
<h2>Please complete your signup</h2>
</mj-text>
<mj-text>
Copy and paste the confirmation code into the sign up form to verify your email address. This confirmation
code will expire in {{ .Expire }} minutes.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="10px 25px">
<mj-column css-class="well">
<mj-text font-size="22px" font-weight="bold" align="center"> {{ .ConfirmationCode }} </mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-text> Alternatively, you can use the button below to complete your sign up. </mj-text>
<mj-button href="{{ .AppUrl }}login/?code={{ .Code }}&amp;confirmationCode={{ .ConfirmationCode }}&amp;signup=true">
Complete Sign Up
</mj-button>
<mj-text> You can also copy and paste this link into your browser directly: </mj-text>
<mj-text>
<a
rel="noopener"
href="{{ .AppUrl }}login?code={{ .Code }}&amp;confirmationCode={{ .ConfirmationCode }}&amp;signup=true"
>{{ .AppUrl }}login?code={{ .Code }}&amp;confirmationCode={{ .ConfirmationCode }}&amp;signup=true</a
>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<mj-section>
<mj-include path="./partials/layout/footer.mjml" />
</mj-section>
</mj-body>
</mjml>
@@ -0,0 +1,10 @@
[[HiddenSubject .Subject "Welcome to Grafana, please complete your signup!"]]
Hi,
Copy and paste the email verification code:
[[.ConfirmationCode]]
into the sign up form to verify your email address. This confirmation code will expire in {{ .Expire }} minutes.
Alternatively, you can use the button below to verify your email address.
[[.AppUrl]]login/?code=[[.Code]]&confirmationCode=[[.ConfirmationCode]]
+1 -1
View File
@@ -61,7 +61,7 @@ require (
github.com/go-stack/stack v1.8.1 // @grafana/grafana-backend-group
github.com/gobwas/glob v0.2.3 // @grafana/grafana-backend-group
github.com/gogo/protobuf v1.3.2 // @grafana/alerting-backend
github.com/golang-jwt/jwt/v4 v4.5.0 // @grafana/grafana-backend-group
github.com/golang-jwt/jwt/v4 v4.5.1 // @grafana/grafana-backend-group
github.com/golang-migrate/migrate/v4 v4.7.0 // @grafana/grafana-backend-group
github.com/golang/mock v1.6.0 // @grafana/alerting-backend
github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group
+2 -1
View File
@@ -2060,8 +2060,9 @@ github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+6 -6
View File
@@ -208,7 +208,7 @@
"knip": "^5.10.0",
"lerna": "8.1.8",
"mini-css-extract-plugin": "2.9.2",
"msw": "2.4.13",
"msw": "2.6.4",
"mutationobserver-shim": "0.3.7",
"ngtemplate-loader": "2.1.0",
"node-notifier": "10.0.1",
@@ -225,7 +225,7 @@
"redux-mock-store": "1.5.5",
"rimraf": "6.0.1",
"rudder-sdk-js": "2.48.26",
"sass": "1.79.5",
"sass": "1.80.7",
"sass-loader": "16.0.3",
"smtp-tester": "^2.1.0",
"style-loader": "4.0.0",
@@ -253,7 +253,7 @@
"@emotion/css": "11.13.4",
"@emotion/react": "11.13.3",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@floating-ui/react": "0.26.27",
"@floating-ui/react": "0.26.28",
"@formatjs/intl-durationformat": "^0.6.0",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/aws-sdk": "0.5.0",
@@ -272,8 +272,8 @@
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/saga-icons": "workspace:*",
"@grafana/scenes": "5.23.1",
"@grafana/scenes-react": "5.23.1",
"@grafana/scenes": "5.25.0",
"@grafana/scenes-react": "5.25.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -361,7 +361,7 @@
"prismjs": "1.29.0",
"rc-slider": "11.1.7",
"rc-time-picker": "3.7.3",
"rc-tree": "5.9.0",
"rc-tree": "5.10.1",
"re-resizable": "6.10.1",
"react": "18.2.0",
"react-diff-viewer-continued": "^3.4.0",
@@ -278,5 +278,6 @@ export interface AuthSettings {
GenericOAuthSkipOrgRoleSync?: boolean;
disableLogin?: boolean;
passwordlessEnabled?: boolean;
basicAuthStrongPasswordPolicy?: boolean;
}
@@ -35,7 +35,6 @@ export interface FeatureToggles {
autoMigratePiechartPanel?: boolean;
autoMigrateWorldmapPanel?: boolean;
autoMigrateStatPanel?: boolean;
autoMigrateXYChartPanel?: boolean;
disableAngular?: boolean;
canvasPanelNesting?: boolean;
vizActions?: boolean;
@@ -232,9 +231,11 @@ export interface FeatureToggles {
preinstallAutoUpdate?: boolean;
dashboardSchemaV2?: boolean;
playlistsWatcher?: boolean;
passwordlessMagicLinkAuthentication?: boolean;
exploreMetricsRelatedLogs?: boolean;
enableExtensionsAdminPage?: boolean;
zipkinBackendMigration?: boolean;
enableSCIM?: boolean;
crashDetection?: boolean;
reportingUseRawTimeRange?: boolean;
}
@@ -39,6 +39,19 @@ export const versionedPages = {
'10.2.3': 'data-testid Skip change password button',
},
},
PasswordlessLogin: {
url: {
[MIN_GRAFANA_VERSION]: '/login/passwordless/authenticate',
},
email: {
'10.2.3': 'data-testid Email input field',
[MIN_GRAFANA_VERSION]: 'Email input field',
},
submit: {
'10.2.3': 'data-testid PasswordlessLogin button',
[MIN_GRAFANA_VERSION]: 'PasswordlessLogin button',
},
},
Home: {
url: {
[MIN_GRAFANA_VERSION]: '/',
+3 -3
View File
@@ -37,10 +37,10 @@
},
"dependencies": {
"@emotion/css": "11.13.4",
"@floating-ui/react": "0.26.27",
"@floating-ui/react": "0.26.28",
"@grafana/data": "11.4.0-pre",
"@grafana/experimental": "2.1.2",
"@grafana/faro-web-sdk": "1.11.0",
"@grafana/faro-web-sdk": "1.12.0",
"@grafana/runtime": "11.4.0-pre",
"@grafana/schema": "11.4.0-pre",
"@grafana/ui": "11.4.0-pre",
@@ -130,7 +130,7 @@
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "6.1.1",
"rollup-plugin-node-externals": "^7.1.3",
"sass": "1.79.5",
"sass": "1.80.7",
"sass-loader": "16.0.3",
"style-loader": "4.0.0",
"testing-library-selector": "0.3.1",
@@ -17,11 +17,11 @@ import {
Icon,
InlineField,
InlineFieldRow,
ScrollContainer,
SelectMenuOptions,
useStyles2,
useTheme2,
} from '@grafana/ui';
import { ScrollContainer } from '@grafana/ui/src/unstable';
import { PrometheusDatasource } from '../../datasource';
import { truncateResult } from '../../language_utils';
@@ -21,17 +21,20 @@ export function useSidecar_EXPERIMENTAL() {
throw new Error('No SidecarContext found');
}
const activePluginId = useObservable(service.activePluginIdObservable, service.activePluginId);
const initialContext = useObservable(service.initialContextObservable, service.initialContext);
const activePluginId = useObservable(service.activePluginIdObservable, service.activePluginId);
const locationService = service.getLocationService();
return {
activePluginId,
initialContext,
locationService,
// TODO: currently this allows anybody to open any app, in the future we should probably scope this to the
// current app but that means we will need to incorporate this better into the plugin platform APIs which
// we will do once the functionality is reasonably stable
openApp: (pluginId: string) => service.openApp(pluginId),
closeApp: (pluginId: string) => service.closeApp(pluginId),
openApp: (pluginId: string, context?: unknown) => service.openApp(pluginId, context),
openAppV2: (pluginId: string, path?: string) => service.openAppV2(pluginId, path),
closeApp: () => service.closeApp(),
isAppOpened: (pluginId: string) => service.isAppOpened(pluginId),
};
}
@@ -11,37 +11,51 @@ describe('SidecarService_EXPERIMENTAL', () => {
});
it('has the correct state after opening and closing an app', () => {
const sidecarService = new SidecarService_EXPERIMENTAL({});
const sidecarService = new SidecarService_EXPERIMENTAL();
sidecarService.openApp('pluginId', { filter: 'test' });
expect(sidecarService.activePluginId).toBe('pluginId');
expect(sidecarService.initialContext).toMatchObject({ filter: 'test' });
expect(sidecarService.getLocationService().getLocation().pathname).toBe('/a/pluginId');
sidecarService.closeApp('pluginId');
sidecarService.closeApp();
expect(sidecarService.activePluginId).toBe(undefined);
expect(sidecarService.initialContext).toBe(undefined);
expect(sidecarService.getLocationService().getLocation().pathname).toBe('/');
});
it('has the correct state after opening and closing an app v2', () => {
const sidecarService = new SidecarService_EXPERIMENTAL();
sidecarService.openAppV2('pluginId', '/test');
expect(sidecarService.activePluginId).toBe('pluginId');
expect(sidecarService.getLocationService().getLocation().pathname).toBe('/a/pluginId/test');
sidecarService.closeApp();
expect(sidecarService.activePluginId).toBe(undefined);
expect(sidecarService.initialContext).toBe(undefined);
expect(sidecarService.getLocationService().getLocation().pathname).toBe('/');
});
it('reports correct opened state', () => {
const sidecarService = new SidecarService_EXPERIMENTAL({});
const sidecarService = new SidecarService_EXPERIMENTAL();
expect(sidecarService.isAppOpened('pluginId')).toBe(false);
sidecarService.openApp('pluginId');
expect(sidecarService.isAppOpened('pluginId')).toBe(true);
sidecarService.closeApp('pluginId');
sidecarService.closeApp();
expect(sidecarService.isAppOpened('pluginId')).toBe(false);
});
it('does not close app that is not opened', () => {
const sidecarService = new SidecarService_EXPERIMENTAL({});
sidecarService.openApp('pluginId');
sidecarService.closeApp('foobar');
it('reports correct opened state v2', () => {
const sidecarService = new SidecarService_EXPERIMENTAL();
expect(sidecarService.isAppOpened('pluginId')).toBe(false);
sidecarService.openAppV2('pluginId');
expect(sidecarService.isAppOpened('pluginId')).toBe(true);
expect(sidecarService.activePluginId).toBe('pluginId');
sidecarService.closeApp();
expect(sidecarService.isAppOpened('pluginId')).toBe(false);
});
});
@@ -1,16 +1,14 @@
import { BehaviorSubject } from 'rxjs';
import * as H from 'history';
import { pick } from 'lodash';
import { BehaviorSubject, map, Observable } from 'rxjs';
import { config } from '../config';
import { locationService } from './LocationService';
interface Options {
localStorageKey?: string;
}
import { HistoryWrapper, LocationService, locationService } from './LocationService';
/**
* This is a service that handles state and operation of a sidecar feature (sideview to render a second app in grafana).
* At this moment this is highly experimental and if used should be understand to break easily with newer versions.
* At this moment this is highly experimental and if used should be understood to break easily with newer versions.
* None of this functionality works without a feature toggle `appSidecar` being enabled.
*
* Right now this being in a single service is more of a practical tradeoff for easier isolation in the future these
@@ -19,18 +17,16 @@ interface Options {
* @experimental
*/
export class SidecarService_EXPERIMENTAL {
private _activePluginId: BehaviorSubject<string | undefined>;
private _initialContext: BehaviorSubject<unknown | undefined>;
private localStorageKey: string | undefined;
private memoryLocationService: LocationService;
private history: LocationStorageHistory;
constructor(options: Options) {
this.localStorageKey = options.localStorageKey;
let initialId = undefined;
if (this.localStorageKey) {
initialId = localStorage.getItem(this.localStorageKey) || undefined;
}
this._activePluginId = new BehaviorSubject<string | undefined>(initialId);
constructor() {
this._initialContext = new BehaviorSubject<unknown | undefined>(undefined);
// We need a local ref for this so we can tap into the location changes and drive rerendering of components based
// on it without having a parent Router.
this.history = createLocationStorageHistory({ storageKey: 'grafana.sidecar.history' });
this.memoryLocationService = new HistoryWrapper(this.history);
}
private assertFeatureEnabled() {
@@ -50,7 +46,11 @@ export class SidecarService_EXPERIMENTAL {
* @experimental
*/
get activePluginIdObservable() {
return this._activePluginId.asObservable();
return this.history.getLocationObservable().pipe(
map((val) => {
return getPluginIdFromUrl(val?.pathname || '');
})
);
}
/**
@@ -74,39 +74,48 @@ export class SidecarService_EXPERIMENTAL {
* @experimental
*/
get activePluginId() {
return this._activePluginId.getValue();
return getPluginIdFromUrl(this.memoryLocationService.getLocation().pathname);
}
getLocationService() {
return this.memoryLocationService;
}
/**
* Opens an app in a sidecar. You can also pass some context object that will be then available to the app.
* @deprecated
* @experimental
*/
openApp(pluginId: string, context?: unknown) {
if (!this.assertFeatureEnabled()) {
return;
}
if (this.localStorageKey) {
localStorage.setItem(this.localStorageKey, pluginId);
this._initialContext.next(context);
this.memoryLocationService.push({ pathname: `/a/${pluginId}` });
}
/**
* Opens an app in a sidecar. You can also relative path inside the app to open.
* @experimental
*/
openAppV2(pluginId: string, path?: string) {
if (!this.assertFeatureEnabled()) {
return;
}
this._activePluginId.next(pluginId);
this._initialContext.next(context);
this.memoryLocationService.push({ pathname: `/a/${pluginId}${path || ''}` });
}
/**
* @experimental
*/
closeApp(pluginId: string) {
closeApp() {
if (!this.assertFeatureEnabled()) {
return;
}
if (this._activePluginId.getValue() === pluginId) {
if (this.localStorageKey) {
localStorage.removeItem(this.localStorageKey);
}
this._activePluginId.next(undefined);
this._initialContext.next(undefined);
}
this._initialContext.next(undefined);
this.memoryLocationService.replace({ pathname: '/' });
}
/**
@@ -130,23 +139,23 @@ export class SidecarService_EXPERIMENTAL {
return false;
}
return !!(
this._activePluginId.getValue() &&
(this._activePluginId.getValue() === pluginId || getMainAppPluginId() === pluginId)
);
return !!(this.activePluginId && (this.activePluginId === pluginId || getMainAppPluginId() === pluginId));
}
}
export const sidecarServiceSingleton_EXPERIMENTAL = new SidecarService_EXPERIMENTAL({
localStorageKey: 'grafana.sidecar.activePluginId',
});
const pluginIdUrlRegex = /a\/([^\/]+)/;
function getPluginIdFromUrl(url: string) {
return url.match(pluginIdUrlRegex)?.[1];
}
// The app plugin that is "open" in the main Grafana view
function getMainAppPluginId() {
// TODO: not great but we have to get a handle on the other locationService used for the main view and easiest way
// right now is through this global singleton
const { pathname } = locationService.getLocation();
// A naive way to sort of simulate core features being an app and having an appID
let mainApp = pathname.match(/\/a\/([^/]+)/)?.[1];
let mainApp = getPluginIdFromUrl(pathname);
if (!mainApp && pathname.match(/\/explore/)) {
mainApp = 'explore';
}
@@ -157,3 +166,88 @@ function getMainAppPluginId() {
return mainApp || 'unknown';
}
type LocalStorageHistoryOptions = {
storageKey: string;
};
interface LocationStorageHistory extends H.MemoryHistory {
getLocationObservable(): Observable<H.Location | undefined>;
}
/**
* Simple wrapper over the memory history that persists the location in the localStorage.
*
* @param options
*/
function createLocationStorageHistory(options: LocalStorageHistoryOptions): LocationStorageHistory {
const storedLocation = localStorage.getItem(options.storageKey);
const initialEntry = storedLocation ? JSON.parse(storedLocation) : '/';
const locationSubject = new BehaviorSubject<H.Location | undefined>(initialEntry);
const memoryHistory = H.createMemoryHistory({ initialEntries: [initialEntry] });
let currentLocation = memoryHistory.location;
function maybeUpdateLocation() {
if (memoryHistory.location !== currentLocation) {
localStorage.setItem(
options.storageKey,
JSON.stringify(pick(memoryHistory.location, 'pathname', 'search', 'hash'))
);
currentLocation = memoryHistory.location;
locationSubject.next(memoryHistory.location);
}
}
// This creates a sort of proxy over the memory location just to add the localStorage persistence and the location
// observer. We could achieve the same effect by a listener but that would create a memory leak as there would be no
// reasonable way to unsubcribe the listener later on.
// Another issue is that react router for some reason does not care about proper `this` binding and just calls these
// as normal functions. So if this were to be a class we would still need to bind each of these methods to the
// instance so at that moment this just seems easier.
return {
...memoryHistory,
// Getter aren't destructured as getter but as values, so they have to be still here even though we are not
// modifying them.
get index() {
return memoryHistory.index;
},
get entries() {
return memoryHistory.entries;
},
get length() {
return memoryHistory.length;
},
get action() {
return memoryHistory.action;
},
get location() {
return memoryHistory.location;
},
push(location: H.Path | H.LocationDescriptor<H.LocationState>, state?: H.LocationState) {
memoryHistory.push(location, state);
maybeUpdateLocation();
},
replace(location: H.Path | H.LocationDescriptor<H.LocationState>, state?: H.LocationState) {
memoryHistory.replace(location, state);
maybeUpdateLocation();
},
go(n: number) {
memoryHistory.go(n);
maybeUpdateLocation();
},
goBack() {
memoryHistory.goBack();
maybeUpdateLocation();
},
goForward() {
memoryHistory.goForward();
maybeUpdateLocation();
},
getLocationObservable() {
return locationSubject.asObservable();
},
};
}
export const sidecarServiceSingleton_EXPERIMENTAL = new SidecarService_EXPERIMENTAL();
@@ -21,6 +21,11 @@ export interface ArcOption {
field?: string;
}
export enum ZoomMode {
Cooperative = 'cooperative',
Greedy = 'greedy',
}
export interface Options {
edges?: {
/**
@@ -46,4 +51,8 @@ export interface Options {
*/
arcs?: Array<ArcOption>;
};
/**
* How to handle zoom/scroll events in the node graph
*/
zoomMode?: ZoomMode;
}
@@ -12,66 +12,85 @@ import * as common from '@grafana/schema';
export const pluginVersion = "11.4.0-pre";
/**
* Auto is "table" in the UI
*/
export enum PointShape {
Circle = 'circle',
Square = 'square',
}
export enum SeriesMapping {
Auto = 'auto',
Manual = 'manual',
}
export enum ScatterShow {
export enum XYShowMode {
Lines = 'lines',
Points = 'points',
PointsAndLines = 'points+lines',
}
/**
* Configuration for the Table/Auto mode
* NOTE: (copied from dashboard_kind.cue, since not exported)
* Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
* It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.
*/
export interface XYDimensionConfig {
exclude?: Array<string>;
frame: number;
x?: string;
export interface MatcherConfig {
/**
* The matcher id. This is used to find the matcher implementation from registry.
*/
id: string;
/**
* The matcher options. This is specific to the matcher implementation.
*/
options?: unknown;
}
export const defaultXYDimensionConfig: Partial<XYDimensionConfig> = {
exclude: [],
export const defaultMatcherConfig: Partial<MatcherConfig> = {
id: '',
};
export interface FieldConfig extends common.HideableFieldConfig, common.AxisConfig {
label?: common.VisibilityMode;
labelValue?: common.TextDimensionConfig;
lineColor?: common.ColorDimensionConfig;
fillOpacity?: number;
lineStyle?: common.LineStyle;
lineWidth?: number;
pointColor?: common.ColorDimensionConfig;
pointSize?: common.ScaleDimensionConfig;
show?: ScatterShow;
pointShape?: PointShape;
pointSize?: {
fixed?: number;
min?: number;
max?: number;
};
pointStrokeWidth?: number;
show?: XYShowMode;
}
export const defaultFieldConfig: Partial<FieldConfig> = {
label: common.VisibilityMode.Auto,
show: ScatterShow.Points,
fillOpacity: 50,
show: XYShowMode.Points,
};
export interface ScatterSeriesConfig extends FieldConfig {
frame?: number;
name?: string;
x?: string;
y?: string;
export interface XYSeriesConfig {
color?: {
matcher: MatcherConfig;
};
frame?: {
matcher: MatcherConfig;
};
name?: {
fixed?: string;
};
size?: {
matcher: MatcherConfig;
};
x?: {
matcher: MatcherConfig;
};
y?: {
matcher: MatcherConfig;
};
}
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
/**
* Table Mode (auto)
*/
dims: XYDimensionConfig;
/**
* Manual Mode
*/
series: Array<ScatterSeriesConfig>;
seriesMapping?: SeriesMapping;
mapping: SeriesMapping;
series: Array<XYSeriesConfig>;
}
export const defaultOptions: Partial<Options> = {
@@ -14,11 +14,11 @@ import { QueryHeader, QueryHeaderProps } from './QueryHeader';
import { RawEditor } from './query-editor-raw/RawEditor';
import { VisualEditor } from './visual-query-builder/VisualEditor';
interface SqlQueryEditorProps extends QueryEditorProps<SqlDatasource, SQLQuery, SQLOptions> {
export interface SqlQueryEditorProps extends QueryEditorProps<SqlDatasource, SQLQuery, SQLOptions> {
queryHeaderProps?: Pick<QueryHeaderProps, 'dialect'>;
}
export function SqlQueryEditor({
export default function SqlQueryEditor({
datasource,
query,
onChange,
@@ -0,0 +1,27 @@
import { css } from '@emotion/css';
import { lazy, Suspense } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import type { SqlQueryEditorProps } from './QueryEditor';
const QueryEditor = lazy(() => import(/* webpackChunkName: "sql-query-editor" */ './QueryEditor'));
export function SqlQueryEditorLazy(props: SqlQueryEditorProps) {
const styles = useStyles2(getStyles);
return (
<Suspense fallback={<LoadingPlaceholder text={'Loading editor'} className={styles.container} />}>
<QueryEditor {...props} />
</Suspense>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
marginBottom: 'unset',
marginLeft: theme.spacing(1),
}),
};
};
@@ -30,7 +30,7 @@ import {
} from '@grafana/runtime';
import { ResponseParser } from '../ResponseParser';
import { SqlQueryEditor } from '../components/QueryEditor';
import { SqlQueryEditorLazy } from '../components/QueryEditorLazy';
import { MACRO_NAMES } from '../constants';
import { DB, SQLQuery, SQLOptions, SqlQueryModel, QueryFormat } from '../types';
import migrateAnnotation from '../utils/migration';
@@ -63,7 +63,7 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
this.preconfiguredDatabase = settingsData.database ?? '';
this.annotations = {
prepareAnnotation: migrateAnnotation,
QueryEditor: SqlQueryEditor,
QueryEditor: SqlQueryEditorLazy,
};
}
+1 -1
View File
@@ -17,7 +17,7 @@ export { ConnectionLimits } from './components/configuration/ConnectionLimits';
export { Divider } from './components/configuration/Divider';
export { TLSSecretsConfig } from './components/configuration/TLSSecretsConfig';
export { useMigrateDatabaseFields } from './components/configuration/useMigrateDatabaseFields';
export { SqlQueryEditor } from './components/QueryEditor';
export { SqlQueryEditorLazy } from './components/QueryEditorLazy';
export type { QueryHeaderProps } from './components/QueryHeader';
export { createSelectClause, haveColumns } from './utils/sql.utils';
export { applyQueryDefaults } from './defaults';
+3 -3
View File
@@ -50,7 +50,7 @@
"@emotion/css": "11.13.4",
"@emotion/react": "11.13.3",
"@emotion/serialize": "1.3.2",
"@floating-ui/react": "0.26.27",
"@floating-ui/react": "0.26.28",
"@grafana/data": "11.4.0-pre",
"@grafana/e2e-selectors": "11.4.0-pre",
"@grafana/faro-web-sdk": "^1.3.6",
@@ -84,12 +84,12 @@
"monaco-editor": "0.34.1",
"ol": "7.4.0",
"prismjs": "1.29.0",
"rc-cascader": "3.28.1",
"rc-cascader": "3.30.0",
"rc-drawer": "7.2.0",
"rc-slider": "11.1.7",
"rc-time-picker": "^3.7.3",
"rc-tooltip": "6.2.1",
"react-calendar": "5.0.0",
"react-calendar": "5.1.0",
"react-colorful": "5.6.1",
"react-custom-scrollbars-2": "4.5.0",
"react-dropzone": "14.2.10",
@@ -0,0 +1,101 @@
import { Meta, Preview, ArgTypes } from '@storybook/blocks';
import { Combobox } from './Combobox';
<Meta title="MDX|Combobox" component={Combobox} />
## Usage
**Do**
- Use in inline query editors
- Use when you require async calls from a select input
**Don't**
- Use the async functionality, when all items are only loaded on the initial load
- Use when fewer than 4 items are needed, as a `RadioButtonGroup` may be more suitable (not for inline use cases)
- Use this component if you need custom option styling
## ComboboxOption
The `ComboboxOption` currently supports 3 properties:
- `label` - The text that is visible in the menu.
- `value` (required) - The value that is selected.
- `description` - A longer description that describes the choice.
If no `label` is given, `value` will be used as a display value.
## Sizing
The recommended way to set the width is by sizing the container element. This is so it may reflect a similar size as other inputs in the context.
If that is not possible, the width can be set directly on the component, by setting a number, which is a multiple of `8px`.
For inline usage, such as in query editors, it may be useful to size the input based on the content. Set `width="auto"` to achieve this. In this case, it is also recommended to set `maxWidth` and `minWidth`.
## Async Usage
The `options` prop can accept an async function:
- When the menu opens, the `options` function is called with `''`, to load all options.
- When the user types, the `options` function is called with the current input value.
Note: The calls are debounced. Old calls are invalidated when a new call is made.
## Unit testing
Writing unit tests with Combobox requires mocking the `getBoundingClientRect` method because of [the virtual list library](https://github.com/TanStack/virtual/issues/29#issuecomment-657519522)
This code sets up the mocking before all tests:
```js
beforeAll(() => {
const mockGetBoundingClientRect = jest.fn(() => ({
width: 120,
height: 120,
top: 0,
left: 0,
bottom: 0,
right: 0,
}));
Object.defineProperty(Element.prototype, 'getBoundingClientRect', {
value: mockGetBoundingClientRect,
});
});
```
### Selecting an option
To select an option, you can use any `*ByRole` methods, as Combobox has proper roles for accessibility.
#### Selecting option by clicking
```js
render(<Combobox options={options} onChange={onChangeHandler} value={null} />);
const input = screen.getByRole('combobox');
await userEvent.click(input);
const item = await screen.findByRole('option', { name: 'Option 1' });
await userEvent.click(item);
expect(screen.getByDisplayValue('Option 1')).toBeInTheDocument();
```
#### Selecting option by typing
```js
render(<Combobox options={options} value={null} onChange={onChangeHandler} />);
const input = screen.getByRole('combobox');
await userEvent.type(input, 'Option 3');
await userEvent.keyboard('{ArrowDown}{Enter}');
expect(screen.getByDisplayValue('Option 3')).toBeInTheDocument();
```
## Props
<ArgTypes of={Combobox} />
@@ -11,12 +11,18 @@ import { Field } from '../Forms/Field';
import { AsyncSelect, Select } from '../Select/Select';
import { Combobox, ComboboxOption } from './Combobox';
import mdx from './Combobox.mdx';
type PropsAndCustomArgs = ComponentProps<typeof Combobox> & { numberOfOptions: number };
const meta: Meta<PropsAndCustomArgs> = {
title: 'Forms/Combobox',
component: Combobox,
parameters: {
docs: {
page: mdx,
},
},
args: {
loading: undefined,
invalid: undefined,
@@ -166,7 +166,7 @@ describe('Combobox', () => {
expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ value: 'custom value' }));
});
it('should proivde custom string when all options are numbers', async () => {
it('should provide custom string when all options are numbers', async () => {
const options = [
{ label: '1', value: 1 },
{ label: '2', value: 2 },
@@ -333,7 +333,7 @@ describe('Combobox', () => {
jest.advanceTimersByTime(500); // Custom value while typing
});
const customItem = screen.queryByRole('option', { name: 'fir Create custom value' });
const customItem = screen.queryByRole('option', { name: 'Custom value: fir' });
expect(customItem).toBeInTheDocument();
});
@@ -5,6 +5,7 @@ import { debounce } from 'lodash';
import { ReactNode, useCallback, useId, useMemo, useState } from 'react';
import { useStyles2 } from '../../themes';
import { logOptions } from '../../utils';
import { t, Trans } from '../../utils/i18n';
import { Icon } from '../Icon/Icon';
import { AutoSizeInput } from '../Input/AutoSizeInput';
@@ -13,8 +14,8 @@ import { Box } from '../Layout/Box/Box';
import { Stack } from '../Layout/Stack/Stack';
import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
import { getComboboxStyles } from './getComboboxStyles';
import { useComboboxFloat, OPTION_HEIGHT } from './useComboboxFloat';
import { getComboboxStyles, MENU_OPTION_HEIGHT } from './getComboboxStyles';
import { useComboboxFloat } from './useComboboxFloat';
import { StaleResultError, useLatestAsyncCall } from './useLatestAsyncCall';
export type ComboboxOption<T extends string | number = string> = {
@@ -27,7 +28,13 @@ export type ComboboxOption<T extends string | number = string> = {
// then the onChange handler emits ComboboxOption with the label as non-undefined.
interface ComboboxBaseProps<T extends string | number>
extends Omit<InputProps, 'prefix' | 'suffix' | 'value' | 'addonBefore' | 'addonAfter' | 'onChange' | 'width'> {
/**
* An `X` appears in the UI, which clears the input and sets the value to `null`. Do not use if you have no `null` case.
*/
isClearable?: boolean;
/**
* Allows the user to set a value which is not in the list of options.
*/
createCustomValue?: boolean;
options: Array<ComboboxOption<T>> | ((inputValue: string) => Promise<Array<ComboboxOption<T>>>);
onChange: (option: ComboboxOption<T> | null) => void;
@@ -42,10 +49,18 @@ interface ComboboxBaseProps<T extends string | number>
width?: number | 'auto';
}
const RECOMMENDED_ITEMS_AMOUNT = 100_000;
type AutoSizeConditionals =
| {
width: 'auto';
/**
* Needs to be set when width is 'auto' to prevent the input from shrinking too much
*/
minWidth: number;
/**
* Recommended to set when width is 'auto' to prevent the input from growing too much.
*/
maxWidth?: number;
}
| {
@@ -57,6 +72,9 @@ type AutoSizeConditionals =
type ComboboxProps<T extends string | number> = ComboboxBaseProps<T> & AutoSizeConditionals;
function itemToString<T extends string | number>(item: ComboboxOption<T> | null) {
if (item?.label?.includes('Custom value: ')) {
return item?.value.toString();
}
return item?.label ?? item?.value.toString() ?? '';
}
@@ -108,15 +126,20 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
const setItems = useCallback(
(items: Array<ComboboxOption<T>>, inputValue: string | undefined) => {
let itemsToSet = items;
logOptions(itemsToSet.length, RECOMMENDED_ITEMS_AMOUNT, id, ariaLabelledBy);
if (inputValue && createCustomValue) {
const optionMatchingInput = items.find((opt) => opt.label === inputValue || opt.value === inputValue);
const optionMatchingInput = items.find(
(opt) => opt.label === 'Custom value: ' + inputValue || opt.value === inputValue
);
if (!optionMatchingInput) {
const customValueOption = {
label: t('combobox.custom-value.label', 'Custom value: ') + inputValue,
// Type casting needed to make this work when T is a number
value: inputValue as unknown as T,
/* TODO: Add this back when we do support descriptions and have need for it
description: t('combobox.custom-value.create', 'Create custom value'),
*/
};
itemsToSet = items.slice(0);
@@ -126,7 +149,7 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
baseSetItems(itemsToSet);
},
[createCustomValue]
[createCustomValue, id, ariaLabelledBy]
);
const selectedItemIndex = useMemo(() => {
@@ -162,7 +185,7 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
const virtualizerOptions = {
count: items.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => OPTION_HEIGHT,
estimateSize: () => MENU_OPTION_HEIGHT,
overscan: 4,
};
@@ -195,8 +218,6 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
getMenuProps,
getItemProps,
openMenu,
closeMenu,
selectItem,
} = useCombobox({
menuId,
@@ -307,10 +328,6 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
const { inputRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, rowVirtualizer.range, isOpen);
const handleSuffixClick = useCallback(() => {
isOpen ? closeMenu() : openMenu();
}, [isOpen, openMenu, closeMenu]);
const InputComponent = width === 'auto' ? AutoSizeInput : Input;
const suffixIcon = asyncLoading
@@ -326,6 +343,7 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
<div>
<InputComponent
width={width === 'auto' ? undefined : width}
className={styles.input}
suffix={
<>
{!!value && value === selectedItem?.value && isClearable && (
@@ -346,11 +364,7 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
/>
)}
{/* When you click the input, it should just focus the text box. However, clicks on input suffix arent
translated to the input, so it blocks the input from being focused. So we need an additional event
handler here to open/close the menu. It should not have button role because we intentionally don't
want it in the a11y tree. */}
<Icon name={suffixIcon} onClick={handleSuffixClick} />
<Icon name={suffixIcon} />
</>
}
{...restProps}
@@ -6,7 +6,12 @@ import { GrafanaTheme2 } from '@grafana/data';
// This should be in sync with the body font size in the theme.
export const MENU_ITEM_FONT_SIZE = 14;
export const MENU_ITEM_FONT_WEIGHT = 500;
export const MENU_ITEM_PADDING_X = 8;
export const MENU_ITEM_PADDING = 8;
export const MENU_ITEM_LINE_HEIGHT = 1.5;
// Used with Downshift to get the height of each item
export const MENU_OPTION_HEIGHT = MENU_ITEM_PADDING * 2 + MENU_ITEM_FONT_SIZE * MENU_ITEM_LINE_HEIGHT;
export const POPOVER_MAX_HEIGHT = MENU_OPTION_HEIGHT * 8.5;
export const getComboboxStyles = (theme: GrafanaTheme2) => {
return {
@@ -27,7 +32,7 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
}),
option: css({
label: 'grafana-select-option',
padding: MENU_ITEM_PADDING_X,
padding: MENU_ITEM_PADDING,
position: 'absolute',
display: 'flex',
alignItems: 'center',
@@ -65,7 +70,7 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
fontWeight: 'normal',
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.secondary,
lineHeight: theme.typography.body.lineHeight,
lineHeight: MENU_ITEM_LINE_HEIGHT,
textOverflow: 'ellipsis',
overflow: 'hidden',
}),
@@ -84,7 +89,7 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
borderRadius: theme.shape.radius.default,
content: '" "',
display: 'block',
height: '100%',
height: MENU_OPTION_HEIGHT,
position: 'absolute',
width: theme.spacing(0.5),
left: 0,
@@ -94,6 +99,7 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
clear: css({
label: 'grafana-select-clear',
cursor: 'pointer',
pointerEvents: 'auto',
'&:hover': {
color: theme.colors.text.primary,
},
@@ -102,5 +108,11 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
label: 'grafana-select-warning-icon',
color: theme.colors.text.secondary,
}),
input: css({
label: 'grafana-select-input',
'> div > div:last-child': {
pointerEvents: 'none',
},
}),
};
};
@@ -4,15 +4,17 @@ import { useMemo, useRef, useState } from 'react';
import { measureText } from '../../utils';
import { ComboboxOption } from './Combobox';
import { MENU_ITEM_FONT_SIZE, MENU_ITEM_FONT_WEIGHT, MENU_ITEM_PADDING_X } from './getComboboxStyles';
import {
MENU_ITEM_FONT_SIZE,
MENU_ITEM_FONT_WEIGHT,
MENU_ITEM_PADDING,
MENU_OPTION_HEIGHT,
POPOVER_MAX_HEIGHT,
} from './getComboboxStyles';
// Only consider the first n items when calculating the width of the popover.
const WIDTH_CALCULATION_LIMIT_ITEMS = 100_000;
// Used with Downshift to get the height of each item
export const OPTION_HEIGHT = 45;
const POPOVER_MAX_HEIGHT = OPTION_HEIGHT * 8.5;
// Clearance around the popover to prevent it from being too close to the edge of the viewport
const POPOVER_PADDING = 16;
@@ -41,7 +43,7 @@ export const useComboboxFloat = (
const preferredMaxHeight = availableHeight - POPOVER_PADDING;
const width = Math.max(preferredMaxWidth, 0);
const height = Math.min(Math.max(preferredMaxHeight, OPTION_HEIGHT * 6), POPOVER_MAX_HEIGHT);
const height = Math.min(Math.max(preferredMaxHeight, MENU_OPTION_HEIGHT * 6), POPOVER_MAX_HEIGHT);
setPopoverMaxSize({ width, height });
},
@@ -68,7 +70,7 @@ export const useComboboxFloat = (
const size = measureText(longestItem, MENU_ITEM_FONT_SIZE, MENU_ITEM_FONT_WEIGHT).width;
return size + MENU_ITEM_PADDING_X * 2 + scrollbarWidth;
return size + MENU_ITEM_PADDING * 2 + scrollbarWidth;
}, [items, scrollbarWidth]);
const floatStyles = {
@@ -32,6 +32,7 @@ interface Props {
/**
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
* @deprecated Use `ScrollContainer` from `@grafana/ui` instead. It uses native scrollbars and has a simpler API.
*/
export const CustomScrollbar = ({
autoHide = false,
@@ -1,7 +1,6 @@
import { css } from '@emotion/css';
import { useCallback } from 'react';
import Calendar from 'react-calendar';
import { CalendarType } from 'react-calendar/dist/cjs/shared/types';
import Calendar, { CalendarType } from 'react-calendar';
import { GrafanaTheme2, dateTimeParse, DateTime, TimeZone } from '@grafana/data';
@@ -58,6 +58,23 @@ describe('AutoSizeInput', () => {
expect(getComputedStyle(inputWrapper).width).toBe('304px');
});
it('should use placeholder for width if input is empty', () => {
render(<AutoSizeInput placeholder="very very long value" />);
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
expect(getComputedStyle(inputWrapper).width).toBe('304px');
});
it('should use value for width even with a placeholder', () => {
render(<AutoSizeInput value="less long value" placeholder="very very long value" />);
const input: HTMLInputElement = screen.getByTestId('autosize-input');
const inputWrapper: HTMLDivElement = screen.getByTestId('input-wrapper');
fireEvent.change(input, { target: { value: 'very very long value' } });
expect(getComputedStyle(inputWrapper).width).toBe('304px');
});
it('should call onBlur if set when blurring', () => {
const onBlur = jest.fn();
const onCommitChange = jest.fn();
@@ -24,6 +24,7 @@ export const AutoSizeInput = React.forwardRef<HTMLInputElement, Props>((props, r
onKeyDown,
onBlur,
value: controlledValue,
placeholder,
...restProps
} = props;
// Initialize internal state
@@ -36,13 +37,16 @@ export const AutoSizeInput = React.forwardRef<HTMLInputElement, Props>((props, r
// Update input width when `value`, `minWidth`, or `maxWidth` change
const inputWidth = useMemo(() => {
const valueString = typeof value === 'string' ? value : value.toString();
const displayValue = value || placeholder || '';
const valueString = typeof displayValue === 'string' ? displayValue : displayValue.toString();
return getWidthFor(valueString, minWidth, maxWidth);
}, [value, minWidth, maxWidth]);
}, [placeholder, value, minWidth, maxWidth]);
return (
<Input
{...restProps}
placeholder={placeholder}
ref={ref}
value={value.toString()}
onChange={(event) => {
@@ -0,0 +1,65 @@
import { Meta, Preview, ArgTypes } from '@storybook/blocks';
import { ScrollContainer } from './ScrollContainer';
<Meta title="MDX|ScrollContainer" component={ScrollContainer} />
# ScrollContainer
This component is used to create a scrollable container. It uses native scrollbars, has an option to show scroll indicators, and supports most `Box` properties.
## Migrating from CustomScrollbar
ScrollContainer is intended to be a functionally equivalent replacement for CustomScrollbar. If you're not using any additional props in CustomScrollbar, it should be a drop-in replacement.
If you were using additional props, there are roughly 3 categories of changes:
1. Props that have been **renamed**
2. Props that have been removed but the **same behavior can be achieved in a different way**
3. Props that have been removed and are **no longer supported**
### Props that have been renamed
- `autoHeightMin` is now `minHeight`
- `autoHeightMax` is now `maxHeight`
- `divId` is now `id`
- `testId` is now `data-testid`
- `scrollRefCallback` is now `ref`
### Props that have been removed but the same behavior can be achieved in a different way
- `hideHorizontalTrack` and `hideVerticalTrack` have been removed.
- You can achieve the same behavior by setting either `overflowX` or `overflowY` to `hidden`. These names more closely match the CSS properties they represent.
- `scrollTop` and `setScrollTop`.
- You can achieve the same behavior by using the `ref` prop to get a reference to the `ScrollContainer` component, and then calling `scrollTo` on that reference.
- Before:
```tsx
const [scrollTop, setScrollTop] = useState(0);
return <CustomScrollbar scrollTop={scrollTop}>// Your amazing scrolling content</CustomScrollbar>;
```
- After:
```tsx
const [scrollTop, setScrollTop] = useState(0);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
scrollRef.current?.scrollTo(0, scrollTop);
}, [scrollTop]);
return <ScrollContainer ref={scrollRef}>// Your amazing scrolling content</ScrollContainer>;
```
### Props that have been removed and are no longer supported
- `autoHide`, `autoHideTimeout` and `hideTracksWhenNotNeeded`.
- These props no longer make sense when using native scrollbars, since some operating systems (Windows for example) don't have overlay scrollbars.
- `className`
- Like our other design system components, the intention is that you'll never need to write custom CSS for this component. We extend all the base `Box` props, so everything _should_ be available as a prop.
- `updateAfterMountMs`
- This wasn't very react-y, and could cause inconsistent behaviour.
<ArgTypes of={ScrollContainer} />
@@ -0,0 +1,93 @@
import { action } from '@storybook/addon-actions';
import { Meta, StoryFn } from '@storybook/react';
import { ScrollContainer } from './ScrollContainer';
import mdx from './ScrollContainer.mdx';
const meta: Meta<typeof ScrollContainer> = {
title: 'General/Layout/ScrollContainer',
component: ScrollContainer,
parameters: {
controls: {
exclude: ['onScroll'],
},
docs: {
page: mdx,
},
},
argTypes: {},
args: {
onScroll: action('onScroll'),
height: 50,
overflowX: 'auto',
overflowY: 'auto',
scrollbarWidth: 'thin',
showScrollIndicators: true,
width: 50,
},
};
export const Basic: StoryFn<typeof ScrollContainer> = (args) => {
return (
<ScrollContainer {...args} borderColor="info" borderStyle="dashed" padding={1}>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
</ScrollContainer>
);
};
export default meta;
@@ -9,7 +9,7 @@ import { Box, BoxProps } from '../Layout/Box/Box';
import { ScrollIndicators } from './ScrollIndicators';
interface Props extends Omit<BoxProps, 'display' | 'direction' | 'flex' | 'position'> {
interface Props extends Omit<BoxProps, 'display' | 'direction' | 'element' | 'flex' | 'position'> {
showScrollIndicators?: boolean;
onScroll?: UIEventHandler<HTMLDivElement>;
overflowX?: Property.OverflowX;
@@ -6,6 +6,7 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { ComponentSize } from '../../types';
import { DragHandlePosition, getDragStyles } from '../DragHandle/DragHandle';
export interface UseSplitterOptions {
@@ -21,6 +22,10 @@ export interface UseSplitterOptions {
*/
onSizeChanged?: (flexSize: number, pixelSize: number) => void;
onResizing?: (flexSize: number, pixelSize: number) => void;
// Size of the region left of the handle indicator that is responsive to dragging. At the same time acts as a margin
// pushing the left pane content left.
handleSize?: ComponentSize;
}
const PIXELS_PER_MS = 0.3 as const;
@@ -45,7 +50,7 @@ const propsForDirection = {
export function useSplitter(options: UseSplitterOptions) {
const { direction, initialSize = 0.5, dragPosition = 'middle', onResizing, onSizeChanged } = options;
const handleSize = 16;
const handleSize = getPixelSize(options.handleSize);
const splitterRef = useRef<HTMLDivElement | null>(null);
const firstPaneRef = useRef<HTMLDivElement | null>(null);
const secondPaneRef = useRef<HTMLDivElement | null>(null);
@@ -413,3 +418,12 @@ function getStyles(theme: GrafanaTheme2, direction: UseSplitterOptions['directio
panel: css({ display: 'flex', position: 'relative', flexBasis: 0 }),
};
}
function getPixelSize(size: ComponentSize = 'md') {
return {
xs: 4,
sm: 8,
md: 16,
lg: 32,
}[size];
}
@@ -217,6 +217,7 @@ export { Box } from './Layout/Box/Box';
export { Stack } from './Layout/Stack/Stack';
export { Grid } from './Layout/Grid/Grid';
export { Space } from './Layout/Space';
export { ScrollContainer } from './ScrollContainer/ScrollContainer';
export { Label } from './Forms/Label';
export { Field, type FieldProps } from './Forms/Field';
-1
View File
@@ -10,4 +10,3 @@
*/
export * from './utils/skeleton';
export * from './components/ScrollContainer/ScrollContainer';
+1
View File
@@ -18,5 +18,6 @@ export { createLogger } from './logger';
export { attachDebugger } from './debug';
export * from './nodeGraph';
export { fuzzyMatch } from './fuzzy';
export { logOptions } from './logOptions';
export { ReactUtils };
@@ -0,0 +1,30 @@
import { logOptions } from './logOptions';
const RECOMMENDED_AMOUNT = 10;
describe('logOptions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should not log anything if amount is less than or equal to recommendedAmount', () => {
console.warn = jest.fn();
logOptions(5, RECOMMENDED_AMOUNT, 'test-id', 'test-aria');
expect(console.warn).not.toHaveBeenCalled();
});
it('should log a warning if amount exceeds recommendedAmount', () => {
console.warn = jest.fn();
logOptions(15, RECOMMENDED_AMOUNT, 'test-id', 'test-aria');
expect(console.warn).toHaveBeenCalledWith('[Combobox] Items exceed the recommended amount 10.', {
itemsCount: '15',
recommendedAmount: '10',
'aria-labelledby': 'test-aria',
id: 'test-id',
});
});
});
@@ -0,0 +1,23 @@
/**
* This function logs a warning if the amount of items exceeds the recommended amount.
*
* @param amount
* @param id
* @param ariaLabelledBy
*/
export function logOptions(
amount: number,
recommendedAmount: number,
id: string | undefined,
ariaLabelledBy: string | undefined
): void {
if (amount > recommendedAmount) {
const msg = `[Combobox] Items exceed the recommended amount ${recommendedAmount}.`;
console.warn(msg, {
itemsCount: '' + amount,
recommendedAmount: '' + recommendedAmount,
'aria-labelledby': ariaLabelledBy ?? '',
id: id ?? '',
});
}
}
+1
View File
@@ -50,6 +50,7 @@ require (
github.com/go-openapi/swag v0.23.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect
+2 -2
View File
@@ -93,8 +93,8 @@ github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+6
View File
@@ -78,6 +78,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/logout", hs.Logout)
r.Post("/login", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPost))
r.Get("/login/:name", quota(string(auth.QuotaTargetSrv)), hs.OAuthLogin)
r.Get("/login", hs.LoginView)
r.Get("/invite/:code", hs.Index)
@@ -207,6 +208,11 @@ func (hs *HTTPServer) registerRoutes() {
r.Post("/api/user/email/start-verify", reqSignedInNoAnonymous, routing.Wrap(hs.StartEmailVerificaton))
}
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabledGlobally(featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
r.Post("/api/login/passwordless/start", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), hs.StartPasswordless)
r.Post("/api/login/passwordless/authenticate", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPasswordless))
}
// invited
r.Get("/api/user/invite/:code", routing.Wrap(hs.GetInviteInfoByCode))
r.Post("/api/user/invite/complete", routing.Wrap(hs.CompleteInvite))
+2
View File
@@ -33,6 +33,7 @@ type FrontendSettingsAuthDTO struct {
DisableLogin bool `json:"disableLogin"`
BasicAuthStrongPasswordPolicy bool `json:"basicAuthStrongPasswordPolicy"`
PasswordlessEnabled bool `json:"passwordlessEnabled"`
}
type FrontendSettingsBuildInfoDTO struct {
@@ -253,6 +254,7 @@ type FrontendSettingsDTO struct {
TokenExpirationDayLimit int `json:"tokenExpirationDayLimit"`
SharedWithMeFolderUID string `json:"sharedWithMeFolderUID"`
RootFolderUID string `json:"rootFolderUID"`
PasswordlessEnabled string `json:"passwordlessEnabled"`
GeomapDefaultBaseLayerConfig *map[string]any `json:"geomapDefaultBaseLayerConfig,omitempty"`
GeomapDisableCustomBaseLayer bool `json:"geomapDisableCustomBaseLayer"`
+1
View File
@@ -360,6 +360,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
OktaSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.OktaProviderName]),
DisableLogin: hs.Cfg.DisableLogin,
BasicAuthStrongPasswordPolicy: hs.Cfg.BasicAuthStrongPasswordPolicy,
PasswordlessEnabled: hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication),
}
if hs.pluginsCDNService != nil && hs.pluginsCDNService.IsEnabled() {
+20
View File
@@ -241,6 +241,26 @@ func (hs *HTTPServer) LoginPost(c *contextmodel.ReqContext) response.Response {
return authn.HandleLoginResponse(c.Req, c.Resp, hs.Cfg, identity, hs.ValidateRedirectTo, hs.Features)
}
func (hs *HTTPServer) LoginPasswordless(c *contextmodel.ReqContext) response.Response {
identity, err := hs.authnService.Login(c.Req.Context(), authn.ClientPasswordless, &authn.Request{HTTPRequest: c.Req})
if err != nil {
tokenErr := &auth.CreateTokenErr{}
if errors.As(err, &tokenErr) {
return response.Error(tokenErr.StatusCode, tokenErr.ExternalErr, tokenErr.InternalErr)
}
return response.Err(err)
}
return authn.HandleLoginResponse(c.Req, c.Resp, hs.Cfg, identity, hs.ValidateRedirectTo, hs.Features)
}
func (hs *HTTPServer) StartPasswordless(c *contextmodel.ReqContext) {
redirect, err := hs.authnService.RedirectURL(c.Req.Context(), authn.ClientPasswordless, &authn.Request{HTTPRequest: c.Req})
if err != nil {
c.Redirect(hs.redirectURLWithErrorCookie(c, err))
}
c.JSON(http.StatusOK, redirect)
}
func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqContext) error {
if user == nil {
return errors.New("could not login user")
+1
View File
@@ -36,6 +36,7 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
+2 -2
View File
@@ -52,8 +52,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -186,8 +186,9 @@ func (l *logWrapper) Level() sdklog.Level {
}
func (l *logWrapper) With(args ...any) sdklog.Logger {
l.logger = l.logger.New(args...)
return l
return &logWrapper{
logger: l.logger.New(args...),
}
}
func (l *logWrapper) FromContext(ctx context.Context) sdklog.Logger {
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
@@ -62,3 +63,14 @@ func TestNewPlugin(t *testing.T) {
})
}
}
func TestLogger(t *testing.T) {
t.Run("logger.With should create a new logger", func(t *testing.T) {
wrapper := &logWrapper{
logger: log.New("test"),
}
newLogger := wrapper.With("key", "value")
require.NotSame(t, newLogger.(*logWrapper).logger, wrapper.logger, "`With` should not return the same instance")
})
}
@@ -17,7 +17,7 @@ func teamMembershipCollector(store db.DB) legacyTupleCollector {
FROM team_member tm
INNER JOIN team t ON tm.team_id = t.id
INNER JOIN ` + store.GetDialect().Quote("user") + ` u ON tm.user_id = u.id
WHERE org_id = ?
WHERE t.org_id = ?
`
type membership struct {
@@ -109,7 +109,7 @@ func folderTreeCollector(store db.DB) legacyTupleCollector {
}
}
// managedPermissionsCollector collects managed permissions into provided tuple map.
// managedPermissionsCollector collects managed permissions.
// It will only store actions that are supported by our schema. Managed permissions can
// be directly mapped to user/team/role without having to write an intermediate role.
func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector {
@@ -195,6 +195,99 @@ func tupleStringWithoutCondition(tuple *openfgav1.TupleKey) string {
return s
}
// basicRolePermissionsCollector collects permissions for basic roles
func basicRolePermissionsCollector(store db.DB) legacyTupleCollector {
return func(ctx context.Context, _ int64) (map[string]map[string]*openfgav1.TupleKey, error) {
const query = `
SELECT r.uid as role_uid, p.action, p.kind, p.identifier
FROM permission p
INNER JOIN role r ON p.role_id = r.id
LEFT JOIN builtin_role br ON r.id = br.role_id
WHERE r.name LIKE 'basic:%'
`
type Permission struct {
Action string `xorm:"action"`
Kind string
Identifier string
RoleUID string `xorm:"role_uid"`
}
var permissions []Permission
err := store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL(query).Find(&permissions)
})
if err != nil {
return nil, err
}
tuples := make(map[string]map[string]*openfgav1.TupleKey)
for _, p := range permissions {
subject := zanzana.NewTupleEntry(zanzana.TypeRole, p.RoleUID, "assignee")
tuple, ok := zanzana.TranslateToResourceTuple(subject, p.Action, p.Kind, p.Identifier)
if !ok {
continue
}
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
tuples[tuple.Object][tuple.String()] = tuple
}
return tuples, nil
}
}
// basicRoleBindingsCollects collects role bindings for basic roles
func basicRoleBindingsCollector(store db.DB) legacyTupleCollector {
return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) {
query := `
SELECT ou.org_id, u.uid as user_uid, ou.role as org_role
FROM org_user ou
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ou.user_id
WHERE ou.org_id = ?
AND NOT u.is_service_account
`
// FIXME: handle service admin role
type Binding struct {
UserUID string `xorm:"user_uid"`
OrgRole string `xorm:"org_role"`
}
var bindings []Binding
err := store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL(query, orgID).Find(&bindings)
})
if err != nil {
return nil, err
}
tuples := make(map[string]map[string]*openfgav1.TupleKey)
for _, b := range bindings {
subject := zanzana.NewTupleEntry(zanzana.TypeUser, b.UserUID, "")
tuple := &openfgav1.TupleKey{
User: subject,
Relation: zanzana.RelationAssignee,
Object: zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole(b.OrgRole), ""),
}
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
tuples[tuple.Object][tuple.String()] = tuple
}
return tuples, nil
}
}
func zanzanaCollector(relations []string) zanzanaTupleCollector {
return func(ctx context.Context, client zanzana.Client, object string, namespace string) (map[string]*openfgav1.TupleKey, error) {
// list will use continuation token to collect all tuples for object and relation
@@ -64,6 +64,18 @@ func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB,
zanzanaCollector(zanzana.ResourceRelations),
client,
),
newResourceReconciler(
"basic role permissions",
basicRolePermissionsCollector(store),
zanzanaCollector(zanzana.FolderRelations),
client,
),
newResourceReconciler(
"basic role bindings",
basicRoleBindingsCollector(store),
zanzanaCollector([]string{zanzana.RelationAssignee}),
client,
),
},
}
}
+11 -10
View File
@@ -19,16 +19,17 @@ import (
)
const (
ClientAPIKey = "auth.client.api-key" // #nosec G101
ClientAnonymous = "auth.client.anonymous"
ClientBasic = "auth.client.basic"
ClientJWT = "auth.client.jwt"
ClientExtendedJWT = "auth.client.extended-jwt"
ClientRender = "auth.client.render"
ClientSession = "auth.client.session"
ClientForm = "auth.client.form"
ClientProxy = "auth.client.proxy"
ClientSAML = "auth.client.saml"
ClientAPIKey = "auth.client.api-key" // #nosec G101
ClientAnonymous = "auth.client.anonymous"
ClientBasic = "auth.client.basic"
ClientJWT = "auth.client.jwt"
ClientExtendedJWT = "auth.client.extended-jwt"
ClientRender = "auth.client.render"
ClientSession = "auth.client.session"
ClientForm = "auth.client.form"
ClientProxy = "auth.client.proxy"
ClientSAML = "auth.client.saml"
ClientPasswordless = "auth.client.passwordless"
)
const (
+8 -1
View File
@@ -17,10 +17,12 @@ import (
"github.com/grafana/grafana/pkg/services/ldap/service"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/loginattempt"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
@@ -38,7 +40,7 @@ func ProvideRegistration(
features *featuremgmt.FeatureManager, oauthTokenService oauthtoken.OAuthTokenService,
socialService social.Service, cache *remotecache.RemoteCache,
ldapService service.LDAP, settingsProviderService setting.Provider,
tracer tracing.Tracer,
tracer tracing.Tracer, tempUserService tempuser.Service, notificationService notifications.Service,
) Registration {
logger := log.New("authn.registration")
@@ -78,6 +80,11 @@ func ProvideRegistration(
}
}
if cfg.PasswordlessMagicLinkAuth.Enabled && features.IsEnabledGlobally(featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
passwordless := clients.ProvidePasswordless(cfg, loginAttempts, userService, tempUserService, notificationService, cache)
authnSvc.RegisterClient(passwordless)
}
if cfg.AuthProxy.Enabled && len(proxyClients) > 0 {
proxy, err := clients.ProvideProxy(cfg, cache, proxyClients...)
if err != nil {
+335
View File
@@ -0,0 +1,335 @@
package clients
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/loginattempt"
"github.com/grafana/grafana/pkg/services/notifications"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
var (
errPasswordlessClientInvalidConfirmationCode = errutil.Unauthorized("passwordless.invalid.confirmation-code", errutil.WithPublicMessage("Invalid confirmation code"))
errPasswordlessClientTooManyLoginAttempts = errutil.Unauthorized("passwordless.invalid.login-attempt", errutil.WithPublicMessage("Login temporarily blocked"))
errPasswordlessClientInvalidEmail = errutil.Unauthorized("passwordless.invalid.email", errutil.WithPublicMessage("Invalid email"))
errPasswordlessClientCodeAlreadySent = errutil.Unauthorized("passwordless.invalid.code", errutil.WithPublicMessage("Code already sent to email"))
errPasswordlessClientInternal = errutil.Internal("passwordless.failed", errutil.WithPublicMessage("An internal error occurred in the Passwordless client"))
errPasswordlessClientMissingCode = errutil.BadRequest("passwordless.missing.code", errutil.WithPublicMessage("Missing code"))
)
const passwordlessKeyPrefix = "passwordless-%s"
var _ authn.RedirectClient = new(Passwordless)
func ProvidePasswordless(cfg *setting.Cfg, loginAttempts loginattempt.Service, userService user.Service, tempUserService tempuser.Service, notificationService notifications.Service, cache remotecache.CacheStorage) *Passwordless {
return &Passwordless{cfg, loginAttempts, userService, tempUserService, notificationService, cache, log.New("authn.passwordless")}
}
type PasswordlessCacheCodeEntry struct {
Email string `json:"email"`
ConfirmationCode string `json:"confirmation_code"`
SentDate string `json:"sent_date"`
}
type PasswordlessCacheEmailEntry struct {
Code string `json:"code"`
SentDate string `json:"sent_date"`
}
type Passwordless struct {
cfg *setting.Cfg
loginAttempts loginattempt.Service
userService user.Service
tempUserService tempuser.Service
notificationService notifications.Service
cache remotecache.CacheStorage
log log.Logger
}
type EmailForm struct {
Email string `json:"email" binding:"required,email"`
}
type PasswordlessForm struct {
Code string `json:"code" binding:"required"`
ConfirmationCode string `json:"confirmationCode" binding:"required"`
Name string `json:"name"`
Username string `json:"username"`
}
// Authenticate implements authn.Client.
func (c *Passwordless) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
var form PasswordlessForm
if err := web.Bind(r.HTTPRequest, &form); err != nil {
return nil, err
}
return c.authenticatePasswordless(ctx, r, form)
}
func (c *Passwordless) generateCodes() (string, string, error) {
alphabet := []byte("BCDFGHJKLMNPQRSTVWXZ")
confirmationCode, err := util.GetRandomString(8, alphabet...)
if err != nil {
return "", "", err
}
code, err := util.GetRandomString(32)
if err != nil {
return "", "", err
}
return confirmationCode, code, err
}
// RedirectURL implements authn.RedirectClient.
func (c *Passwordless) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) {
var form EmailForm
if err := web.Bind(r.HTTPRequest, &form); err != nil {
return nil, err
}
// TODO: add IP address validation
ok, err := c.loginAttempts.Validate(ctx, form.Email)
if err != nil {
return nil, err
}
if !ok {
return nil, errPasswordlessClientTooManyLoginAttempts.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked")
}
err = c.loginAttempts.Add(ctx, form.Email, web.RemoteAddr(r.HTTPRequest))
if err != nil {
return nil, err
}
code, err := c.startPasswordless(ctx, form.Email)
if err != nil {
return nil, err
}
return &authn.Redirect{
URL: c.cfg.AppSubURL + "/login?code=" + code,
Extra: map[string]string{"code": code},
}, nil
}
func (c *Passwordless) IsEnabled() bool {
return true
}
func (c *Passwordless) Name() string {
return authn.ClientPasswordless
}
func (c *Passwordless) startPasswordless(ctx context.Context, email string) (string, error) {
// 1. check if is existing user with email or user invite with email
var existingUser *user.User
var tempUsers []*tempuser.TempUserDTO
var err error
if !util.IsEmail(email) {
return "", errPasswordlessClientInvalidEmail.Errorf("invalid email %s", email)
}
cacheKey := fmt.Sprintf(passwordlessKeyPrefix, email)
_, err = c.cache.Get(ctx, cacheKey)
if err != nil && !errors.Is(err, remotecache.ErrCacheItemNotFound) {
return "", errPasswordlessClientInternal.Errorf("cache error: %s", err)
}
// if code already sent to email, return error
if err == nil {
return "", errPasswordlessClientCodeAlreadySent.Errorf("passwordless code already sent to email %s", email)
}
existingUser, err = c.userService.GetByEmail(ctx, &user.GetUserByEmailQuery{Email: email})
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return "", errPasswordlessClientInternal.Errorf("error retreiving user by email: %w - email: %s", err, email)
}
if existingUser == nil {
tempUsers, err = c.tempUserService.GetTempUsersQuery(ctx, &tempuser.GetTempUsersQuery{Email: email, Status: tempuser.TmpUserInvitePending})
if err != nil && !errors.Is(err, tempuser.ErrTempUserNotFound) {
return "", err
}
if tempUsers == nil {
return "", errPasswordlessClientInvalidEmail.Errorf("no user or invite found with email %s", email)
}
}
// 2. if existing user or temp user found, send email with passwordless link
confirmationCode, code, err := c.generateCodes()
if err != nil {
return "", err
}
emailCmd := notifications.SendEmailCommand{
To: []string{email},
Data: map[string]any{
"Email": email,
"ConfirmationCode": confirmationCode,
"Code": code,
"Expire": c.cfg.PasswordlessMagicLinkAuth.CodeExpiration.Minutes(),
},
}
if existingUser != nil {
emailCmd.Template = "passwordless_verify_existing_user"
} else {
emailCmd.Template = "passwordless_verify_new_user"
}
err = c.notificationService.SendEmailCommandHandler(ctx, &emailCmd)
if err != nil {
return "", err
}
sentDate := time.Now().Format(time.RFC3339)
value := &PasswordlessCacheCodeEntry{
Email: email,
ConfirmationCode: confirmationCode,
SentDate: sentDate,
}
valueBytes, err := json.Marshal(value)
if err != nil {
return "", err
}
cacheKey = fmt.Sprintf(passwordlessKeyPrefix, code)
err = c.cache.Set(ctx, cacheKey, valueBytes, c.cfg.PasswordlessMagicLinkAuth.CodeExpiration)
if err != nil {
return "", errPasswordlessClientInternal.Errorf("cache error: %s", err)
}
// second cache entry to lookup code by email
emailValue := &PasswordlessCacheEmailEntry{
Code: code,
SentDate: sentDate,
}
valueBytes, err = json.Marshal(emailValue)
if err != nil {
return "", err
}
cacheKey = fmt.Sprintf(passwordlessKeyPrefix, email)
err = c.cache.Set(ctx, cacheKey, valueBytes, c.cfg.PasswordlessMagicLinkAuth.CodeExpiration)
if err != nil {
return "", errPasswordlessClientInternal.Errorf("cache error: %s", err)
}
return code, nil
}
func (c *Passwordless) authenticatePasswordless(ctx context.Context, r *authn.Request, form PasswordlessForm) (*authn.Identity, error) {
code := form.Code
confirmationCode := form.ConfirmationCode
if len(code) == 0 || len(confirmationCode) == 0 {
return nil, errPasswordlessClientMissingCode.Errorf("no code provided")
}
cacheKey := fmt.Sprintf(passwordlessKeyPrefix, code)
jsonData, err := c.cache.Get(ctx, cacheKey)
if err != nil {
return nil, errPasswordlessClientInternal.Errorf("cache error: %s", err)
}
var codeEntry PasswordlessCacheCodeEntry
err = json.Unmarshal(jsonData, &codeEntry)
if err != nil {
return nil, errPasswordlessClientInternal.Errorf("failed to parse entry from passwordless cache: %w - entry: %s", err, string(jsonData))
}
if subtle.ConstantTimeCompare([]byte(codeEntry.ConfirmationCode), []byte(confirmationCode)) != 1 {
return nil, errPasswordlessClientInvalidConfirmationCode
}
ok, err := c.loginAttempts.Validate(ctx, codeEntry.Email)
if err != nil {
return nil, err
}
if !ok {
return nil, errPasswordlessClientTooManyLoginAttempts.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked")
}
if err := c.loginAttempts.Reset(ctx, codeEntry.Email); err != nil {
c.log.Warn("could not reset login attempts", "err", err, "username", codeEntry.Email)
}
usr, err := c.userService.GetByEmail(ctx, &user.GetUserByEmailQuery{Email: codeEntry.Email})
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return nil, errPasswordlessClientInternal.Errorf("error retreiving user by email: %w - email: %s", err, codeEntry.Email)
}
if usr == nil {
tempUsers, err := c.tempUserService.GetTempUsersQuery(ctx, &tempuser.GetTempUsersQuery{Email: codeEntry.Email, Status: tempuser.TmpUserInvitePending})
if err != nil {
return nil, err
}
if tempUsers == nil {
return nil, errPasswordlessClientInvalidEmail.Errorf("no user or invite found with email %s", codeEntry.Email)
}
createUserCmd := user.CreateUserCommand{
Email: codeEntry.Email,
Login: form.Username,
Name: form.Name,
}
// TODO: use user sync hook to create user
usr, err = c.userService.Create(ctx, &createUserCmd)
if err != nil {
return nil, err
}
for _, tempUser := range tempUsers {
if err := c.tempUserService.UpdateTempUserStatus(ctx, &tempuser.UpdateTempUserStatusCommand{Code: tempUser.Code, Status: tempuser.TmpUserCompleted}); err != nil {
return nil, err
}
}
}
// delete cache entry with code as key
err = c.cache.Delete(ctx, cacheKey)
if err != nil {
return nil, errPasswordlessClientInternal.Errorf("failed to delete entry from passwordless cache: %w - key: %s", err, cacheKey)
}
// delete cache entry with email as key
cacheKey = fmt.Sprintf(passwordlessKeyPrefix, codeEntry.Email)
err = c.cache.Delete(ctx, cacheKey)
if err != nil {
return nil, errPasswordlessClientInternal.Errorf("failed to delete entry from passwordless cache: %w - key: %s", err, cacheKey)
}
// user was found so set auth module in req metadata
r.SetMeta(authn.MetaKeyAuthModule, login.PasswordlessAuthModule)
return &authn.Identity{
ID: strconv.FormatInt(usr.ID, 10),
Type: claims.TypeUser,
OrgID: r.OrgID,
ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true},
AuthenticatedBy: login.PasswordlessAuthModule,
}, nil
}
@@ -0,0 +1,160 @@
package clients
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest"
"github.com/grafana/grafana/pkg/services/notifications"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/temp_user/tempusertest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func TestPasswordless_StartPasswordless(t *testing.T) {
type testCase struct {
desc string
email string
findUser bool
findTempUser bool
blockLogin bool
expectedErr error
}
tests := []testCase{
{
desc: "should succeed if user is found",
email: "user@domain.com",
findUser: true,
blockLogin: false,
},
{
desc: "should succeed if temp user is found",
email: "user@domain.com",
findUser: false,
findTempUser: true,
blockLogin: false,
},
{
desc: "should fail if user or temp user is not found",
email: "user@domain.com",
findUser: false,
findTempUser: false,
blockLogin: false,
expectedErr: errPasswordlessClientInvalidEmail.Errorf("no user or invite found with email user@domain.com"),
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
hashed, _ := util.EncodePassword("password", "salt")
userService := &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1, Email: "user@domain.com", Login: "user", Password: user.Password(hashed), Salt: "salt"},
}
las := &loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}
tus := &tempusertest.FakeTempUserService{}
tus.GetTempUsersQueryFN = func(ctx context.Context, query *tempuser.GetTempUsersQuery) ([]*tempuser.TempUserDTO, error) {
return []*tempuser.TempUserDTO{{
ID: 1,
Email: "user@domain.com",
Status: tempuser.TmpUserInvitePending,
EmailSent: true,
}}, nil
}
ns := notifications.MockNotificationService()
cache := remotecache.NewFakeCacheStorage()
if !tt.findUser {
userService.ExpectedUser = nil
userService.ExpectedError = user.ErrUserNotFound
}
if !tt.findTempUser {
tus.GetTempUsersQueryFN = func(ctx context.Context, query *tempuser.GetTempUsersQuery) ([]*tempuser.TempUserDTO, error) {
return nil, tempuser.ErrTempUserNotFound
}
}
c := ProvidePasswordless(setting.NewCfg(), las, userService, tus, ns, cache)
_, err := c.startPasswordless(context.Background(), tt.email)
assert.ErrorIs(t, err, tt.expectedErr)
})
}
}
func TestPasswordless_AuthenticatePasswordless(t *testing.T) {
type testCase struct {
desc string
email string
findUser bool
blockLogin bool
expectedErr error
expectedIdentity *authn.Identity
}
tests := []testCase{
{
desc: "should successfully authenticate user with correct passwordless magic link",
email: "user@domain.com",
findUser: true,
blockLogin: false,
expectedIdentity: &authn.Identity{
ID: "1",
Type: claims.TypeUser,
OrgID: 1,
AuthenticatedBy: login.PasswordlessAuthModule,
ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true},
},
},
{
desc: "should fail if login is blocked",
email: "user@domain.com",
findUser: true,
blockLogin: true,
expectedErr: errPasswordlessClientTooManyLoginAttempts.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked"),
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
hashed, _ := util.EncodePassword("password", "salt")
userService := &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1, Email: "user@domain.com", Login: "user", Password: user.Password(hashed), Salt: "salt"},
}
las := &loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}
tus := &tempusertest.FakeTempUserService{}
ns := notifications.MockNotificationService()
cache := remotecache.NewFakeCacheStorage()
if !tt.findUser {
userService.ExpectedUser = nil
userService.ExpectedError = user.ErrUserNotFound
}
c := ProvidePasswordless(setting.NewCfg(), las, userService, tus, ns, cache)
code, err := c.startPasswordless(context.Background(), tt.email)
if err != nil {
t.Fatalf("failed to start passwordless: %v", err)
}
form := &PasswordlessForm{
Code: code,
ConfirmationCode: ns.Email.Data["ConfirmationCode"].(string),
Name: "user",
Username: "username",
}
identity, err := c.authenticatePasswordless(context.Background(), &authn.Request{OrgID: 1}, *form)
assert.ErrorIs(t, err, tt.expectedErr)
assert.EqualValues(t, tt.expectedIdentity, identity)
})
}
}
+7 -11
View File
@@ -1,7 +1,6 @@
package server
import (
"errors"
"sync"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
@@ -25,19 +24,16 @@ var _ authzextv1.AuthzExtentionServiceServer = (*Server)(nil)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/server")
var errStoreNotFound = errors.New("store not found")
var errAuthorizationModelNotInitialized = errors.New("authorization model not initialized")
type Server struct {
authzv1.UnimplementedAuthzServiceServer
authzextv1.UnimplementedAuthzExtentionServiceServer
openfga openfgav1.OpenFGAServiceServer
logger log.Logger
modules []transformer.ModuleFile
storeMap map[string]storeInfo
storeLock *sync.Mutex
logger log.Logger
modules []transformer.ModuleFile
stores map[string]storeInfo
storesMU *sync.Mutex
}
type storeInfo struct {
@@ -65,9 +61,9 @@ func NewAuthzServer(cfg *setting.Cfg, openfga openfgav1.OpenFGAServiceServer) (*
func NewAuthz(openfga openfgav1.OpenFGAServiceServer, opts ...ServerOption) (*Server, error) {
s := &Server{
openfga: openfga,
storeLock: &sync.Mutex{},
storeMap: make(map[string]storeInfo),
openfga: openfga,
storesMU: &sync.Mutex{},
stores: make(map[string]storeInfo),
}
for _, o := range opts {
@@ -21,7 +21,7 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
}
func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info common.TypeInfo) (*authzv1.CheckResponse, error) {
storeInf, err := s.getNamespaceStore(ctx, r.Namespace)
storeInf, err := s.getStoreInfo(ctx, r.Namespace)
if err != nil {
return nil, err
}
@@ -64,7 +64,7 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info c
}
func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
storeInf, err := s.getNamespaceStore(ctx, r.Namespace)
storeInf, err := s.getStoreInfo(ctx, r.Namespace)
if err != nil {
return nil, err
}
@@ -24,7 +24,7 @@ func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzext
}
func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info common.TypeInfo) (*authzextv1.ListResponse, error) {
storeInf, err := s.getNamespaceStore(ctx, r.Namespace)
storeInf, err := s.getStoreInfo(ctx, r.Namespace)
if err != nil {
return nil, err
}
@@ -67,7 +67,7 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info
}
func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*authzextv1.ListResponse, error) {
storeInf, err := s.getNamespaceStore(ctx, r.Namespace)
storeInf, err := s.getStoreInfo(ctx, r.Namespace)
if err != nil {
return nil, err
}
@@ -52,8 +52,16 @@ func testList(t *testing.T, server *Server) {
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 2)
assert.Equal(t, res.GetFolders()[0], "1")
assert.Equal(t, res.GetFolders()[1], "3")
first := res.GetFolders()[0]
second := res.GetFolders()[1]
if first == "3" {
first, second = second, first
}
assert.Equal(t, first, "1")
assert.Equal(t, second, "3")
})
t.Run("user:5 should be get list all dashboards.grafana.app/dashboards in folder 1 with set relation", func(t *testing.T) {
@@ -13,7 +13,7 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze
ctx, span := tracer.Start(ctx, "authzServer.Read")
defer span.End()
storeInf, err := s.getNamespaceStore(ctx, req.Namespace)
storeInf, err := s.getStoreInfo(ctx, req.Namespace)
if err != nil {
return nil, err
}
+50 -130
View File
@@ -2,7 +2,6 @@ package server
import (
"context"
"errors"
"fmt"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
@@ -12,66 +11,35 @@ import (
"github.com/grafana/grafana/pkg/services/authz/zanzana/schema"
)
func (s *Server) getOrCreateStore(ctx context.Context, namespace string) (*openfgav1.Store, error) {
store, err := s.getStore(ctx, namespace)
if errors.Is(err, errStoreNotFound) {
var res *openfgav1.CreateStoreResponse
res, err = s.openfga.CreateStore(ctx, &openfgav1.CreateStoreRequest{Name: namespace})
if res != nil {
store = &openfgav1.Store{
Id: res.GetId(),
Name: res.GetName(),
CreatedAt: res.GetCreatedAt(),
}
s.storeMap[res.GetName()] = storeInfo{
Id: res.GetId(),
}
}
func (s *Server) getStoreInfo(ctx context.Context, namespace string) (*storeInfo, error) {
s.storesMU.Lock()
defer s.storesMU.Unlock()
info, ok := s.stores[namespace]
if ok {
return &info, nil
}
return store, err
}
func (s *Server) getStoreInfo(namespace string) (*storeInfo, error) {
info, ok := s.storeMap[namespace]
if !ok {
return nil, errStoreNotFound
store, err := s.getOrCreateStore(ctx, namespace)
if err != nil {
return nil, err
}
modelID, err := s.loadModel(ctx, store.GetId(), schema.SchemaModules)
if err != nil {
return nil, err
}
info = storeInfo{
Id: store.GetId(),
AuthorizationModelId: modelID,
}
s.stores[namespace] = info
return &info, nil
}
func (s *Server) getStore(ctx context.Context, namespace string) (*openfgav1.Store, error) {
if len(s.storeMap) == 0 {
err := s.initStores(ctx)
if err != nil {
return nil, err
}
}
storeInf, err := s.getStoreInfo(namespace)
if err != nil {
return nil, err
}
res, err := s.openfga.GetStore(ctx, &openfgav1.GetStoreRequest{
StoreId: storeInf.Id,
})
if err != nil {
return nil, err
}
store := &openfgav1.Store{
Id: res.GetId(),
Name: res.GetName(),
CreatedAt: res.GetCreatedAt(),
}
return store, nil
}
func (s *Server) initStores(ctx context.Context) error {
func (s *Server) getOrCreateStore(ctx context.Context, namespace string) (*openfgav1.Store, error) {
var continuationToken string
for {
@@ -81,13 +49,12 @@ func (s *Server) initStores(ctx context.Context) error {
})
if err != nil {
return fmt.Errorf("failed to load zanzana stores: %w", err)
return nil, fmt.Errorf("failed to load zanzana stores: %w", err)
}
for _, store := range res.GetStores() {
name := store.GetName()
s.storeMap[name] = storeInfo{
Id: store.GetId(),
for _, s := range res.GetStores() {
if s.GetName() == namespace {
return s, nil
}
}
@@ -99,10 +66,18 @@ func (s *Server) initStores(ctx context.Context) error {
continuationToken = res.GetContinuationToken()
}
return nil
res, err := s.openfga.CreateStore(ctx, &openfgav1.CreateStoreRequest{Name: namespace})
if err != nil {
return nil, err
}
return &openfgav1.Store{
Id: res.GetId(),
Name: res.GetName(),
}, nil
}
func (s *Server) loadModel(ctx context.Context, namespace string, modules []transformer.ModuleFile) (string, error) {
func (s *Server) loadModel(ctx context.Context, storeID string, modules []transformer.ModuleFile) (string, error) {
var continuationToken string
model, err := schema.TransformModulesToModel(modules)
@@ -110,41 +85,27 @@ func (s *Server) loadModel(ctx context.Context, namespace string, modules []tran
return "", err
}
store, err := s.getStore(ctx, namespace)
// ReadAuthorizationModels returns authorization models for a store sorted in descending order of creation.
// So with a pageSize of 1 we will get the latest model.
res, err := s.openfga.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{
StoreId: storeID,
PageSize: &wrapperspb.Int32Value{Value: 1},
ContinuationToken: continuationToken,
})
if err != nil {
return "", err
return "", fmt.Errorf("failed to load authorization model: %w", err)
}
for {
// ReadAuthorizationModels returns authorization models for a store sorted in descending order of creation.
// So with a pageSize of 1 we will get the latest model.
res, err := s.openfga.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{
StoreId: store.GetId(),
PageSize: &wrapperspb.Int32Value{Value: 20},
ContinuationToken: continuationToken,
})
if err != nil {
return "", fmt.Errorf("failed to load authorization model: %w", err)
for _, m := range res.GetAuthorizationModels() {
// If provided dsl is equal to a stored dsl we use that as the authorization id
if schema.EqualModels(m, model) {
return m.GetId(), nil
}
for _, m := range res.GetAuthorizationModels() {
// If provided dsl is equal to a stored dsl we use that as the authorization id
if schema.EqualModels(m, model) {
return m.GetId(), nil
}
}
// If we have not found any matching authorization model we break the loop and create a new one
if res.GetContinuationToken() == "" {
break
}
continuationToken = res.GetContinuationToken()
}
writeRes, err := s.openfga.WriteAuthorizationModel(ctx, &openfgav1.WriteAuthorizationModelRequest{
StoreId: store.GetId(),
StoreId: storeID,
TypeDefinitions: model.GetTypeDefinitions(),
SchemaVersion: model.GetSchemaVersion(),
Conditions: model.GetConditions(),
@@ -156,44 +117,3 @@ func (s *Server) loadModel(ctx context.Context, namespace string, modules []tran
return writeRes.GetAuthorizationModelId(), nil
}
func (s *Server) getNamespaceStore(ctx context.Context, namespace string) (*storeInfo, error) {
var storeInf *storeInfo
var err error
s.storeLock.Lock()
defer s.storeLock.Unlock()
storeInf, err = s.getStoreInfo(namespace)
if errors.Is(err, errStoreNotFound) || storeInf.AuthorizationModelId == "" {
storeInf, err = s.initNamespaceStore(ctx, namespace)
}
if err != nil {
return nil, err
}
return storeInf, nil
}
func (s *Server) initNamespaceStore(ctx context.Context, namespace string) (*storeInfo, error) {
store, err := s.getOrCreateStore(ctx, namespace)
if err != nil {
return nil, err
}
modules := schema.SchemaModules
modelID, err := s.loadModel(ctx, namespace, modules)
if err != nil {
return nil, err
}
if info, ok := s.storeMap[store.GetName()]; ok {
s.storeMap[store.GetName()] = storeInfo{
Id: info.Id,
AuthorizationModelId: modelID,
}
}
updatedInfo := s.storeMap[store.GetName()]
return &updatedInfo, nil
}
@@ -62,7 +62,7 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
require.NoError(t, err)
namespace := "default"
storeInf, err := srv.initNamespaceStore(context.Background(), namespace)
storeInf, err := srv.getStoreInfo(context.Background(), namespace)
require.NoError(t, err)
// seed tuples
@@ -13,13 +13,10 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth
ctx, span := tracer.Start(ctx, "authzServer.Write")
defer span.End()
storeInf, err := s.getNamespaceStore(ctx, req.Namespace)
storeInf, err := s.getStoreInfo(ctx, req.Namespace)
if err != nil {
return nil, err
}
if storeInf.AuthorizationModelId == "" {
return nil, errAuthorizationModelNotInitialized
}
writeTuples := make([]*openfgav1.TupleKey, 0)
for _, t := range req.GetWrites().GetTupleKeys() {
@@ -5,6 +5,22 @@ import (
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
)
const (
roleGrafanaAdmin = "Grafana Admin"
roleAdmin = "Admin"
roleEditor = "Editor"
roleViewer = "Viewer"
roleNone = "None"
)
var basicRolesTranslations = map[string]string{
roleGrafanaAdmin: "basic_grafana_admin",
roleAdmin: "basic_admin",
roleEditor: "basic_editor",
roleViewer: "basic_viewer",
roleNone: "basic_none",
}
type resourceTranslation struct {
typ string
group string
+12 -17
View File
@@ -73,19 +73,6 @@ const (
KindFolders string = "folders"
)
const (
RoleGrafanaAdmin = "Grafana Admin"
RoleAdmin = "Admin"
RoleEditor = "Editor"
RoleViewer = "Viewer"
RoleNone = "None"
BasicRolePrefix = "basic:"
BasicRoleUIDPrefix = "basic_"
GlobalOrgID = 0
)
var (
ToAuthzExtTupleKey = common.ToAuthzExtTupleKey
ToAuthzExtTupleKeys = common.ToAuthzExtTupleKeys
@@ -98,11 +85,11 @@ var (
ToOpenFGATupleKeyWithoutCondition = common.ToOpenFGATupleKeyWithoutCondition
)
// NewTupleEntry constructs new openfga entry type:id[#relation].
// Relation allows to specify group of users (subjects) related to type:id
// NewTupleEntry constructs new openfga entry type:name[#relation].
// Relation allows to specify group of users (subjects) related to type:name
// (for example, team:devs#member refers to users which are members of team devs)
func NewTupleEntry(objectType, id, relation string) string {
obj := fmt.Sprintf("%s:%s", objectType, id)
func NewTupleEntry(objectType, name, relation string) string {
obj := fmt.Sprintf("%s:%s", objectType, name)
if relation != "" {
obj = fmt.Sprintf("%s#%s", obj, relation)
}
@@ -121,6 +108,10 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf
return nil, false
}
if name == "*" {
return common.NewNamespaceResourceTuple(subject, m.relation, translation.group, translation.resource), true
}
if translation.typ == TypeResource {
return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, name), true
}
@@ -174,3 +165,7 @@ func TranslateToCheckRequest(namespace, action, kind, folder, name string) (*aut
return req, true
}
func TranslateBasicRole(name string) string {
return basicRolesTranslations[name]
}
-1
View File
@@ -7,7 +7,6 @@ type codeowner string
const (
grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad"
grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad"
grafanaExploreSquad codeowner = "@grafana/explore-squad"
grafanaDatavizSquad codeowner = "@grafana/dataviz-squad"
grafanaFrontendPlatformSquad codeowner = "@grafana/grafana-frontend-platform"
grafanaBackendGroup codeowner = "@grafana/grafana-backend-group"
+22 -13
View File
@@ -96,7 +96,7 @@ var (
Name: "correlations",
Description: "Correlations page",
Stage: FeatureStageGeneralAvailability,
Owner: grafanaExploreSquad,
Owner: grafanaDatavizSquad,
Expression: "true", // enabled by default
AllowSelfServe: true,
},
@@ -142,14 +142,6 @@ var (
FrontendOnly: true,
Owner: grafanaDatavizSquad,
},
{
Name: "autoMigrateXYChartPanel",
Description: "Migrate old XYChart panel to new XYChart2 model",
Stage: FeatureStageGeneralAvailability,
FrontendOnly: true,
Expression: "true", // enabled by default
Owner: grafanaDatavizSquad,
},
{
Name: "disableAngular",
Description: "Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.",
@@ -1232,7 +1224,7 @@ var (
Name: "queryLibrary",
Description: "Enables Query Library feature in Explore",
Stage: FeatureStageExperimental,
Owner: grafanaExploreSquad,
Owner: grafanaFrontendPlatformSquad,
FrontendOnly: false,
AllowSelfServe: false,
},
@@ -1456,7 +1448,7 @@ var (
{
Name: "singleTopNav",
Description: "Unifies the top search bar and breadcrumb bar into one",
Stage: FeatureStageExperimental,
Stage: FeatureStagePublicPreview,
FrontendOnly: true,
Owner: grafanaFrontendPlatformSquad,
},
@@ -1500,7 +1492,7 @@ var (
Name: "appSidecar",
Description: "Enable the app sidecar feature that allows rendering 2 apps at the same time",
Stage: FeatureStageExperimental,
Owner: grafanaExploreSquad,
Owner: grafanaFrontendPlatformSquad,
},
{
Name: "groupAttributeSync",
@@ -1584,8 +1576,9 @@ var (
{
Name: "preinstallAutoUpdate",
Description: "Enables automatic updates for pre-installed plugins",
Stage: FeatureStageExperimental,
Stage: FeatureStageGeneralAvailability,
Owner: grafanaPluginsPlatformSquad,
Expression: "true", // enabled by default
},
{
Name: "dashboardSchemaV2",
@@ -1601,6 +1594,15 @@ var (
Owner: grafanaAppPlatformSquad,
RequiresRestart: true,
},
{
Name: "passwordlessMagicLinkAuthentication",
Description: "Enable passwordless login via magic link authentication",
Stage: FeatureStageExperimental,
Owner: identityAccessTeam,
HideFromDocs: true,
HideFromAdminPage: true,
AllowSelfServe: false,
},
{
Name: "exploreMetricsRelatedLogs",
Description: "Display Related Logs in Explore Metrics",
@@ -1635,6 +1637,13 @@ var (
Owner: grafanaObservabilityTracesAndProfilingSquad,
FrontendOnly: true,
},
{
Name: "reportingUseRawTimeRange",
Description: "Uses the original report or dashboard time range instead of making an absolute transformation",
Stage: FeatureStagePublicPreview,
Owner: grafanaSharingSquad,
Expression: "false", // disabled by default
},
}
)
+7 -6
View File
@@ -9,14 +9,13 @@ publicDashboardsScene,GA,@grafana/sharing-squad,false,false,true
lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,false
featureHighlights,GA,@grafana/grafana-as-code,false,false,false
storage,experimental,@grafana/search-and-storage,false,false,false
correlations,GA,@grafana/explore-squad,false,false,false
correlations,GA,@grafana/dataviz-squad,false,false,false
autoMigrateOldPanels,preview,@grafana/dataviz-squad,false,false,true
autoMigrateGraphPanel,preview,@grafana/dataviz-squad,false,false,true
autoMigrateTablePanel,preview,@grafana/dataviz-squad,false,false,true
autoMigratePiechartPanel,preview,@grafana/dataviz-squad,false,false,true
autoMigrateWorldmapPanel,preview,@grafana/dataviz-squad,false,false,true
autoMigrateStatPanel,preview,@grafana/dataviz-squad,false,false,true
autoMigrateXYChartPanel,GA,@grafana/dataviz-squad,false,false,true
disableAngular,preview,@grafana/dataviz-squad,false,false,true
canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true
vizActions,experimental,@grafana/dataviz-squad,false,false,true
@@ -161,7 +160,7 @@ cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false
accessActionSets,GA,@grafana/identity-access-team,false,false,false
disableNumericMetricsSortingInExpressions,experimental,@grafana/observability-metrics,false,true,false
grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false
queryLibrary,experimental,@grafana/explore-squad,false,false,false
queryLibrary,experimental,@grafana/grafana-frontend-platform,false,false,false
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true
alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true
@@ -191,13 +190,13 @@ dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,
newFiltersUI,experimental,@grafana/dashboards-squad,false,false,false
lokiSendDashboardPanelNames,experimental,@grafana/observability-logs,false,false,false
alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true
singleTopNav,experimental,@grafana/grafana-frontend-platform,false,false,true
singleTopNav,preview,@grafana/grafana-frontend-platform,false,false,true
exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true
exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true
exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true
homeSetupGuide,experimental,@grafana/growth-and-onboarding,false,false,true
appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false
appSidecar,experimental,@grafana/explore-squad,false,false,false
appSidecar,experimental,@grafana/grafana-frontend-platform,false,false,false
groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false
alertingQueryAndExpressionsStepMode,experimental,@grafana/alerting-squad,false,false,true
improvedExternalSessionHandling,experimental,@grafana/identity-access-team,false,false,false
@@ -210,11 +209,13 @@ timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,fa
prometheusUsesCombobox,experimental,@grafana/observability-metrics,false,false,false
userStorageAPI,experimental,@grafana/plugins-platform-backend,false,false,false
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
preinstallAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false
preinstallAutoUpdate,GA,@grafana/plugins-platform-backend,false,false,false
dashboardSchemaV2,experimental,@grafana/dashboards-squad,false,false,true
playlistsWatcher,experimental,@grafana/grafana-app-platform-squad,false,true,false
passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false
exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true
enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false
zipkinBackendMigration,experimental,@grafana/oss-big-tent,false,false,false
enableSCIM,experimental,@grafana/identity-access-team,false,false,false
crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true
reportingUseRawTimeRange,preview,@grafana/sharing-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
9 lokiExperimentalStreaming experimental @grafana/observability-logs false false false
10 featureHighlights GA @grafana/grafana-as-code false false false
11 storage experimental @grafana/search-and-storage false false false
12 correlations GA @grafana/explore-squad @grafana/dataviz-squad false false false
13 autoMigrateOldPanels preview @grafana/dataviz-squad false false true
14 autoMigrateGraphPanel preview @grafana/dataviz-squad false false true
15 autoMigrateTablePanel preview @grafana/dataviz-squad false false true
16 autoMigratePiechartPanel preview @grafana/dataviz-squad false false true
17 autoMigrateWorldmapPanel preview @grafana/dataviz-squad false false true
18 autoMigrateStatPanel preview @grafana/dataviz-squad false false true
autoMigrateXYChartPanel GA @grafana/dataviz-squad false false true
19 disableAngular preview @grafana/dataviz-squad false false true
20 canvasPanelNesting experimental @grafana/dataviz-squad false false true
21 vizActions experimental @grafana/dataviz-squad false false true
160 accessActionSets GA @grafana/identity-access-team false false false
161 disableNumericMetricsSortingInExpressions experimental @grafana/observability-metrics false true false
162 grafanaManagedRecordingRules experimental @grafana/alerting-squad false false false
163 queryLibrary experimental @grafana/explore-squad @grafana/grafana-frontend-platform false false false
164 logsExploreTableDefaultVisualization experimental @grafana/observability-logs false false true
165 newDashboardSharingComponent GA @grafana/sharing-squad false false true
166 alertingListViewV2 experimental @grafana/alerting-squad false false true
190 newFiltersUI experimental @grafana/dashboards-squad false false false
191 lokiSendDashboardPanelNames experimental @grafana/observability-logs false false false
192 alertingPrometheusRulesPrimary experimental @grafana/alerting-squad false false true
193 singleTopNav experimental preview @grafana/grafana-frontend-platform false false true
194 exploreLogsShardSplitting experimental @grafana/observability-logs false false true
195 exploreLogsAggregatedMetrics experimental @grafana/observability-logs false false true
196 exploreLogsLimitedTimeRange experimental @grafana/observability-logs false false true
197 homeSetupGuide experimental @grafana/growth-and-onboarding false false true
198 appPlatformGrpcClientAuth experimental @grafana/identity-access-team false false false
199 appSidecar experimental @grafana/explore-squad @grafana/grafana-frontend-platform false false false
200 groupAttributeSync privatePreview @grafana/identity-access-team false false false
201 alertingQueryAndExpressionsStepMode experimental @grafana/alerting-squad false false true
202 improvedExternalSessionHandling experimental @grafana/identity-access-team false false false
209 prometheusUsesCombobox experimental @grafana/observability-metrics false false false
210 userStorageAPI experimental @grafana/plugins-platform-backend false false false
211 azureMonitorDisableLogLimit GA @grafana/partner-datasources false false false
212 preinstallAutoUpdate experimental GA @grafana/plugins-platform-backend false false false
213 dashboardSchemaV2 experimental @grafana/dashboards-squad false false true
214 playlistsWatcher experimental @grafana/grafana-app-platform-squad false true false
215 passwordlessMagicLinkAuthentication experimental @grafana/identity-access-team false false false
216 exploreMetricsRelatedLogs experimental @grafana/observability-metrics false false true
217 enableExtensionsAdminPage experimental @grafana/plugins-platform-backend false true false
218 zipkinBackendMigration experimental @grafana/oss-big-tent false false false
219 enableSCIM experimental @grafana/identity-access-team false false false
220 crashDetection experimental @grafana/observability-traces-and-profiling false false true
221 reportingUseRawTimeRange preview @grafana/sharing-squad false false false
+8 -4
View File
@@ -75,10 +75,6 @@ const (
// Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking
FlagAutoMigrateStatPanel = "autoMigrateStatPanel"
// FlagAutoMigrateXYChartPanel
// Migrate old XYChart panel to new XYChart2 model
FlagAutoMigrateXYChartPanel = "autoMigrateXYChartPanel"
// FlagDisableAngular
// Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.
FlagDisableAngular = "disableAngular"
@@ -863,6 +859,10 @@ const (
// Enables experimental watcher for playlists
FlagPlaylistsWatcher = "playlistsWatcher"
// FlagPasswordlessMagicLinkAuthentication
// Enable passwordless login via magic link authentication
FlagPasswordlessMagicLinkAuthentication = "passwordlessMagicLinkAuthentication"
// FlagExploreMetricsRelatedLogs
// Display Related Logs in Explore Metrics
FlagExploreMetricsRelatedLogs = "exploreMetricsRelatedLogs"
@@ -882,4 +882,8 @@ const (
// FlagCrashDetection
// Enables browser crash detection reporting to Faro.
FlagCrashDetection = "crashDetection"
// FlagReportingUseRawTimeRange
// Uses the original report or dashboard time range instead of making an absolute transformation
FlagReportingUseRawTimeRange = "reportingUseRawTimeRange"
)
+61 -17
View File
@@ -415,13 +415,16 @@
{
"metadata": {
"name": "appSidecar",
"resourceVersion": "1722872007883",
"creationTimestamp": "2024-09-09T12:45:05Z"
"resourceVersion": "1731608393499",
"creationTimestamp": "2024-09-09T12:45:05Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-11-14 18:19:53.499798 +0000 UTC"
}
},
"spec": {
"description": "Enable the app sidecar feature that allows rendering 2 apps at the same time",
"stage": "experimental",
"codeowner": "@grafana/explore-squad"
"codeowner": "@grafana/grafana-frontend-platform"
}
},
{
@@ -535,6 +538,7 @@
"name": "autoMigrateXYChartPanel",
"resourceVersion": "1722537244598",
"creationTimestamp": "2024-03-22T15:44:37Z",
"deletionTimestamp": "2024-11-14T01:17:06Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-08-01 18:34:04.598082 +0000 UTC"
}
@@ -841,16 +845,16 @@
{
"metadata": {
"name": "correlations",
"resourceVersion": "1720021873452",
"resourceVersion": "1731608393499",
"creationTimestamp": "2022-09-16T13:14:27Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC"
"grafana.app/updatedTimestamp": "2024-11-14 18:19:53.499798 +0000 UTC"
}
},
"spec": {
"description": "Correlations page",
"stage": "GA",
"codeowner": "@grafana/explore-squad",
"codeowner": "@grafana/dataviz-squad",
"allowSelfServe": true,
"expression": "true"
}
@@ -2462,6 +2466,20 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "passwordlessMagicLinkAuthentication",
"resourceVersion": "1730232874003",
"creationTimestamp": "2024-10-29T20:14:34Z"
},
"spec": {
"description": "Enable passwordless login via magic link authentication",
"stage": "experimental",
"codeowner": "@grafana/identity-access-team",
"hideFromAdminPage": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "pdfTables",
@@ -2602,13 +2620,17 @@
{
"metadata": {
"name": "preinstallAutoUpdate",
"resourceVersion": "1730904343525",
"creationTimestamp": "2024-11-06T14:45:43Z"
"resourceVersion": "1731581146864",
"creationTimestamp": "2024-11-06T14:45:43Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-11-14 10:45:46.864585 +0000 UTC"
}
},
"spec": {
"description": "Enables automatic updates for pre-installed plugins",
"stage": "experimental",
"codeowner": "@grafana/plugins-platform-backend"
"stage": "GA",
"codeowner": "@grafana/plugins-platform-backend",
"expression": "true"
}
},
{
@@ -2828,14 +2850,17 @@
{
"metadata": {
"name": "queryLibrary",
"resourceVersion": "1718727528075",
"resourceVersion": "1731609408207",
"creationTimestamp": "2022-10-07T18:31:45Z",
"deletionTimestamp": "2023-03-20T16:00:14Z"
"deletionTimestamp": "2023-03-20T16:00:14Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-11-14 18:36:48.207329 +0000 UTC"
}
},
"spec": {
"description": "Enables Query Library feature in Explore",
"stage": "experimental",
"codeowner": "@grafana/explore-squad"
"codeowner": "@grafana/grafana-frontend-platform"
}
},
{
@@ -2992,6 +3017,22 @@
"requiresRestart": true
}
},
{
"metadata": {
"name": "reportingUseRawTimeRange",
"resourceVersion": "1731604692461",
"creationTimestamp": "2024-11-13T15:48:28Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-11-14 17:18:12.461446 +0000 UTC"
}
},
"spec": {
"description": "Uses the original report or dashboard time range instead of making an absolute transformation",
"stage": "preview",
"codeowner": "@grafana/sharing-squad",
"expression": "false"
}
},
{
"metadata": {
"name": "rolePickerDrawer",
@@ -3047,12 +3088,15 @@
{
"metadata": {
"name": "singleTopNav",
"resourceVersion": "1724861961030",
"creationTimestamp": "2024-08-29T08:48:32Z"
"resourceVersion": "1731664029942",
"creationTimestamp": "2024-08-29T08:48:32Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-11-15 09:47:09.942158 +0000 UTC"
}
},
"spec": {
"description": "Unifies the top search bar and breadcrumb bar into one",
"stage": "experimental",
"stage": "preview",
"codeowner": "@grafana/grafana-frontend-platform",
"frontend": true
}
@@ -3483,4 +3527,4 @@
}
}
]
}
}
+9 -8
View File
@@ -25,14 +25,15 @@ type Store interface {
const (
// modules
PasswordAuthModule = "password"
APIKeyAuthModule = "apikey"
SAMLAuthModule = "auth.saml"
LDAPAuthModule = "ldap"
AuthProxyAuthModule = "authproxy"
JWTModule = "jwt"
ExtendedJWTModule = "extendedjwt"
RenderModule = "render"
PasswordAuthModule = "password"
PasswordlessAuthModule = "passwordless"
APIKeyAuthModule = "apikey"
SAMLAuthModule = "auth.saml"
LDAPAuthModule = "ldap"
AuthProxyAuthModule = "authproxy"
JWTModule = "jwt"
ExtendedJWTModule = "extendedjwt"
RenderModule = "render"
// OAuth provider modules
AzureADAuthModule = "oauth_azuread"
GoogleAuthModule = "oauth_google"
@@ -452,6 +452,8 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
if hasAccess(ac.EvalAny(
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsWrite),
)) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Notification policies", SubTitle: "Determine how alerts are routed to contact points", Id: "am-routes", Url: s.cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
}
@@ -337,6 +337,7 @@ func TestRouteGetRuleByUID(t *testing.T) {
createdRules := gen.With(
gen.WithUniqueGroupIndex(), gen.WithUniqueID(),
gen.WithEditorSettingsSimplifiedQueryAndExpressionsSection(true),
gen.WithEditorSettingsSimplifiedNotificationsSection(true),
).GenerateManyRef(3)
require.Len(t, createdRules, 3)
ruleStore.PutRule(context.Background(), createdRules...)
@@ -356,6 +357,7 @@ func TestRouteGetRuleByUID(t *testing.T) {
require.Equal(t, expectedRule.RuleGroup, result.GrafanaManagedAlert.RuleGroup)
require.Equal(t, expectedRule.Title, result.GrafanaManagedAlert.Title)
require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection)
require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedNotificationsSection)
})
t.Run("error when fetching rule with non-existent UID", func(t *testing.T) {
Loaded 100 of 261 files, more files were not shown because too many files have changed in this diff. Show more